0.0
No commit activity in last 3 years
No release in over 3 years
Sanitizes a jsonapi Document and maps it to ActiveRecord, creating or updating as needed. Prevents mistakes when assigingng attributes or referring to unscoped relationships.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 5.1.4, ~> 5.1
~> 1.15
>= 6.0.0, ~> 6.0
~> 10.0
~> 3.0
>= 1.0.0, ~> 1.0

Runtime

>= 5.1.4, ~> 5.1
>= 5.1.4, ~> 5.1
 Project Readme

JsonapiMapper

Sanitizes a jsonapi Document and maps it to ActiveRecord, creating or updating as needed.

  • Prevents assiginging unexpected attributes on your records.
  • Prevents unscoped queries when creating/updating records.

Installation

Add this line to your application's Gemfile:

gem 'jsonapi_mapper'

And then execute:

$ bundle

Or install it yourself as:

$ gem install jsonapi_mapper

Usage

See the specs directory for more examples.

      class Person < ActiveRecord::Base
        belongs_to :parent, class_name: 'Person'
        has_many :children, class_name: 'Person', foreign_key: 'parent_id'
        belongs_to :pet, class_name: 'PetDog'
      end

      class PetDog < ActiveRecord::Base
        has_one :person, foreign_key: 'pet_id'
      end

      # This document should create a person and several associations.
      # Notice how these not-persisted resources can be referenced using
      # an internal id, which starts with @
      # The local @ ids shall be replaced with proper server assigned ids
      # once the resources are persisted.
      document = {
        data: {
          type: 'people',
          attributes: { name: 'ian', admin: true },
          relationships: {
            pet: { data: { type: 'pet_dogs', id: '@1' }},
            parent: { data: { type: 'people', id: '@1' }},
            children: { data: [
              { type: 'people', id: '@2' },
              { type: 'people', id: '@3' },
            ]},
          }
        },
        included: [
          { type: 'people', id: '@1', attributes: { name: 'ana', admin: true } },
          { type: 'people', id: '@2', attributes: { name: 'bob', admin: true } },
          { type: 'people', id: '@3', attributes: { name: 'zoe', admin: true } },
          { type: 'pet_dogs', id: '@1', attributes: { name: 'ace', age: 11 } }
        ]
      }

      # The mapper whitelists which types should be expected from the
      # jsonapi document. It also whitelists attributes and relationship names.
      # The last item of the attributes list is a Hash to be used as 'scope'
      # when attempting to fetch and/or modify any resource.
      mapper = JsonapiMapper.doc(document,
        people: [:name, :pet, :parent, :children, country: 'argentina'],
        pet_dogs: [:name, country: 'argentina']
      )

      # The document data lives in mapper.data
      # It could be a simple or multiple resource response.
      # If you want to check wether the document had a single resource
      # or a collection as its primary data you can use the following methods.
      mapper.collection? # Was primary document data a collection?
      mapper.single? # Was primary document data a single resource?


      # The rest of the included resources live in mapper.included
      others = mapper.included 

      # Attempts to save both data and included. Returns false if there
      # were any validation errors.
      mapper.save_all 
      
      # Four people have been created
      Person.count.should == 4

      # All of them from 'argentina' according to the provided scope.
      Person.where(country: 'argentina').count.should == Person.count

      # The 'admin' field was not set, because it wasn't in the mapper list.
      Person.where(admin: true).count.should == 0
      
      # This other document tries to update a bob's name and parent.
      # And it also creates a new dow and assigns it as pet for 'bob' and 'ana'
      other_document = {
        data: {
          type: 'people',
          id: '1',
          attributes: { name: 'rob' },
          relationships: {
            pet: { data: { type: 'pet_dogs', id: '@1' }},
            parent: { data: { type: 'people', id: '2' }},
          }
        },
        included: [
          { 
            type: 'people',
            id: ana.id,
            relationships: {
              pet: { data: { type: 'pet_dogs', id: '@1' }},
            }
          },
          { type: 'pet_dogs', id: '@1', attributes: { name: 'ace' } }
        ]
      }

      mapper = JsonapiMapper.doc other_document,
        people: [:name, :pet, :parent, country: 'uruguay'],
        pet_dogs: [:name, country: 'uruguay']

      mapper.save_all

      # Is dangerous to use unscoped queries
      # For those rare occassions where you don't need them they can be disabled.
      # The JsonapiMapper.doc_unsafe! method receives an argument with the names
      # of all the types for which a scope is not required.
      JsonapiMapper.doc_unsafe! document,
        [:pet_dogs],
        people: [:name, :pet, :parent, country: 'uruguay'],
        pet_dogs: [:name]

      # If you're needing to 'translate' between your jsonapi document names
      # and your ActiveRecord class and column names, you can do it like so:
      # Notice how the second hash has translations for type and attribute names.
      mapper = JsonapiMapper.doc(document, {
        persons: [:handle, :dog, :parental_figure, country: 'uruguay'],
        pets: [:nickname, country: 'uruguay']
      },
      { types: { persons: Person, pets: PetDog },
        attributes: {
          persons: {handle: :name, dog: :pet, parental_figure: :parent},
          pets: {nickname: :name},
        }  
      }).save_all


      # If any resource in your document has errors, you can get a collection
      # with pointers to the specific fields and the type and id of the resource
      # that has the error.
      document = {
        data: [
          { type: 'pets', attributes: { age: 3 } },
          { type: 'pets', attributes: { age: 6 } },
        ],
        included: [
          { type: 'pets', id: '@1', attributes: { age: 4 } }
        ]
      }
      mapper = JsonapiMapper.doc(document,
        { pets: [:nickname, country: 'uruguay'] },
        { types: { pets: PetDog }, attributes: { pets: {nickname: :name} } }
      )

      # all_valid? triggers all validations and sets up errors.
      mapper.all_valid?.should be_falsey

      # Then all errors are presented like so, honoring remapped names too.
      mapper.all_errors.should == {
        errors: [
          { status: 422,
            title: "can't be blank",
            detail: "can't be blank",
            code: "can_t_be_blank",
            meta: {type: "pets"},
            source: {pointer: "/data/0/attributes/nickname"}
          },
          { status: 422,
            title: "can't be blank",
            detail: "can't be blank",
            code: "can_t_be_blank",
            meta: {type: "pets"},
            source: {pointer: "/data/1/attributes/nickname"}
          },
          { status: 422,
            title: "can't be blank",
            detail: "can't be blank",
            code: "can_t_be_blank",
            meta: {type: "pets"},
            source: {pointer: "/included/0/attributes/nickname"}
          }
        ]
      }

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec 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, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome here.

Code Status

Build Status

License

The gem is available as open source under the terms of the MIT License.