# MediaTypes [![Build Status](https://travis-ci.com/SleeplessByte/media-types-ruby.svg?branch=master)](https://travis-ci.com/SleeplessByte/media-types-ruby) [![Gem Version](https://badge.fury.io/rb/media_types.svg)](https://badge.fury.io/rb/media_types) [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) [![Maintainability](https://api.codeclimate.com/v1/badges/6f2dc1fb37ecb98c4363/maintainability)](https://codeclimate.com/github/SleeplessByte/media-types-ruby/maintainability) ## Installation Add this line to your application's Gemfile: ```ruby 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: - `%s`: the type `media_type` received - `%s`: the version, defaults to `:current_version` - `%s`: the view, defaults to - `%s`: the suffix ```Ruby require 'media_types' class Venue < MediaTypes::Base 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 def self.base_format 'application/vnd.mydomain.%s.v%.s%s+%s' 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 `String` 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 ```Ruby 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 ```Ruby 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 ```Ruby 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 ```Ruby 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 ```Ruby class MyMedia include MediaTypes::Dsl validations do collection :foo, String end end MyMedia.valid?({ collection: ['foo', 'bar'] }) # => true ``` #### Collection with defined scheme ```Ruby 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 ```Ruby 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 ```Ruby class MyMedia include MediaTypes::Dsl validations do link :image do attribute :templated, TrueClass end end end MyMedia.valid?({ _links: { self: { href: 'https://example.org/s' }, image: { href: 'https://image.org/i' }} }) # => true ``` #### Link with extra attributes ```Ruby 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: ```Ruby 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: ```Ruby 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`: ```Ruby 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" ``` ## Register in Rails or Rack 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. As long as `action_dispatch` is available, you can register the mime type with `action_dispatch/http/mime_type`: ```Ruby Venue.register # => Mime type is now available using the symbol, or lookup the actual mimetype ``` You can do this in the `mime_types` initializer, or anywhere before your controllers are instantiated. Yes, the symbol (by default `_v_`) can now be used in your `format` blocks, or as extension in the url. ## 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](https://github.com/SleeplessByte/media-types-ruby)