mnot / I-D

My Internet-Drafts
https://mnot.github.io/I-D/
Other
98 stars 38 forks source link

The json:api:home specification #280

Closed krainboltgreene closed 4 years ago

krainboltgreene commented 6 years ago

Alright, so I've been working on a sister specification to jsonhome (which has been released for a while) with the first implementation being a ruby gem called jsonapi-home. However, since then I've used it in production and found some weak points that I'm looking to improve upon in a second version. Before committing to that version I was hoping to get insight into my design and maybe contribute to the discourse around jsonhome.

type signature

Below is the flowtype signature for a payload.

{
  data: {
    id: string,
    type: string,
    attributes: {
      tags: string | Array<string>,
      resource: string,
      intent: string,
      verb: string,
      details: null | string,
      uri-template: string,
      availability: null | "healthy" | "unhealthy" | "down" | "unknown",
      status: "usable" | "deprecated" | "gone",
      supported: boolean,
      allowed-path-parameters: Array<{
        ref: string,
        required: boolean,
        rel: "plain" | "jsonpointer" | "jsonpath",
        href: null | string | Array<string>
      }>,
      allowed-query-parameters: Array<{
        ref: string,
        required: boolean,
        rel: "plain" | "jsonpointer" | "jsonpath",
        href: null | string | Array<string>
      }>,
      allowed-headers: Array<{
        ref: string,
        required: boolean,
        rel: "plain" | "jsonpointer" | "jsonpath",
        href: null | string | Array<string>
      }>,
      returnable-headers: Array<{
        ref: string,
        rel: "plain" | "jsonpointer" | "jsonpath",
        href: null | string | Array<string>
      }>,
      allowed-body-parameters: Array<{
        ref: string,
        required: boolean,
        rel: "plain" | "jsonpointer" | "jsonpath",
        href: null | string | Array<string>
      }>,
      returnable-body-parameters: Array<{
        ref: string,
        rel: "plain" | "jsonpointer" | "jsonpath",
        href: null | string | Array<string>
      }>,
    }
  }
}

With an example:

GET https://discovery.poutineer.com/v1/jsonapi-home-resources/v1-accounts-show
Accept: application/vnd.api+json
{
  "data": {
    "id": "v1-accounts-show",
    "type": "jsonapi-home-resources",
    "attributes": {
      "tags": ["v1"],
      "resource": "accounts",
      "intent": "show",
      "verb": "GET",
      "details": "Returns an account's data",
      "uri-template": "https://origin.poutineer.com/v1/accounts/{id}",
      "availability": "healthy",
      "status": "usable",
      "supported": true,
      "allowed-path-parameters": [
        {
          "ref": "{id}",
          "rel": "plain"
        }
      ],
      "allowed-query-parameters": [
        {
          "ref": "/fields",
          "rel": "jsonpath"
        },
        {
          "ref": "/include",
          "rel": "jsonpath"
        },
        {
          "ref": "/sort",
          "rel": "jsonpath"
        },
        {
          "ref": "/filter",
          "rel": "jsonpath"
        }
      ],
      "allowed-headers": [
        {
          "ref": "Content-Type",
          "required": true,
          "rel": "plain",
          "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type"
        },
        {
          "ref": "Authorization",
          "required": true,
          "rel": "plain",
          "href": [
            "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization",
            "https://tools.ietf.org/html/rfc6750",
            "https://mnot.github.io/I-D/how-did-that-get-into-the-repo/"
          ]
        },
        {
          "ref": "If-Modified-Since",
          "required": false,
          "rel": "plain",
          "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since"
        },
        {
          "ref": "If-Match",
          "required": false,
          "rel": "plain",
          "href": [
            "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match"
          ]
        }
      ],
      "returnable-headers": [
        {
          "ref": "Date",
          "rel": "plain",
          "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date"
        },
        {
          "ref": "Set-Cookie",
          "rel": "plain",
          "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie"
        },
        {
          "ref": "If-Modified-Since",
          "rel": "plain",
          "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since"
        },
        {
          "ref": "Etag",
          "rel": "plain",
          "href": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Etag"
        }
      ],
      "returnable-body-parameters": [
        {
          "ref": "/data/id",
          "rel": "jsonpointer"
        },
        {
          "ref": "/data/type",
          "rel": "jsonpointer"
        },
        {
          "ref": "/data/attributes/email",
          "rel": "jsonpointer"
        },
        {
          "ref": "$.data.relationships.posts[].data.id",
          "rel": "jsonpath"
        },
        {
          "ref": "$.data.relationships.posts[].data.type",
          "rel": "jsonpath"
        },
        {
          "ref": "$.data.relationships.posts[].links.self",
          "rel": "jsonpath"
        },
        {
          "ref": "/data/links",
          "rel": "jsonpath"
        },
        {
          "ref": "$.included[?(@.type=posts)].id",
          "rel": "jsonpath"
        },
        {
          "ref": "$.included[?(@.type=posts)].type",
          "rel": "jsonpath"
        },
        {
          "ref": "$.included[?(@.type=posts)].attributes.title",
          "rel": "jsonpath"
        },
        {
          "ref": "$.included[?(@.type=posts)].attributes.body",
          "rel": "jsonpath"
        },
        {
          "ref": "$.included[?(@.type=posts)].relationships",
          "rel": "jsonpath"
        },
        {
          "ref": "/jsonapi/version",
          "rel": "jsonpointer"
        },
        {
          "ref": "/meta/api/version",
          "rel": "jsonpointer"
        },
        {
          "ref": "/links/discovery/href",
          "rel": "jsonpointer"
        }
      ],
      "created-at": "2018-08-14T20:25:00-07:00Z",
      "updated-at": "2018-08-15T10:23:30-07:00Z"
    }
  }
}

comparing json:api:home to jsonhome

Differences:

  1. Instead of preconditionRequired, authSchemes, acceptRanges, and acceptPrefer we have allowed-headers
  2. Instead of hrefVars we have allowed-path-parameters
  3. Instead of status we have availability, status, and supported
  4. Instead of docs we have details, intent, json:api links, and also each parameter has an href
  5. Instead of acceptPatch, acceptPost, and acceptPut have verb

Extras:

  1. json:api compliant: filters, sparse fields, sorting
  2. self-described
  3. has allowed-headers and returnable-headers which hint at header usage
  4. has payload hinting with allowed-path-parameters, allowed-query-parameters, allowed-body-parameters, and returnable-body-parameters
  5. updated-at timestamp so a client can determine how old a defined is
  6. grouping mechanisms (tags, resource)
  7. details, which human descriptions
krainboltgreene commented 6 years ago

The more I think about it, the more I want to offload "parameter" hinting to a single point (for example a link to a json-schema document), but I'm not sure which of these approaches:

handrews commented 6 years ago

@krainboltgreene JSON Hyper-Schema has separate schemas for the body (although it is more in terms of target representation vs data for processing), URI Template variables (including but not limited to the query string), and headers. This works well as they have different usage and re-use patterns.

krainboltgreene commented 6 years ago

@handrews This is fantastic to learn! Thanks so much!

krainboltgreene commented 5 years ago

I'm going to be releasing a ruby gem that takes rails' internal routing API and generates these endpoints dynamically (cached, of course) and then another library that will take these http resources and turn them into documentation.

mnot commented 4 years ago

Hey,

Thanks for the input, but this is really difficult to reason about, since it's basically a complete (and competing) proposal. If you have specific feedback or suggestions for json home, please do file those as discrete issues.