Skip to content

Instantly share code, notes, and snippets.

@ethanresnick
Last active November 12, 2015 09:41
Show Gist options
  • Save ethanresnick/fdbf87a4ee3b0442a7c6 to your computer and use it in GitHub Desktop.
Save ethanresnick/fdbf87a4ee3b0442a7c6 to your computer and use it in GitHub Desktop.
Namespacing Strategies
/**
* In this first approach, every key (those added by extensions
* and those in the base spec) is part of the same global namespace.
* Registration is required for an extension to claim a key (it can
* only claim one or two, to keep the space free), as otherwise two
* extensions could conflict. The extension then puts all the data it
* wants to add as the value for that key. The base spec is only able
* to add keys that haven't already been registered. And a consumer can
* determine which extension governs each key's value simply by
* memorizing in advance which extensions (that it's interested in) use
* which keys. Therefore, looking at the extensions in use (as listed in
* the profile link) isn't even strictly necessary; the list of extensions
* just becomes a more visible signal to the recipient of the document
* to turn on plugins it may have for any of the recognized extensions.
*/
{
"links": {
"self": "http://example.com/articles",
"profile": ["http://jsonapi.org/extensions/write-templates", "http://jsonapi.org/extensions/readonly"]
},
// Without any explicit indicator, the value of the template key below would
// be known to be defined by the "http://jsonapi.org/extensions/write-templates"
// extension,because that'd be the sole extension that claims the "template" key
// in this single registry.
"template": {
// Some write template for creating an article
},
"data": [{
"type": "articles",
"id": "1",
"attributes": { /* ... */ }, // etc
//the readonly extension adds this new key listing immutable fields.
"readonly": ["tags", "author", "title"]
}, {
/* more resource objects */
}]
}
/**
* One objection to the global registry approach above is that it may not
* provide enough good names. For example, suppose there's some
* commonly-requested feature for which it's hard to create a good, general
* design, but that many API implementors want to start using right away.
* A resource's available "actions" (in the Siren/HATEOS/issue #684 sense)
* might be such a feature. For this feature, there are "x many" decent
* names (x = 10 easily; maybe 50 max?). And the fear is that there might be
* (many) more than x attempts at designing this feature before some design
* gets it "right". Therefore, as each attempt must be registered, there
* might not be any good names left when the right design comes along.
* (This is more likely if we hope to restrict names to single words, to
* avoid extensions taking a position on camelCased vs dasherized.)
*
* It's unclear to me how real a problem this is. RFC 6648 (worth reading)
* doesn't think it's a problem. But that RFC was also written in the context
* of protocols like email and HTTP, for which getting an extension off the
* ground takes, I imagine, much more work than it would in the JSON API
* context. So this could be an issue for us and, if it is, it's clearly one
* that gets bigger if it's easier to register an extension--and super easy
* registration is something we should want.
*
* So, the approach below takes the above problem very seriously (an assumption
* that future approaches back off of) and goes the XML namespace route: a prefix
* for each extension is defined in its `"profile"` link, and that prefix is used
* to reference its keys throughout the document; unprefixed keys (i.e., those
* missing a colon) implicitly are from the namespace of the nearest enclosing key
* that is prefiexd.
*
* By opening up the available key space, such an approach solves the narrow
* "what if there are too many extensions doing x" problem, but also obviates other
* management strategies + limitations imposed by a global registry. Specifically:
*
* - Each extension can now claim an unlimited number of keys and those keys
* can be scattered all over the document, if appropriate. By contrast, the
* global registry approach encourages us to give one or two keys to each
* extension and have those keys store object "bags" under which all the
* extension's real data lives. [Note how, below, the validation constraint
* from one extension is mixed into the write template from another.]
*
* - The base spec (and each extension) can add new keys over time without
* considering what's still left unregistered.
*
* - Registration also wouldn't need to be required, since there's no risk
* of conflict when everything's namespaced...but we'd want to strongly
* encourage registration anyway, so this may not be a virtue.
*/
{
"links": {
"self": "http://example.com/articles",
"profile": [
{"href": "http://jsonapi.org/extensions/write-templates", "prefix": "templates"},
{"href": "http://jsonapi.org/extensions/validation-constraints", "prefix": "valid"}
]
},
"templates:create": {
"fields": [{
"name": "title"
"type": "string",
"validate": {
"maxlength": "50",
"valid:title-case": true
}
}]
},
"data": [{
/* some resource object here, with new key */
"new-base-spec-key": ""
}]
}
/**
* The XML namespace-like approach given above is more powerful than
* the global registry approach, but it's _a lot_ more complicated to
* implement, which is a huge drawback. Document recipients can no
* longer simply memorize which keys to look for and where when a
* given extension is in use; instead, they must read prefixes to
* assemble key names, and they must account for nasty scoping rules
* (e.g. in he above example, the "fields" key could also have been
* written "templates:fields", but wasn't because the "templates:"
* part is implied by its parent). Also, the ability to add fragments
* from one extension into the data provided by another extension
* (as with `"valid:title-case"` above), while powerful, is also a
* huge complexity increase that may not justify itself.
*
* Accordingly, I went looking for hybrid approaches that would:
*
* - avoid the risk from the global-registry approach of running out
* of good names for commonly-registered features.
*
* - avoid the complications of implementing xml-style namespaces;
* that is: every key name should be constant and scoping rules
* should be minimized
*
* - leave open the door for mixing bits of different extensions together,
* as in the `"valid:title-case"` example, but assume that such a
* feature isn't needed for now (YAGNI). (The most compelling case for
* such a feature is probably the JSON-LD integration extension, since
* a server might want to annotate with JSON-LD the data added by a
* different extesnion. But JSON-LD will likely need to be a special
* case anyway, so I'm not worrying too much about this right now.)
*
* - get rid of the `"x:y"` style keys, which are kinda ugly.
*
* My first attempt at such a hybrid was to take the global registry version
* and simply allow multiple extensions to register the same key. It would
* then be up to the document producer to only use one extension at a time
* that claims a given key. That is, e.g., twenty different actions-related
* extensions could all register under the key "actions", but each API server
* would only return one of those extensions' data in its payload.
*
* This approach still allows recipients to memorize the keys for each extension
* they know how to handle (since, if a given extension is listed, it will always
* put its data under the key it registered with) and it doesn't have any of the
* namespace-induced complexity. It also gets rid of the ugly "x:y"-style keys,
* though such keys could be introduced in a backwards-compatible way, leaving
* open the possibility of interspersing data from different extensions in the
* future.
*/
// The JSON below is the same as from the first example, but the difference is
// that template and readonly could have different content if a different extension
// were listed in `"links"`.
{
"links": {
"self": "http://example.com/articles",
"profile": ["http://jsonapi.org/extensions/write-templates", "http://jsonapi.org/extensions/readonly"]
},
"template": {
// Some write template for creating an article
},
"data": [{
"type": "articles",
"id": "1",
"attributes": { /* ... */ },
"readonly": ["tags", "author", "title"]
}, {
/* more resource objects */
}]
}
/**
* The only problem with the above approach is that it doesn't let two extensions
* that reserve the same key be used in the same doc (whereas any extensions could
* be combined under the first two approaches).
*
* This issue is mitigated somewhat by extension negotiation. For example, if the
* server supports multiple "actions" extensions (to maximize compatibility with
* clients), it can know which flavor to return to each client by looking at the
* `"profile"` parameter in the client's Accept header. Conversely, if a client
* wants to send data with an extension applied, and it supports multiple
* suitable extensions that use the same key, it can send an OPTIONS request
* first to see which extension it should apply.
*
* Still, this negotiation has some costs: in the `Accept` case, cacheability
* is hurt, and the OPTIONS case requires an extra response. Therefore, it might
* be nice to relax the restrictions on the keys being totally memorizable if
* it would allow multiple extensions registering the same key to coexist.
*
* Relaxing this restriction naively looks like this:
*/
{
"links": {
"self": "http://example.com/articles/1",
"profile": [
{"href": "http://jsonapi.org/extensions/actions", "as": "actions"},
{"href": "http://jsonapi.org/extensions/actions-2", "as": "actions-2"}
]
},
// Actiions in two formats, for maximum compatibility
"actions": {
},
"actions-2": {
}
"data": { /* a resoruce object */ }
}
/**
* The problem with the above, though, is that it means that any
* extension's data can appear under any key name at any time.
* And that means it's impossible to add new members to the base
* spec while ensuring that those new members won't conflict
* with an extension in use somewhere. So, if the base spec adds
* "new-member-x":
* 1. any existing API using an extension under the name
* "new-member-x" will not be ableto use the new base spec
* member (without risking breaking some of its clients); and
*
* 2. any client that encounters "new-member-x" must inspect the
* `links` to figure out whether the base spec's meaning is
* in play or an extension's.
*
* So, I think the solution to that is to require that any aliases
* (i.e. the value at the `"as"` key above) start with an underscore
* followed by the key name, and to strongly discourage them in general.
* So the JSON would be:
*/
{
"links": {
"self": "http://example.com/articles",
"profile": [
"http://jsonapi.org/extensions/readonly",
"http://jsonapi.org/extensions/actions",
{"href": "http://jsonapi.org/extensions/actions-other-proposal", "as": "_actions2"},
]
},
"actions": {
// Some HATEOS actions
},
"_actions2": {
// Actions in a different format than the above. 90%+ of the time,
// these wouldn't be included and document producers would instead
// choose which actions format to send using negotiation. But, in
// the rare cases where that's unsuitable (e.g. if two extensions
// that really need to be used together somehow get registered with
// the same key, or if the client is sending the data and can't
// afford an OPTIONS), this is an escape hatch for letting the
// extensions coexist.
},
"data": [{
"type": "articles",
"id": "1",
"attributes": { /* ... */ },
"readonly": ["tags", "author", "title"]
}, {
/* more resource objects */
}]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy