-
Notifications
You must be signed in to change notification settings - Fork 0
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
Community review #1
base: community-review
Are you sure you want to change the base?
Conversation
This static method could be highly opinionated as there are various strategies one can use so let’s start from the original one.
r2-shared/Format/Format.ts
Outdated
export interface IFormat { | ||
name: string; | ||
mediaType: MediaType; | ||
fileExtension: string; | ||
} |
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.
Bear with me, because I don't know much about TypeScript, but why use an interface in this case? Format
will have only one implementation.
I'm also a bit confused how Format
is created in its constructor, why not constructor(name: string, mediaType: MediaType, fileExtension: string)
?
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.
It’s more like I wanted to keep named parameters in static methods for more clarity as this can turn super ugly when done directly in constructor
, cf. this SO question.
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.
@danielweck During the call there was a consensus in favor of using interfaces to create anonymous object arguments to replace the concept of "named parameters". But we're not sure about the naming of such interfaces. Is there a standard convention for this? Or a more idiomatic way to tackle the issue of named parameters?
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.
How about using a type alias on the object structure instead of an interface? This doesn't solve the naming issue, but at least it's clear that it's not a type to be implemented.
type FormatDefinition = { name: string }
class Format {
public name: string;
constructor(params: FormatDefinition) {
this.name = params.name;
};
}
A few other ideas for the name: FormatDefinition
, FormatConstructor
, FormatSchema
, FormatData
and FormatLike
, as seen in this PR.
Technically we could also ditch the name entirely. But I don't know enough TypeScript to see if that could be a problem:
class Format {
public name: string;
constructor(params: { name: string }) {
this.name = params.name;
};
}
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.
I have come across a number of Javascript / TypeScript APIs where objects are used as function parameters in order to pass multiple atomic pieces of data (quick example from the top of my head: React component props and state), but there are many instances of the opposite design pattern (extreme example: curry-ing which “spreads” individual arguments over multiple function calls).
I think that generalising the use of objects to carry multiple function arguments is an unusual approach, but as far as I know it is not an anti-pattern ... though I am sure quite a few JS/TS programmers will find this “weird”, so I am not convinced this will improve the developer experience (DX) across the board.
Note that there is a runtime cost incurred when creating objects instead of just spreading the function arguments the “normal” way. Garbage collection in JS engines is pretty good, and the Readium SDK is not a “real time” / “high performance” system ... but still, this is something to keep in mind in addition to DX considerations.
Another potential problem is that variable with primitive types passed as function arguments are sometimes not used as constants within the closure scope, and instead are modified directly in order to avoid creating unnecessary stack allocations (i.e. this is an implementation detail below the API surface). With objects, there is the potential problem of mutating properties in one function scope, and inadvertently changing the value in another. Just a thought.
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.
Thanks for chiming in @danielweck. Yeah that would probably be overkill. If I understood correctly, @JayPanoz planned to use these mostly for large constructors with multiple parameters of the same type. One could argue that Format
doesn't fit the bill, but it would probably be safer for Link
, Metadata
, etc. Because adding a new parameter constructors in an update means that the constructor could break silently if the type of the new parameter is the same.
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.
Yes, it is quite common in JavaScript/TypeScript APIs to use a "config" object argument which properties are assigned defaults when not defined on the caller's side. I can see how this would work for constructors or other initializers. That being said, it is also quite common to have such "config" function argument alongside other arguments, so the design pattern is quite open.
Personally I would favour consistency, so if "object with named properties is used to pass function arguments" gets applied to constructors, then I would prefer the design pattern to be used for all other public API functions (which then becomes quite cumbersome, as noted in a previous comment).
const components = mediaType.replace(/\s/g, "").split(";"); | ||
const types = components[0].split("/"); | ||
if (types.length === 2) { | ||
this.type = types[0].toLowerCase(); | ||
this.subtype = types[1].toLowerCase(); | ||
} | ||
|
||
let parameters: ParametersMap = {}; | ||
for (let i = 1; i < components.length; i++) { | ||
const component = components[i].split("="); | ||
if (component.length === 2) { | ||
const key = component[0]; | ||
const value = component[1]; | ||
parameters[key] = value; | ||
} | ||
} | ||
this.parameters = parameters; | ||
|
||
let parametersString: string = ""; | ||
for (const p in parameters) { | ||
const value = parameters[p]; | ||
parametersString += `;${p}=${value}`; | ||
} | ||
this.string = `${this.type}/${this.subtype}${parametersString}`; | ||
|
||
this.encoding = parameters["encoding"]; |
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.
This part is quite complicated, so I think it would be important to add some unit tests once we have the test suite set up, based on Swift for example.
Also for the comparison functions below.
r2-shared/Format/MediaType.ts
Outdated
} | ||
|
||
public matches(other: MediaType | string): boolean { | ||
return this.contains(other); |
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.
matches()
is not just an alias to contains()
, but needs to work in both directions. So it should be something like this:
return this.contains(other) || other.contains(this)
But I think the focus should be on implementing unit tests, they should catch that kind of errors.
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.
Note: Resolve Pending as I wanted to doublecheck.
@@ -0,0 +1 @@ | |||
export type Properties = "mathml" | "onix" | "remote-resources" | "js" | "svg" | "xmp"; |
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.
Is there a finite amount of contains
properties? On mobile we used a simple string array, but an enum might be better if we're sure it's all there is.
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.
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.
In TS string literal unions are an alternative to enums.
I prefer them as they are more lightweight in terms of writing them and with the final output (no runtime artifacts)
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.
Looks like this is an exhaustive type indeed, let's confirm that on the call with Hadrien and probably we can migrate to enums on mobile.
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.
@jccr Would you consequently prefer ReadingProgression and EPUBLayout as string literal unions too? Let’s try to keep it consistent.
Make fragments, locations and text non-nullable, rename DOMRangeInfo → DOMRange and StartEndInfo → DOMRangePoint
That should prevent confusion and services need further discussions in the Web context.
This branch should make it easier for the community to review.