MediaTypes
Media Types based on scheme, with versioning, views, suffixes and validations. Integrations available for Rails / ActionPack and http.rb.
Installation
Add this line to your application's Gemfile:
gem 'media_types'
And then execute:
$ bundle
Or install it yourself as:
$ gem install media_types
Usage
By default there are no media types registered or defined, except for an abstract base type.
Definition
You can define media types by inheriting from this base type, or create your own base type with a class method .base_format that is used to create the final media type string by injecting formatted parameters:
%<type>s: the typemedia_typereceived%<version>s: the version, defaults to:current_version%<view>s: the view, defaults to%<suffix>s: the suffix
require 'media_types'
class Venue < MediaTypes::Base
def self.base_format
'application/vnd.mydomain.%<type>s.v%<version>.s%<view>s+%<suffix>s'
end
media_type 'venue', defaults: { suffix: :json, version: 2 }
validations do
attribute :name, String
collection :location do
attribute :latitude, Numeric
attribute :longitude, Numeric
attribute :altitude, AllowNil(Numeric)
end
link :self
link :route, allow_nil: true
version 1 do
attribute :name, String
attribute :coords, String
attribute :updated_at, String
link :self
end
view 'create' do
collection :location do
attribute :latitude, Numeric
attribute :longitude, Numeric
attribute :altitude, AllowNil(Numeric)
end
version 1 do
collection :location do
attribute :latitude, Numeric
attribute :longitude, Numeric
attribute :altitude, AllowNil(Numeric)
end
end
end
end
registrations :venue_json do
view 'create', :create_venue
view 'index', :venue_urls
view 'collection', :venue_collection
versions [1,2]
suffix :json
suffix :xml
end
end
Schema Definitions
If you define a scheme using current_scheme { }, you may use any of the following dsl:
attribute
Adds an attribute to the schema, if a +block+ is given, uses that to test against instead of +type+
| param | type | description |
|---|---|---|
| key | Symbol |
the attribute name |
| opts | Hash |
options to pass to Scheme or Attribute |
| type | Class, ===, Scheme |
The type of the value, can be anything that responds to ===, or scheme to use if no &block is given. Defaults to Object without a &block and to Hash with a &block. |
| optional: | TrueClass, FalseClass |
if true, key may be absent, defaults to false |
| &block | Block |
defines the scheme of the value of this attribute |
Add an attribute named foo, expecting a string
require 'media_types'
class MyMedia
include MediaTypes::Dsl
validations do
attribute :foo, String
end
end
MyMedia.valid?({ foo: 'my-string' })
# => true
Add an attribute named foo, expecting nested scheme
class MyMedia
include MediaTypes::Dsl
validations do
attribute :foo do
attribute :bar, String
end
end
end
MyMedia.valid?({ foo: { bar: 'my-string' }})
# => true
any
Allow for any key. The &block defines the Schema for each value.
| param | type | description |
|---|---|---|
| scheme | Scheme, NilClass |
scheme to use if no &block is given |
| allow_empty: | TrueClass, FalsClass |
if true, empty (no key/value present) is allowed |
| expected_type: | Class, |
forces the validated value to have this type, defaults to Hash. Use Object if either Hash or Array is fine |
| &block | Block |
defines the scheme of the value of this attribute |
Add a collection named foo, expecting any key with a defined value
class MyMedia
include MediaTypes::Dsl
validations do
collection :foo do
any do
attribute :bar, String
end
end
end
end
MyMedia.valid?({ foo: [{ anything: { bar: 'my-string' }, other_thing: { bar: 'other-string' } }] })
# => true
not_strict
Allow for extra keys in the schema/collection even when passing strict: true to #validate!
Allow for extra keys in collection
class MyMedia
include MediaTypes::Dsl
validations do
collection :foo do
attribute :required, String
not_strict
end
end
end
MyMedia.valid?({ foo: [{ required: 'test', bar: 42 }] })
# => true
collection
Expect a collection such as an array or hash. The &block defines the Schema for each item in that collection.
| param | type | description |
|---|---|---|
| key | Symbol |
key of the collection (same as #attribute) |
| scheme | Scheme, NilClass, Class |
scheme to use if no &block is given or Class of each item in the |
| allow_empty: | TrueClass, FalseClass |
if true, empty (no key/value present) is allowed |
| expected_type: | Class, |
forces the validated value to have this type, defaults to Array. Use Object if either Array or Hash is fine. |
| optional: | TrueClass, FalseClass |
if true, key may be absent, defaults to false |
| &block | Block |
defines the scheme of the value of this attribute |
Collection with an array of string
class MyMedia
include MediaTypes::Dsl
validations do
collection :foo, String
end
end
MyMedia.valid?({ collection: ['foo', 'bar'] })
# => true
Collection with defined scheme
class MyMedia
include MediaTypes::Dsl
validations do
collection :foo do
attribute :required, String
attribute :number, Numeric
end
end
end
MyMedia.valid?({ foo: [{ required: 'test', number: 42 }, { required: 'other', number: 0 }] })
# => true
link
Expect a link with a required href: String attribute
| param | type | description |
|---|---|---|
| key | Symbol |
key of the link (same as #attribute) |
| allow_nil: | TrueClass, FalseClass |
if true, value may be nil |
| optional: | TrueClass, FalseClass |
if true, key may be absent, defaults to false |
| &block | Block |
defines the scheme of the value of this attribute, in addition to the href attribute |
Links as defined in HAL, JSON-Links and other specs
class MyMedia
include MediaTypes::Dsl
validations do
link :_self
link :image
end
end
MyMedia.valid?({ _links: { self: { href: 'https://example.org/s' }, image: { href: 'https://image.org/i' }} })
# => true
Link with extra attributes
class MyMedia
include MediaTypes::Dsl
validations do
link :image do
attribute :templated, TrueClass
end
end
end
MyMedia.valid?({ _links: { image: { href: 'https://image.org/{md5}', templated: true }} })
# => true
Validation
If your type has a validations, you can now use this media type for validation:
Venue.valid?({
#...
})
# => true if valid, false otherwise
Venue.validate!({
# /*...*/
})
# => raises if it's not valid
If an array is passed, check the scheme for each value, unless the scheme is defined as expecting a hash:
expected_hash = Scheme.new(expected_type: Hash) { attribute(:foo) }
expected_object = Scheme.new { attribute(:foo) }
expected_hash.valid?({ foo: 'string' })
# => true
expected_hash.valid?([{ foo: 'string' }])
# => false
expected_object.valid?({ foo: 'string' })
# => true
expected_object.valid?([{ foo: 'string' }])
# => true
Formatting for headers
Any media type object can be coerced in valid string to be used with Content-Type or Accept:
Venue.mime_type.to_s
# => "application/vnd.mydomain.venue.v2+json"
Venue.mime_type.version(1).to_s
# => "application/vnd.mydomain.venue.v1+json"
Venue.mime_type.version(1).suffix(:xml).to_s
# => "application/vnd.mydomain.venue.v1+xml"
Venue.mime_type.to_s(0.2)
# => "application/vnd.mydomain.venue.v2+json; q=0.2"
Venue.mime_type.collection.to_s
# => "application/vnd.mydomain.venue.v2.collection+json"
Venue.mime_type.view('active').to_s
# => "application/vnd.mydomain.venue.v2.active+json"
Integrations
The integrations are not loaded by default, so you need to require them:
# For Rails / ActionPack
require 'media_types/integrations/actionpack'
# For HTTP.rb
require 'media_types/integrations/http'
Define a registrations block on your media type, indicating the symbol for the base type (registrations :symbol do) and inside use the registrations dsl to define which media types to register. versions array_of_numbers determines which versions, suffix name adds a suffix, type_alias name adds an alias and view name, symbol adds a view.
Venue.register
Rails
Load the actionpack integration and call .register on all the media types you want to be available in Rails. You can do this in the mime_types initializer, or anywhere before your controllers are instantiated. Yes, the symbol (by default <type>_v<version>_<suffix>) can now be used in your format blocks, or as extension in the url.
Rails only has a default serializer for application/json, and content with your +json media types (or different once) will not be deserialized by default. A way to overcome this is to set the JSON parameter parser for all new symbols. .register gives you back an array of Registerable objects that responds to #to_sym to get that symbol.
symbols = Venue.register.map(&:to_sym)
original_parsers = ActionDispatch::Request.parameter_parsers
new_parser = original_parsers[Mime[:json].symbol]
new_parsers = original_parsers.merge(Hash[*symbols.map { |s| [s, new_parser] }])
ActionDispatch::Request.parameter_parsers = new_parsers
If you want to validate the content-type and not have your errors be Rack::Error but be handled by your controllers, leave this out and add a before_action to your controller that deserializes + validates for you.
HTTP.rb
Load the http integration and call .register on all media types you want to be able to serialize and deserialize. The media type validations will run both before serialization and after deserialization.
Currently uses oj under the hood and this can not be changed.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, call bundle exec rake release to create a new git tag, push git commits and tags, and
push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at SleeplessByte/media-types-ruby