Skip to content
hoegertn edited this page Nov 21, 2012 · 22 revisions

Second Strawman proposal:

(also - apologies to @hoegertn, did not mean to clobber your changes on strawman1 yesterday!) [hoegertn] no problem

I've made the following changes from the first example:

  • Renamed 'body' to 'accepts' to make it's purpose more clear.
  • Changed the 'representations' array items to objects as in alternative #1, with the exception of the 'body' element.
  • Changed example request bodies to strings.
  • Added an optional "headers" element to example requests.
    • [hoegertn] how are headers described? You may want to use custom headers
    • [grncdr] Headers are always a flat key-value object. Mapping header names to header strings.
    • [hoegertn] Sure, but how can I specify which headers I can use? I think we need a definition section for request and response headers per method with doc and pattern info.
    • [grncdr] Ah, I understand what you mean now. Explicit description of custom headers for a resource. I've tried adding it to the revised example below.
  • Added a query param to the request template as an alternative to alternative #1's 'style' member.
    • The above is also an example of using an enum for parameter descriptions.
  • Removed redundant type: "string" elements from parameter descriptions.
  • Added more comments.
{
  // Keeping the top level open in the schema means we could allow other metadata such as API versions,
  // authors, and so on. (Idea lifted from https://github.com/SPORE/specifications)
  // [hoegertn] I really like this idea
  resources: [
    {
      id: "LocalizedMessage",
      doc: "A localized message",
      path: "/{language}/{messageId}{?seasonal}", // representing query params with L3 URI templates
      params: { // Request parameters only, JSON schemas with type: 'string' implied
        locale: {
          description: "A standard locale string, e.g. \"en_US.utf-8\"",
          pattern: "[a-z]+(_[A-Z]+)?(\\.[a-z-]+)?"
        },
        messageId: {
          description: "A free-form message string",
          pattern: "[a-z_]+"
        },
        seasonal: {
          description: "Whether the message is seasonal.",
          enum: ['true', 'false', 'yes', 'no'],
        } 
      },
      methods: {
        PUT: {
          doc: "Update or create a message",
          statusCodes: [ 201 ],
          accepts: [   // Representations accepted by the method on this resource.
            { type: "text/plain" },
            { type: "application/json", schema: "http://some.json/schema" }
          ],
          examples: [
            {
              path: "/en_US/greeting",
              body: "Hello, world!"
            },
            {
              path: "/en_US/greeting",
              headers: {"content-type": "application/json"},
              body: '{"message":"Hello, world!"}',
            },
            {
              path: "/en_US/greeting",
              headers: {"content-type": "application/xml"},
              body: "<message>Hello, world</message>",
            }
          ]
        },
        GET: {
          doc: "Retrieve a message",
          statusCodes: [ 200, 302 ],
          accepts: null, // Possible to explicitly indicate that this method takes no request body. 
        }
      },
      representations: [ // Response representations this resource provides
        { type: 'application/json', schema: 'http://my.json/schema' },
        { type: 'application/xml', schema: 'http://my.xml/dtd' }, // Externally referenced schema
        { type: 'text/plain' },
      ]
    },
    {
      // This resource has no human-readable documentation, but still provides some info on how to use it.
      id: "FallbackLocale",
      methods: {
        GET: { statusCodes: [ 200 ] },
        PUT: { statusCodes: [ 201 ] }
      },
      parameters: {
        locale: { pattern: "[a-z]+(_[A-Z]+)?(\\.[a-z-]+)?" }
      }
    }
  ]
}

(Perceived) Improvements:

  • This version handles multiple representation formats for requests and responses independently of each other.
  • It is independent of JSON, (could just as easily be represented in XML, Avro, MessagePack, etc). While this may not be of immediate necessity, it seems prudent to try not to bind ourselves to strongly to one serialization format.

[hoegertn] Ideas:

  • Add an object array with the schema URI to URL mapping
  • Rename and split the doc element to name (a short name) and description (a longer description of the element)
  • Do we want to move the representation block into the method or is it only possible to have one format for every method of a resource?

[saary] Ideas:

  • In some places we can support both a string value or a more descriptive json value. for example the accepts field can be a string in simple cases and an object with type and schema in others.
    • [hoegertn] there I see a problem with language with types. If I want to use Java I would have a List<AcceptTypes> object, but strings in this list will break the runtime.
    • [grncdr] Agree with hoegertn here. Even in dynamic languages, the need to check whether you're dealing with string or object/hash/dict isn't worth the trade-off. Of course, the application API's by which this metadata is defined could accept the simple string and turn it into { type: <string type> } before serving it to clients.
  • Using doc/description across the board. We should stick to one term as it means the same all around.
    • [grncdr] Agreed, I'll open a "decision" issue for it. (I really like that approach btw!)
  • Having both a pattern and enum fields makes for a bit of inconsistency (you could have both). I think I would start with only one of them as enum could be represented as "option1|option2|option3" which is pretty self explanatory.
    • [hoegertn] Check!
    • [grncdr] - I agree. I was using things that were "built-in" to JSON schemas, but now that schema definitions for requests/responses are no longer inline, requiring a JSON schema implementation to validate some strings is overkill.

Strawman 2.1

This revision changes:

  • 'doc' becomes 'description' for consistency. (The actual property name is still in question though)
  • Changed 'pattern' to 'validations' array, only using regex patterns.
  • Moved 'response' array into the method description object.
  • Add description object for custom headers.
  • [hoegertn] Changed response representation to same format as "accept"
  • [saary] Changed status codes to be a dictionary which also holds the description for the status.
  • [hoegertn] Added schema block to define used schemas
OPTIONS * HTTP/1.1
Accept: application/json;

{
  "schemas" : {
    "http://some.json/msg" : {
      "type" : "inline",
      "schema" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string"
          },
          "content" : {
            "type" : "string"
          }
        }
      }
    },
    "http://some.json/external" : {
      "type" : "url",
      "url" : "http://some.json/concrete.url.json"
    }
  },
  // Headers can be described at the server level and/or per-method
  "headers": {
    "request": {
      "Authorization": {
        "description": "This server uses a custom authentication scheme. See http://myapi.com/docs/auth",
        "required": true
      }
    },
    "response": {
      "X-RateLimit-Total": {
        "description": "The number of API calls allowed per-hour"
      },
      "X-RateLimit-Remaining": {
        "description": "Number of requests remaining until next refill"
      },
      "X-RateLimit-Reset": {
        "description": "The time at which X-RateLimit-Remaining will be reset back to X-RateLimit-Total"
      }
    }
  },
  "resources": [
    {
      "id": "LocalizedMessage",
      "description": "A localized message",
      "path": "/{language}/{messageId}{?seasonal}", // representing query params with L3 URI templates
      "params": { // URI parameters descriptions
        "locale": {
          "description": "A standard locale string, e.g. \"en_US.utf-8\"",
          "validations": [
            { "type": "match", "pattern": "[a-z]+(_[A-Z]+)?(\\\\.[a-z-]+)?" }
          ]
        },
        "messageId": {
          "description": "A free-form message string",
          "validations": [ { "type": "match", "pattern": "[a-z_]+" } ]
        },
        "seasonal": {
          "description": "Whether the message is seasonal.",
          "validations": [ { "type": "match", "pattern": "^(true|false|yes|no)$" } ]
        }
      },
      "methods": {
        "PUT": {
          "description": "Update or create a message",
          "statusCodes": { "201": "Created" },
          "accepts": [   // Representations accepted by the method on this resource.
            { "type": "text/plain" },
            { "type": "application/json", "schema": "http://some.json/schema" }
          ],
          "headers": { // Request headers only, response headers are defined under 'response'
            "X-User-Token": {
              "description": "Used to identify the user creating the message"
            }
          },
          "response": { // Response representations this resource/method provides
            "types": [
              { "type": "text/plain" },
              { "type": "application/json", "schema": "http://my.json/schema" },
              { "type": "application/xml", "schema": "http://my.xml/dtd" }
            ],
            "headers": {
              "X-What-Response-Header-Would-Only-Show-Up-For-One-Resource?": {}
            }
          },
          "examples": [
            {
              "path": "/en_US/greeting",
              "body": "Hello, world!"
            },
            {
              "path": "/en_US/greeting",
              "headers": {"content-type": "application/json"},
              "body": "{\\"message\\":\\"Hello, world!\\"}!" // Should bodies *always* be strings?
            },
            {
              "path": "/en_US/greeting",
              "headers": {"content-type": "application/xml"},
              "body": "<message>Hello, world</message>"
            }
          ]
        },
        "GET": {
          "description": "Retrieve a message",
          "statusCodes": { 
             "200": "Message retrieved successfully", 
             "401": "Parameter error"
          },
          "accepts": [] // Explicitly indicate that this method takes no request body. 
        }
      }
    },
    {
      // This resource has no human-readable documentation, but still provides some info on how to use it.
      "id": "FallbackLocale",
      "methods": {
        "GET": { "statusCodes": { "200": "OK" } },
        "PUT": { "statusCodes": { "201": "Created" } }
      },
      "params": {
        "locale": { 
          "validations": [ { "type": "match", "pattern": "[a-z]+(_[A-Z]+)?(\\\\.[a-z-]+)?" } ]
        }
      }
    }
  ]
}

This method of representing headers trades off a little bit of implementation complexity (clients might have to know about global and per-method headers) for a big reduction in repetition. What do you think?

[saary] I think it is a good trade off, it is a good balance. We already have it with parameters.

Strawman 2.2

This revision changes:

  • 'doc' becomes 'description' for consistency.
  • Changed 'pattern' to 'validations' array, only using regex patterns.
  • Moved 'response' array into the method description object.
  • Add description object for custom headers.
  • [hoegertn] Changed response representation to same format as "accept"
  • MediaType for RestDoc
OPTIONS * HTTP/1.1
Accept: application/x-restdoc+json;

{
  // Headers can be described at the server level and/or per-method
  "headers": {
    "request": {
      "Authorization": {
        "description": "This server uses a custom authentication scheme. See http://myapi.com/docs/auth",
        "required": true
      }
    },
    "response": {
      "X-RateLimit-Total": {
        "description": "The number of API calls allowed per-hour"
      },
      "X-RateLimit-Remaining": {
        "description": "Number of requests remaining until next refill"
      },
      "X-RateLimit-Reset": {
        "description": "The time at which X-RateLimit-Remaining will be reset back to X-RateLimit-Total"
      }
    }
  },
  "resources": [
    {
      "id": "LocalizedMessage",
      "description": "A localized message",
      "path": "/{locale}/{messageId}{?seasonal}", // representing query params with L3 URI templates
      "params": { // URI parameters descriptions
        "locale": {
          "description": "A standard locale string, e.g. \"en_US.utf-8\"",
          "validations": [
            { "type": "match", "pattern": "[a-z]+(_[A-Z]+)?(\\\\.[a-z-]+)?" }
          ]
        },
        "messageId": {
          "description": "A free-form message string",
          "validations": [ { "type": "match", "pattern": "[a-z_]+" } ]
        },
        "seasonal": {
          "description": "Whether the message is seasonal.",
          "validations": [ { "type": "match", "pattern": "^(true|false|yes|no)$" } ]
        }
      },
      "methods": {
        "PUT": {
          "description": "Update or create a message",
          "statusCodes": { "201": "Created" },
          "accepts": [   // Representations accepted by the method on this resource.
            { "type": "text/plain" },
            { "type": "application/json", "schema": "http://some.json/schema" }
          ],
          "headers": { // Request headers only, response headers are defined under 'response'
            "X-User-Token": {
              "description": "Used to identify the user creating the message"
            }
          },
          "response": { // Response representations this resource/method provides
            "types": [
              { "type": "text/plain" },
              { "type": "application/json", "schema": "http://my.json/schema" },
              { "type": "application/xml", "schema": "http://my.xml/dtd" }
            ],
            "headers": {
              "X-What-Response-Header-Would-Only-Show-Up-For-One-Resource?": {}
            }
          },
          "examples": [
            {
              "path": "/en_US/greeting",
              "body": "Hello, world!"
            },
            {
              "path": "/en_US/greeting",
              "headers": {"content-type": "application/json"},
              "body": "{\\"message\\":\\"Hello, world!\\"}!" // Should bodies *always* be strings?
            },
            {
              "path": "/en_US/greeting",
              "headers": {"content-type": "application/xml"},
              "body": "<message>Hello, world</message>"
            }
          ]
        },
        "GET": {
          "description": "Retrieve a message",
          "statusCodes": { 
             "200": "Message retrieved successfully", 
             "401": "Parameter error"
          },
          "accepts": [] // Explicitly indicate that this method takes no request body. 
        }
      }
    },
    {
      // This resource has no human-readable documentation, but still provides some info on how to use it.
      "id": "FallbackLocale",
      "methods": {
        "GET": { "statusCodes": { "200": "OK" } },
        "PUT": { "statusCodes": { "201": "Created" } }
      },
      "params": {
        "locale": { 
          "validations": [ { "type": "match", "pattern": "[a-z]+(_[A-Z]+)?(\\\\.[a-z-]+)?" } ]
        }
      }
    }
  ]
}

Clone this wiki locally