-
Notifications
You must be signed in to change notification settings - Fork 350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add and pass regression tests for PerseusItem parser #1907
Changes from all commits
64b514d
367580b
647f501
3e2175b
3d27504
c5a0cf8
67cf0d9
9df5170
8fb3143
29a9466
8cadbcf
b0ed10b
d910fe6
adcea9c
7aca4b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@khanacademy/perseus": patch | ||
--- | ||
|
||
Internal: add and pass regression tests for `PerseusItem` parser. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import { | |
optional, | ||
string, | ||
} from "../general-purpose-parsers"; | ||
import {defaulted} from "../general-purpose-parsers/defaulted"; | ||
|
||
import {parseWidget} from "./widget"; | ||
|
||
|
@@ -19,7 +20,7 @@ export const parseCategorizerWidget: Parser<CategorizerWidget> = parseWidget( | |
items: array(string), | ||
categories: array(string), | ||
randomizeItems: boolean, | ||
static: boolean, | ||
static: defaulted(boolean, () => false), | ||
values: array(number), | ||
highlightLint: optional(boolean), | ||
linterContext: optional( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This definitely shouldn't be part of the serialized widget options for categorizer. It is a prop passed down by the editor components during content authoring. I can imagine this is in our published content because of a bug somewhere. No action required, just musing. |
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,17 @@ | ||||||
import {number, object, record, string} from "../general-purpose-parsers"; | ||||||
import {defaulted} from "../general-purpose-parsers/defaulted"; | ||||||
|
||||||
import type {PerseusImageDetail} from "../../../perseus-types"; | ||||||
import type {Parser} from "../parser-types"; | ||||||
|
||||||
export const parseImages: Parser<{[key: string]: PerseusImageDetail}> = | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a required change, but the keys in these image maps should be the url to the image. Clarifying the label may be helpful for others.
Suggested change
|
||||||
defaulted( | ||||||
record( | ||||||
string, | ||||||
object({ | ||||||
width: number, | ||||||
height: number, | ||||||
}), | ||||||
), | ||||||
() => ({}), | ||||||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,32 @@ | ||
import { | ||
array, | ||
number, | ||
object, | ||
optional, | ||
record, | ||
string, | ||
} from "../general-purpose-parsers"; | ||
import {array, object, optional, string} from "../general-purpose-parsers"; | ||
import {defaulted} from "../general-purpose-parsers/defaulted"; | ||
|
||
import {parseImages} from "./images-map"; | ||
import {parseWidgetsMap} from "./widgets-map"; | ||
|
||
import type {PerseusRenderer} from "../../../perseus-types"; | ||
import type {Parser} from "../parser-types"; | ||
|
||
export const parsePerseusRenderer: Parser<PerseusRenderer> = object({ | ||
content: string, | ||
// This module has an import cycle with parseWidgetsMap, because the | ||
// `group` widget can contain another renderer. | ||
// The anonymous function below ensures that we don't try to access | ||
// parseWidgetsMap before it's defined. | ||
widgets: defaulted( | ||
(rawVal, ctx) => parseWidgetsMap(rawVal, ctx), | ||
() => ({}), | ||
), | ||
metadata: optional(array(string)), | ||
images: defaulted( | ||
record( | ||
string, | ||
object({ | ||
width: number, | ||
height: number, | ||
}), | ||
export const parsePerseusRenderer: Parser<PerseusRenderer> = defaulted( | ||
object({ | ||
// TODO(benchristel): content is also defaulted to empty string in | ||
// renderer.tsx. See if we can remove one default or the other. | ||
Comment on lines
+12
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been thinking about this as I work through this PR. Some of our Perseus widgets/components provide default props. There are places we might be able to de-duplicate these defaults to only be defaulted in our parsers. But there may be some complications on the editing side as I'm pretty sure they depend on those default props. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been thinking about this too. I believe the correct place to add default values is in the parsing layer. That ensures that old content always renders consistently even if we change the default props used by the widget editors. We may still need For example, imagine we changed the default If, on the other hand, we defaulted By defaulting values in the parser, we can decouple the "defaults for legacy content" from the "defaults for content creators". |
||
content: defaulted(string, () => ""), | ||
// This module has an import cycle with parseWidgetsMap, because the | ||
// `group` widget can contain another renderer. | ||
// The anonymous function below ensures that we don't try to access | ||
// parseWidgetsMap before it's defined. | ||
widgets: defaulted( | ||
(rawVal, ctx) => parseWidgetsMap(rawVal, ctx), | ||
() => ({}), | ||
), | ||
() => ({}), | ||
), | ||
}); | ||
metadata: optional(array(string)), | ||
images: parseImages, | ||
}), | ||
// Default value | ||
() => ({ | ||
content: "", | ||
widgets: {}, | ||
images: {}, | ||
}), | ||
); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -7,6 +7,7 @@ import { | |||||
optional, | ||||||
string, | ||||||
} from "../general-purpose-parsers"; | ||||||
import {defaulted} from "../general-purpose-parsers/defaulted"; | ||||||
|
||||||
import {parseWidget} from "./widget"; | ||||||
import {parseWidgetsMap} from "./widgets-map"; | ||||||
|
@@ -19,7 +20,7 @@ export const parseRadioWidget: Parser<RadioWidget> = parseWidget( | |||||
object({ | ||||||
choices: array( | ||||||
object({ | ||||||
content: string, | ||||||
content: defaulted(string, () => ""), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if Something like this: defaulted<T|Defaulted>(Parser<T>, Defaulted | () => Defaulted) That'd allow many of these usages to be simpler, like this:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The danger with that approach is that if the default value is an object or array, the same instance will be reused for every default. E.g. defaulted(array(someParser), []) If some other code later mutates the array, it might affect unrelated code that happens to be holding a reference to the same value. This kind of "aliasing" bug has bitten me many times in my career, and it's always a nightmare. I believe we have a pretty good culture of avoiding unnecessary mutation, but aliasing bugs are so bad that I feel it's better to be safe than sorry. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. Let's leave it as a function then. |
||||||
clue: optional(string), | ||||||
correct: optional(boolean), | ||||||
isNoneOfTheAbove: optional(boolean), | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,9 +20,11 @@ export function parseWidget<Type extends string, Options>( | |
alignment: optional(string), | ||
options: parseOptions, | ||
key: optional(number), | ||
version: object({ | ||
major: number, | ||
minor: number, | ||
}), | ||
version: optional( | ||
object({ | ||
major: number, | ||
minor: number, | ||
Comment on lines
+25
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to other notes about defaults at different parts of Perseus, I wonder if we should make these defaulted() to No action needed, just an idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those defaults would let us handle data like Evergreen parsers are a possibility, but I think we might sometimes want versioning to help make complex migrations typesafe and predictable. I do wish There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for
|
||
}), | ||
), | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't the
?
make this optional (ie. making 'undefined' a legal "value")?Seeing both
?
andundefined
feels like we're saying the same thing twice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I thought
prefix?: string
wouldn't allow the field to be explictly set toundefined
, but that appears not to be the case.