Media Types
Media types provide the structural representation of the API resources we want to expose. They also commonly have an associated name (i.e., its identifier).
How does that work? Let's take a look at an example. Let's say we want to expose Users in our API. In this case, our users MediaType could be defined by a structure that contains an id
, name
and email
, and be represented by the following identifier application/vnd.mycompany.users
. So in this case, if a client issues an API to retrieve a user, he will receive a response with Content-Type: application/vnd.mycompany.users+json
, which indicates 2 different things: The first one is to indicat what type of attribute shape the payload contains (i.e., id
, name
and email
fields). The second one (denoted by the +json
suffix) indicates that the returned structure is encoded using JSON.
Praxis Media-Types are concerned only with the design aspects only (i.e., structure and name), and leaves any encoding concerns to the implementation part. So it is worth noting that a MediaType definition does imply any particular encoding (JSON vs XML...).
So, how do we define these in Praxis? To define a media type for an API resource, we simply create a class that derives from Praxis::MediaType
, where we can define its structure within the attributes
section and give it a unique identifier name. A media type can also have a human-readable description, which will show up in any generated documentation.
Here's an example of a simple media type that describes a few attributes for a hypothetical Blog API resource.
class Blog < Praxis::MediaType
description 'A Blog API resource represents...'
identifier 'application/vnd.acme.blog'
attributes do
attribute :id, Integer
attribute :href, String
attribute :owner, Person
attribute :subject, String
attribute :locale do
attribute :language, String
attribute :country, String
end
end
end
Description
You can specify a description for the media type using the description
method. This description string is just for human consumption and is simply inserted directly into to the generated API documentation.
class Blog < Praxis::MediaType
description <<-eos
This is a sample blog.
Which requires a much longer an elaborate description to be written.
eos
end
Identifier
The media type identifier method allows you to associate an internet media type string with the MediaType definition. internet media types should typically be used as unique names (i.e., 'application/vnd.acme.blog') that way the client receiving the response knows exactly what structure to expect to find. However, it is not necessary to do so, and they can be also be left withot a name by using just an encoder string (i.e., 'application/json').
class Blog < Praxis::MediaType
identifier 'application/vnd.acme.blog'
end
Identifiers have an optional suffix that indicates the encoding format used to represent the media;
for instance, a blog could be represented as an application/vnd.acme.blog+json
or a +xml
without changing its essential blog-ness. Identifiers can also have semicolon-delimited options
such as text/html; charset=utf-8
..
In Praxis, media type identifiers are represented by the MediaTypeIdentifier
class which parses
the identifier's components and makes them available as instance accessors: type
, subtype
,
suffix
and parameters
. Identifier objects can be compared, modified, fuzzy-matched against
broader or narrower types, and transformed back into strings.
MediaTypeIdentifier.load('text/plain; charset=utf-8').parameters['charset'] # => "utf-8"
MediaTypeIdentifier.load('image/*').match('image/jpeg') # => true
Attributes
The attributes section of the media type describes the full structure of the resource representation. It describes the superset of all possible attributes that can appear in any view that can be rendered.
The attributes
method expects a block of attribute definitions:
class Blog < Praxis::MediaType
attributes do
attribute :id, Integer
attribute :href, String, regexp: %r{/blogs/\d+}
attribute :owner, Person, description: 'Owner of this Blog'
attribute :subject, String
attribute :created_at, DateTime
attribute :visibility, String, values: ['public','private']
end
end
Each attribute has a name, type, description and other specific configuration
options: allowed values, format, examples, etc. While some options, such as
description
and values
are always available for any attribute, different
attribute types support type-specific options: min
/max
values for Integers,
regexp
for Strings, etc.
Similarly to the overall MediaType description, the documentation browser will render attribute description
values in the generated docs.
To read more about supported types and defining a complex and rich structures, take a look at the Attributor gem and other Praxis media type examples.
Default fielset
One way to think of media types is that they contain the superset of attributes that can ever be returned. Often times, we might want to return only some of those when rendering responses. In fact, we will see in the rendering extensions sections that we can allow the API client to fully specify which subset of the available attributes to return, depending on what it needs.
As a convenience, especially while developing, it is nice to be able to not have to worry about listing the fields you want rendered. For that reason Praxis Media Types have the concept of a default_fieldset
, which defines what attributes to return if no specific fields have been asked for. By default, Praxis will calculate this default fieldset as the set of terminal fields (i.e., simple fields that aren't other related MediaTypes). In the case for our Blog above, the default_fieldset
will only contain the id
, href
, subject
and locale
, but it will leave out the owner
as it would recurse into the fields of a Person
media type.
You can easily override the Praxis-picked set of fields and define them yourself using the default_fieldset
DSL. For example, the following restrict the default fields to only have id
, href
and subject
:
class Blog < Praxis::MediaType
description 'A Blog API resource represents...'
identifier 'application/vnd.acme.blog'
attributes do
attribute :id, Integer
attribute :href, String
attribute :owner, Person
attribute :subject, String
attribute :locale do
attribute :language, String
attribute :country, String
end
end
default_fieldset do
attribute :id
attribute :href
attribute :subject
end
end
Collections
Often times, we need to expose arrays of media types. In fact, this is very common for a given media type to embed collections of other related media types or even the case of returning a full top-level collection of mediatypes in an index
API response.
To help in defining those, Praxis provides a Praxis::Collection
class.
Praxis::Collection
To define a related collection of mediatype objects, you can use Praxis::Collection.of(media_type)
For example, here is how you would define a collection of related Post media types for your Blog:
class Blog < Praxis::MediaType
attributes do
attribute :id, Integer
attribute :posts, Praxis::Collection.of(Post)
end
end
In its underbelly, this Praxis::Collection.of
type is backed by the Collection
class of Attributor, which also includes the Praxis::MediaTypeCommon
module in it. For the folk interested in greedy details, it would be interested to know that this resulting Attributor type is gonna be stored in a Collection
constant under the Post
mediatype. In other words, one can think of Praxis::Collection.of(Post)
to be equivalent to accessing Post::Collection
, except that it will create one if it does not exist already.
As stated above, the created inner Collection
class will simply be an Attributor::Collection
that wraps members of a given MediaType, and that has an identifier that matches the member's identifier, with an added "collection=true" suffix. So in the above case, the Post::Collection
will have an identifier of "application/vnd.acme.post;collection=true".
This inner Collection
should be sufficient in most cases. However, you can explicitly define your own and Praxis will use that instead. Here's an example of how to do that if you wanted Post::Collection
to have an identifier of "application/vnd.acme.posts" instead:
class Post < Praxis::MediaType
identifier 'application/vnd.acme.post'
# ...
class Collection < Attributor::Collection
member_type Post
identifier 'application/vnd.acme.posts'
end
end
Note: When defined without using the .of
helper, you use the member_type
method to specify what type of media type class this collection is wrapping. If you want a non-anonymous class that is a collection and also has other attributes (such as description), you have two ways to define it:
# Option 1: implicit collection type; reopen class to add more information
CommentThread = Praxis::Collection.of(Comment)
class CommentThread
description 'A sequence of comments on a blog post.'
end
# Option 2: explicit collection type with member_type specified inline
class CommentThread < Praxis::Collection
member_type Comment
description 'A sequence of comments on a blog post.'
end
Attributor::Collection
An Attributor::Collection (as opposed to Praxis::Collection
) is the way to embed a collection of lower-level types that aren't media types. In fact they use the same .of(type)
method to create the types.
For example, you may want a collection of tag strings to be an attribute of your blog media type:
class Blog < Praxis::MediaType
attributes do
attribute :id, Integer
attribute :tags, Attributor::Collection.of(String)
end
end
Rendering mediatypes
Maybe some of this data is best to be brought up in implementation rendering?....
????
Once a media type is defined within your application, you can use it to wrap a
compatible data object holding resource data, and render it using any of the
available views. A compatible object must respond to the method names matching
the media type attribute names, and return sub-objects that are compatible with
the types defined in the media type. Praxis renders media types into Hash
structures to achieve format-independence. These rendered hash structures can
be formatted in your application using the desired wire encoding (JSON, XML, or
any other type you might need).
Here are two examples of how to render a blog_object using the Blog
media
type: one using its default
view and another using its link
view:
{% highlight ruby %} Blog.render(blog_object, view: :default) => { 'id' : 123, 'owner' : { ...an owner hash rendered with its :compact view...}, 'subject' : 'First post', 'locale' : { 'language': 'en', 'country': 'us' } }
Blog.render(blog_object, view: :link) => { 'href' : '/blogs/123' } {% endhighlight %}
In this example, your blog_object
must return:
{% highlight bash %} +---------+---------------------------------------------+ | Method | Return value | |---------+---------------------------------------------| | id | integer | | owner | object compatible with a Person media type | | subject | String | | locale | object compatible with the locale structure | | href | String | +---------+---------------------------------------------+ {% endhighlight %}
Praxis provides a lot of help in managing resource objects and linking them to data sources (including databases) by integrating with the Praxis::Mapper gem.
Also, Praxis allows you to generate compatible objects using the .example
feature of MediaType classes. Using this .example
feature you can create
random instances of compatible objects without any extra effort, which is great
to simulate returning data objects when testing controller responses without
requiring any data source access. There is also some help available for
creating realistic examples for your test cases. See more examples
at the end of this document.
Examples
??? TODO????
Praxis provides tools to automatically generate example object values that will respond to each and every attribute name of a media type and will return an object that responds to the correct methods of their defined type, including when the attribute type is another media type.
The values of the generated example attributes will also conform to specifications like default values, regexp, etc.
Please see Praxis::Mapper and Attributor for more on generating example objects.