-
Notifications
You must be signed in to change notification settings - Fork 79
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
Better JSDoc Types: Emit @typedefs, use standard primitive types, & more #13
Conversation
RE:Union types, It looks like the Java SDK, which currently doesn't have an open source generator, has deserialization logic that treats "collapsed" and "non-collapsed" unions differently. The HTTP documentation shows all error types, which are all union types AFAIK, as having the following structure: {
"error_summary": "string",
"error": {
".tag": "union tag",
// extra properties on this particular tag, if any
}
} But |
Elated to see this, but it will take me some time to review this week. To answer your last question, we should have a type definition like this:
However, we don't because stone doesn't have support for generics. To get around this, these types are simply handwritten into our SDKs and thus absent from our specs. |
Re union types: Specifying a union variant with an object that has a We introduced the compact form to make it easier to specify when using a command-line tool like curl or httpie. We also did it for aesthetic reasons, which is why you'll see our documentation page use it for example arguments, but not for results; we always return the verbose form. There's a short blurb here in stone's documentation. |
Is it JSDoc or TypeScript that isn't happy with having multiple typedef files? |
@@ -4,6 +4,8 @@ | |||
import json | |||
import six | |||
import sys | |||
import os |
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.
Alphabetize imports.
Multiple typedef files are fine, but we need a mechanism to avoid re-emitting the same type twice in different files. In the case of Dropbox, there are certain structs that are shared amongst the team and non-team routes. Simply emitting every typedef referenced by the routes would result in typedef duplicates.
That sounds like a great idea! I'll do that. |
@braincore / @rileytomasek I have updated the PR with a new I updated the PR description, and linked to new documentation / JS files generated with these changes. Let me know what you think! |
@@ -29,21 +29,21 @@ command-line interface (CLI):: | |||
usage: stone [-h] [-v] [--clean-build] [-f FILTER_BY_ROUTE_ATTR] | |||
[-w WHITELIST_NAMESPACE_ROUTES | -b BLACKLIST_NAMESPACE_ROUTES] | |||
generator output [spec [spec ...]] | |||
|
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 configured Visual Studio Code to auto-remove extraneous whitespace at the end of lines when saving. Sorry for cluttering up the PR! lmk if I should reintroduce this whitespace.
@braincore Do I need to toggle some type of generator configuration option to enable |
I fixed a few bugs, and updated the documentation / generated code links in the original post. |
@jvilk: You'll need to use the |
@braincore Ahh, I overlooked that before. OK, I verified that the generator properly processes extra arguments and adds them to the relevant data type(s). |
poke I'm assuming everyone is busy. Let me know if there's anything else I should do for this PR (besides squash the commits once reviewed). |
""" | ||
|
||
|
||
class JavascriptTypeGenerator(CodeGenerator): |
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.
nit: Type -> Types
Sorry for the delay. I just hooked this up into my local The one omission I see is struct polymorphism. For example, the output of |
Once you address struct polymorphism, and the minor n its, I think we'll be good to go! Thanks again! |
If I understand correctly: I'll have to check if any of a type's parent types is polymorphic. If it is, the type has an implicit Let me give this a shot... |
Actually, this is a tad confusing. You're saying that {
".tag": "file",
"name": ...,
"id": ...,
(etc)
} In a standard stone union, Is this interpretation correct? |
Yes, that's correct. The (short) serialization docs for them are here: https://github.com/dropbox/stone/blob/master/doc/json_serializer.rst#L71 How well will this map to JSDocs? |
@braincore Well, there's two ways to handle this.
I think the second option is better. |
@braincore OK, I've addressed the nits, and added support for structural polymorphism! The links to generated docs / JavaScript have been updated in the OP. |
@braincore btw, with this change, structs with enumerated subtypes that are a closed union (which I believe is the criteria for In this case, should I not emit a typing for the struct? |
|
||
jsdoc_tag_union = fmt_jsdoc_union(tag_values) | ||
self.emit_wrapped_text('@property {%s} .tag - Tag identifying the subtype variant.' % jsdoc_tag_union) | ||
else: |
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 case is unnecessary. If one of the subtypes is directly referenced, then there will be no .tag
attribute since it isn't ambiguous. An example of this is the filesUpload
route. It will return a FilesFileMetadata
object (you can only upload a file, not a folder), and there's no .tag
attribute.
Also, we don't support multiple layers of inheritance with enumerated subtypes. It's always only a single root struct and one layer of children (we might expand the design later).
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 always only a single root struct and one layer of children
OK; some of the helper functions in the code seemed to assume otherwise, so I went with a general design.
If one of the subtypes is directly referenced, then there will be no .tag attribute since it isn't ambiguous.
Oh, that complicates things. I could do the following:
- Make
.tag
an optional field on these data types. - Make a second data type for
FilesFileMetadataVariant
or something that has.tag
, whileFilesFileMetadata
does not. - Ignore the
.tag
option entirely.
I think 1 is the cleanest solution. I can put a comment, like "Tag identifying this subtype variant. This field is only present when needed to discriminate between multiple possible types."
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.
Also, unless you have a good reason for me not to, I think I'll keep the more general code in there as it seems consistent with other design choices. For example, is_member_of_enumerated_subtypes_tree
recursively climbs up the inheritance hierarchy.
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.
Okay, that's fine :)
Verified that the enumerated subtypes output looks correct in JSDocs! Last bit is to remove some unnecessary logic, and then I think we're ready to merge this! |
OK, I think I came up with a useful compromise. I don't think the logic is unnecessary. When the docs points to As a compromise, I made it an optional field with a clarifying description. Does this work for you, or do you have a better idea in mind? EDIT: I didn't update the docs at the top after this change, since it's so minor. |
Haven't had a chance to test this today. But in preparation for landing, please squash your commits and write a descriptive commit log. |
Just tested. Looks great! Just waiting for the squash. |
4e79a61
to
16f6afb
Compare
This commit adds a js_types generator that generates JSDoc @typedefs for Stone struct and union types. JSDoc annotations for routes, which the js_client generator generates, now reference these new @typedefs. As a consequence of this change, I have migrated the --extra-args parameter from js_client to js_types, since that parameter augments specific types with additional fields. Below, I detail how the generator maps each Stone data type to JSDoc. Namespaces ---------- To preserve namespaces, each user-defined type is prefixed with its Pascal-cased namespace. For example, the Metadata struct in the files namespace is emitted as FilesMetadata. Basic Types ----------- Most basic types are emitted as-before, except now using lowercase type names (e.g., number instead of Number). This convention is consistent with JSDoc's own documentation (see http://usejsdoc.org/tags-type.html), as well as alternative JavaScript type systems (e.g., TypeScript). A few other adjustments have been made, however: * Void type is emitted as void rather than null. * JSDoc does not have an official void type, but ESLint's valid-jsdoc linting rule recognizes the void type. This is also consistent with TypeScript. * valid-jsdoc documentation: http://eslint.org/docs/rules/valid-jsdoc * List types are emitted as Array.<ElementType> rather than just Array. * Timestamps are emitted as Timestamp, which is JSDoc @typedef'd to string. * Previously, these were incorrectly emitted as "Object". Aliases ------- All aliases are unwrapped and emitted as their base type. If desired in the future, every alias can be @typedef'd into their actual type, but that may be confusing to developers perusing the documentation. Structs ------- Each struct is mapped to a JSDoc @typedef. Each field is emitted as a @Property on that @typedef. Unfortunately, JSDoc typedefs do not support inheritance like JSDoc @classes, so every struct @typedef contains the fields from the struct's parent type(s). (Note: @Class would be inappropriate to use for Stone data types, as these are expected to be nominal types backed by a constructor.) More information on typedefs here: http://usejsdoc.org/tags-typedef.html Unions ------ Each union is mapped to a JSDoc @typedef with the appropriate fields. All unions have a .tag property that contains a string value indicating the union's variant. The JSDoc lists the type of .tag as the union of all known string values (e.g., 'foo' | 'bar'). Strictly speaking, open unions may have more possible values than listed; should this subtlety become an issue, the generator could be modified to emit the generic string type in the union (e.g., 'foo' | 'bar' | string). All of the fields on the union that are only present on a particular variant are marked as an optional @typedef @Property, with a description mentioning which .tag value it is tied to. If a union uses inheritance, the @typedef simply incorporates all of the options from the parent type(s). Struct Polymorphism ------------------- If a struct enumerates its subtypes, the generator emits an annotation for the struct's .tag field that lets developers discriminate between the possible subtypes. Like with unions, the .tag field's type is a union of all known possible values (e.g., 'foo' | 'bar'). All subtypes contain an *optional* .tag field typed with that subtype's tag value, since the .tag field may be omitted when the subtype is referenced directly. Any references to a struct that enumerates its subtypes are replaced with a union of all possible subtypes. For example, consider the following Stone types: ``` struct Resource union file File folder Folder path String struct ResourceHolder value Resource ``` In the JSDoc, the `value` field on `ResourceHolder` is typed as `File | Folder | Resource`. If `Resource` were a closed union, then it would be typed as `File | Folder`. Nullable Types -------------- Nullable types are mapped to optional parameters, indicating that the particular field may or may not be present on the data type. Routes ------ js_client emits a method for each route that accepts a single parameter corresponding to that route's Arg type and returns a Promise that resolves to the route's Result type (or rejects with the route's Error type, wrapped in a hardcoded Error object). js_client now emits proper JSDoc annotations for each of these types. The arg parameter references the appropriate type name emitted by js_types, and the return value is a Promise parameterized with the result type and the error type (`Promise.<ResultType, Error.<ErrorType>>`), where `Error` is the hardcoded object that wraps an API error.
16f6afb
to
e638edd
Compare
Squashed with descriptive commit message! (Which could probably be spun off into a doc if you feel like maintaining the description.) |
Thanks for the detailed commit message! Rebased in. I'll be updating the Dropbox JS SDK soon. |
This PR changes the following in the Javascript generator, and addresses this issue in the Dropbox JS SDK repository:
Changes
Changes to
js_client
generator:Void
type tovoid
rather thannull
.void
type. It also lines up with TypeScript'svoid
type.Promise
object, with success and error types (e.g.Promise.<FilesMetadata, Error.<FilesAlphaGetMetadataError>>
)Array.<ComposedType>
rather than justArray
.Timestamp
, which it JSDoc@typedef
s tostring
-e
parameter to the newjs_types
generator...Adds a
js_types
generator that:--extra-args
to add extra parameters to route argument types.Possible Improvements
@typedef
for alias types. For now, we unwrap all aliases.Example Output
Here is the Dropbox Javascript SDK documentation and here is the SDK route/typedef code generated with these generator changes.
Since this is a rather large change to your generator, I'm expecting you to have plenty of comments and nits regarding the code I've written. Python isn't my primary language, so it's possible that some of my code isn't idiomatic / doesn't follow your style guide.
Let me know what you think!