Media Types based on scheme, with versioning, views, suffixes and validations. Integrations available for Rails / ActionPack and http.rb.
Add this line to your application's Gemfile:
gem 'media_types'
And then execute:
$ bundle
Or install it yourself as:
$ gem install media_types
By default there are no media types registered or defined, except for an abstract base type.
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_type
received%<version>s
: the version, defaults to:current_version
%<view>s
: the view, defaults to%<suffix>s
: the suffix
require 'media_types'
class Venue
include MediaTypes::Dsl
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
If you define a scheme using current_scheme { }
, you may use any of the following dsl:
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 |
require 'media_types'
class MyMedia
include MediaTypes::Dsl
validations do
attribute :foo, String
end
end
MyMedia.valid?({ foo: 'my-string' })
# => true
class MyMedia
include MediaTypes::Dsl
validations do
attribute :foo do
attribute :bar, String
end
end
end
MyMedia.valid?({ foo: { bar: 'my-string' }})
# => true
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 |
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
Allow for extra keys in the schema/collection even when passing strict: true
to #validate!
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
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 |
class MyMedia
include MediaTypes::Dsl
validations do
collection :foo, String
end
end
MyMedia.valid?({ collection: ['foo', 'bar'] })
# => true
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
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 |
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
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
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
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"
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
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.
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.
MediaTypes::Serialization
: π Add media types supported serialization using your favourite serializerMediaTypes::Validation
: β Response validations according to a media-type
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.
Bug reports and pull requests are welcome on GitHub at SleeplessByte/media-types-ruby