├── spec ├── jbuilder │ ├── examples │ │ ├── .keep │ │ ├── internal_errors.json │ │ ├── meta.json │ │ ├── errors.json │ │ ├── resources_errors.json │ │ ├── errors_meta.json │ │ ├── resources_errors_meta.json │ │ ├── resources.json │ │ ├── resources_meta.json │ │ └── resources_admin.json │ └── json_api_spec.rb ├── spec_helper.rb ├── factory.rb └── support │ └── dummies.rb ├── .rspec ├── lib └── jbuilder │ ├── json_api │ └── version.rb │ └── json_api.rb ├── Gemfile ├── Rakefile ├── .gitignore ├── bin ├── setup └── console ├── .travis.yml ├── MIT-LICENSE ├── jbuilder-json_api.gemspec └── README.md /spec/jbuilder/examples/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/jbuilder/json_api/version.rb: -------------------------------------------------------------------------------- 1 | module JsonAPI 2 | VERSION = '1.0.0' 3 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in jbuilder-json_api.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /.idea 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /spec/jbuilder/examples/internal_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "status": 500, 5 | "detail": "undefined method `id' for 42:Fixnum", 6 | "title": "NoMethodError" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.0 4 | - 2.2.4 5 | - 2.1.8 6 | before_install: gem install bundler -v 1.11.2 7 | addons: 8 | code_climate: 9 | repo_token: 49fa22eb7f9ccb8d824defdbda244a6a4233c0615c8d43e2a8cbefca3e33262b 10 | -------------------------------------------------------------------------------- /spec/jbuilder/examples/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "copyright": "Vlad Faust", 4 | "year": "2016", 5 | "joke": { 6 | "title": "Not funny at all", 7 | "body": "A SQL query walks up to two tables in a restaurant and asks: \"Mind if I join you?\"" 8 | } 9 | }, 10 | "data": [ 11 | 12 | ] 13 | } -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'jbuilder/json_api' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /spec/jbuilder/examples/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "id": 1, 5 | "status": 404, 6 | "detail": "The requested resource cannot be found.", 7 | "code": 112, 8 | "title": "Not found", 9 | "source": { 10 | "pointer": "http://posts_path" 11 | }, 12 | "links": { 13 | "about": "https://en.wikipedia.org/wiki/HTTP_404" 14 | } 15 | }, 16 | { 17 | "id": 2, 18 | "title": "Another error" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /spec/jbuilder/examples/resources_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "id": 1, 5 | "status": 404, 6 | "detail": "The requested resource cannot be found.", 7 | "code": 112, 8 | "title": "Not found", 9 | "source": { 10 | "pointer": "http://posts_path" 11 | }, 12 | "links": { 13 | "about": "https://en.wikipedia.org/wiki/HTTP_404" 14 | } 15 | }, 16 | { 17 | "id": 2, 18 | "title": "Another error" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'codeclimate-test-reporter' 2 | CodeClimate::TestReporter.start 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'jbuilder/json_api' 6 | require 'factory_girl' 7 | require './spec/support/dummies' 8 | 9 | RSpec.configure do |config| 10 | config.include FactoryGirl::Syntax::Methods 11 | 12 | config.before(:each) do 13 | # Force FactoryGirl sequences to be fully reset before each test run to simplify ID testing 14 | # since we are not using a database or real fixtures. Inside of each test case, IDs will 15 | # increment per type starting at 1. 16 | FactoryGirl.reload 17 | load 'factory.rb' 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/jbuilder/examples/errors_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "copyright": "Vlad Faust", 4 | "year": "2016", 5 | "joke": { 6 | "title": "Not funny at all", 7 | "body": "A SQL query walks up to two tables in a restaurant and asks: \"Mind if I join you?\"" 8 | } 9 | }, 10 | "errors": [ 11 | { 12 | "id": 1, 13 | "status": 404, 14 | "detail": "The requested resource cannot be found.", 15 | "code": 112, 16 | "title": "Not found", 17 | "source": { 18 | "pointer": "http://posts_path" 19 | }, 20 | "links": { 21 | "about": "https://en.wikipedia.org/wiki/HTTP_404" 22 | } 23 | }, 24 | { 25 | "id": 2, 26 | "title": "Another error" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /spec/jbuilder/examples/resources_errors_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "copyright": "Vlad Faust", 4 | "year": "2016", 5 | "joke": { 6 | "title": "Not funny at all", 7 | "body": "A SQL query walks up to two tables in a restaurant and asks: \"Mind if I join you?\"" 8 | } 9 | }, 10 | "errors": [ 11 | { 12 | "id": 1, 13 | "status": 404, 14 | "detail": "The requested resource cannot be found.", 15 | "code": 112, 16 | "title": "Not found", 17 | "source": { 18 | "pointer": "http://posts_path" 19 | }, 20 | "links": { 21 | "about": "https://en.wikipedia.org/wiki/HTTP_404" 22 | } 23 | }, 24 | { 25 | "id": 2, 26 | "title": "Another error" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /spec/factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :post, class: DummyApp::Post do 3 | skip_create 4 | 5 | sequence (:id) { |n| n } 6 | sequence (:title) { |n| "Title for post ##{ n }" } 7 | sequence (:body) { |n| "Body for post ##{ n }" } 8 | 9 | trait :with_author do 10 | association :author, factory: :user 11 | end 12 | 13 | trait :with_comments do 14 | comments [] 15 | after :create do |post| 16 | 2.times do 17 | post.comments << (create :comment) 18 | end 19 | end 20 | end 21 | end 22 | 23 | factory :comment, class: DummyApp::Comment do 24 | skip_create 25 | 26 | sequence (:id) { |n| n } 27 | sequence (:body) { |n| "Body for comment ##{ n }" } 28 | association :user 29 | end 30 | 31 | factory :user, class: DummyApp::User do 32 | skip_create 33 | 34 | sequence (:id) { |n| n } 35 | sequence (:name) { |n| "Name for user ##{ n }" } 36 | end 37 | end -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Vlad Faust 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/jbuilder/examples/resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "post", 5 | "id": 1, 6 | "attributes": { 7 | "id": 1, 8 | "title": "Title for post #1" 9 | }, 10 | "relationships": { 11 | "author": { 12 | "data": [ 13 | { 14 | "type": "user", 15 | "id": 1 16 | } 17 | ] 18 | } 19 | } 20 | }, 21 | { 22 | "type": "post", 23 | "id": 2, 24 | "attributes": { 25 | "id": 2, 26 | "title": "Title for post #2" 27 | }, 28 | "relationships": { 29 | "author": { 30 | "data": [ 31 | { 32 | "type": "user", 33 | "id": 4 34 | } 35 | ] 36 | } 37 | } 38 | } 39 | ], 40 | "included": [ 41 | { 42 | "type": "user", 43 | "id": 1, 44 | "attributes": { 45 | "id": 1, 46 | "name": "Name for user #1" 47 | } 48 | }, 49 | { 50 | "type": "user", 51 | "id": 4, 52 | "attributes": { 53 | "id": 4, 54 | "name": "Name for user #4" 55 | } 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /spec/support/dummies.rb: -------------------------------------------------------------------------------- 1 | module DummyApp 2 | class Post 3 | attr_accessor :id 4 | attr_accessor :title 5 | attr_accessor :body 6 | attr_accessor :author 7 | attr_accessor :comments 8 | 9 | def json_api_attrs (options = {}) 10 | attrs = %w(id title) 11 | attrs << 'body' if options[:access_lvl] == :admin 12 | attrs 13 | end 14 | 15 | def json_api_relations (options = {}) 16 | attrs = %w(author) 17 | attrs << 'comments' if options[:access_lvl] == :admin 18 | attrs 19 | end 20 | 21 | def json_api_meta (options = {}) 22 | { blah: "Just another meta info for post ##{ id }" } if options[:access_lvl] == :admin 23 | end 24 | end 25 | 26 | class User 27 | attr_accessor :id 28 | attr_accessor :name 29 | attr_accessor :comments 30 | 31 | def json_api_attrs (options = {}) 32 | %w(id name) 33 | end 34 | 35 | def json_api_relations (options = {}) 36 | %w(comments) 37 | end 38 | end 39 | 40 | class Comment 41 | attr_accessor :id 42 | attr_accessor :body 43 | attr_accessor :user 44 | attr_accessor :post 45 | 46 | def json_api_attrs (options = {}) 47 | %w(id body) 48 | end 49 | 50 | def json_api_relations (options = {}) 51 | %w(user post) 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /jbuilder-json_api.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'jbuilder/json_api/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'jbuilder-json_api' 8 | spec.version = JsonAPI::VERSION 9 | spec.authors = ['Vlad Faust'] 10 | spec.email = ['vladislav.faust@gmail.com'] 11 | 12 | spec.summary = %q{Easily follow jsonapi.org specifications with Jbuilder} 13 | spec.description = %q{Adds a method to build a valid JSON API (jsonapi.org) response without any new superclasses or weird setups. Set'n'go!} 14 | spec.homepage = %q{https://github.com/vladfaust/jbuilder-json_api} 15 | spec.license = 'MIT' 16 | 17 | spec.files = Dir['{lib}/**/*', 'Rakefile', 'README.md'] 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.bindir = 'bin' 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.require_paths = %w(lib) 22 | 23 | spec.add_dependency 'jbuilder', '~> 2' 24 | 25 | spec.add_development_dependency 'bundler', '~> 1.11' 26 | spec.add_development_dependency 'rake', '~> 10.0' 27 | spec.add_development_dependency 'rspec', '~> 3.2' 28 | spec.add_development_dependency 'factory_girl', '~> 4.5' 29 | spec.add_development_dependency 'codeclimate-test-reporter' 30 | end 31 | -------------------------------------------------------------------------------- /spec/jbuilder/examples/resources_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "copyright": "Vlad Faust", 4 | "year": "2016", 5 | "joke": { 6 | "title": "Not funny at all", 7 | "body": "A SQL query walks up to two tables in a restaurant and asks: \"Mind if I join you?\"" 8 | } 9 | }, 10 | "data": [ 11 | { 12 | "type": "post", 13 | "id": 1, 14 | "attributes": { 15 | "id": 1, 16 | "title": "Title for post #1" 17 | }, 18 | "relationships": { 19 | "author": { 20 | "data": [ 21 | { 22 | "type": "user", 23 | "id": 1 24 | } 25 | ] 26 | } 27 | } 28 | }, 29 | { 30 | "type": "post", 31 | "id": 2, 32 | "attributes": { 33 | "id": 2, 34 | "title": "Title for post #2" 35 | }, 36 | "relationships": { 37 | "author": { 38 | "data": [ 39 | { 40 | "type": "user", 41 | "id": 4 42 | } 43 | ] 44 | } 45 | } 46 | } 47 | ], 48 | "included": [ 49 | { 50 | "type": "user", 51 | "id": 1, 52 | "attributes": { 53 | "id": 1, 54 | "name": "Name for user #1" 55 | } 56 | }, 57 | { 58 | "type": "user", 59 | "id": 4, 60 | "attributes": { 61 | "id": 4, 62 | "name": "Name for user #4" 63 | } 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /spec/jbuilder/json_api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | describe JsonAPI do 4 | it 'has a version number' do 5 | expect(JsonAPI::VERSION).to_not be nil 6 | end 7 | 8 | describe 'api_format!' do 9 | DEBUG = false # Set it to true when want to just write JSONs to files, false enables testing itself 10 | 11 | def check (json, name) 12 | if DEBUG 13 | File.write "spec/jbuilder/examples/#{ name }.json", JSON.pretty_unparse(json) 14 | else 15 | expect(JSON.pretty_unparse(json)).to eq File.read("spec/jbuilder/examples/#{ name }.json") 16 | end 17 | end 18 | 19 | let (:resources) { create_list :post, 2, :with_author, :with_comments } 20 | 21 | let (:errors) do 22 | [ 23 | { 24 | id: 1, # Internal ID 25 | status: 404, # HTTP status 26 | title: 'Not found', 27 | detail: 'The requested resource cannot be found.', 28 | code: 112, # Some internal code 29 | source: { 30 | pointer: 'http://posts_path' 31 | }, 32 | links: { 33 | about: 'https://en.wikipedia.org/wiki/HTTP_404' 34 | } 35 | }, 36 | { 37 | id: 2, 38 | title: 'Another error' 39 | } 40 | ] 41 | end 42 | 43 | let (:meta) do 44 | { 45 | copyright: 'Vlad Faust', 46 | year: '2016', 47 | joke: { 48 | title: 'Not funny at all', 49 | body: 'A SQL query walks up to two tables in a restaurant and asks: "Mind if I join you?"' 50 | } 51 | } 52 | end 53 | 54 | it 'fetches resources' do 55 | json = JSON.parse(Jbuilder.new.api_format!(resources).target!) 56 | check json, 'resources' 57 | end 58 | 59 | it 'fetches resources w/ admin rights' do 60 | json = JSON.parse(Jbuilder.new.api_format!(resources, nil, nil, access_lvl: :admin).target!) 61 | check json, 'resources_admin' 62 | end 63 | 64 | it 'fetches resources w/ errors' do 65 | json = JSON.parse(Jbuilder.new.api_format!(resources, errors).target!) 66 | check json, 'resources_errors' 67 | end 68 | 69 | it 'fetches errors only' do 70 | json = JSON.parse(Jbuilder.new.api_format!(nil, errors).target!) 71 | check json, 'errors' 72 | end 73 | 74 | it 'fetches meta only' do 75 | json = JSON.parse(Jbuilder.new.api_format!(nil, nil, meta).target!) 76 | check json, 'meta' 77 | end 78 | 79 | it 'fetches errors w/ meta' do 80 | json = JSON.parse(Jbuilder.new.api_format!(nil, errors, meta).target!) 81 | check json, 'errors_meta' 82 | end 83 | 84 | it 'fetches resources w/ meta' do 85 | json = JSON.parse(Jbuilder.new.api_format!(resources, nil, meta).target!) 86 | check json, 'resources_meta' 87 | end 88 | 89 | it 'fetches resources w/ errors & meta' do 90 | json = JSON.parse(Jbuilder.new.api_format!(resources, errors, meta).target!) 91 | check json, 'resources_errors_meta' 92 | end 93 | 94 | it 'fetches internal errors' do 95 | json = JSON.parse(Jbuilder.new.api_format!(42, nil, meta).target!) 96 | check json, 'internal_errors' 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/jbuilder/examples/resources_admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "post", 5 | "id": 1, 6 | "attributes": { 7 | "id": 1, 8 | "title": "Title for post #1", 9 | "body": "Body for post #1" 10 | }, 11 | "relationships": { 12 | "author": { 13 | "data": [ 14 | { 15 | "type": "user", 16 | "id": 1 17 | } 18 | ] 19 | }, 20 | "comments": { 21 | "data": [ 22 | { 23 | "type": "comment", 24 | "id": 1 25 | }, 26 | { 27 | "type": "comment", 28 | "id": 2 29 | }, 30 | { 31 | "type": "comment", 32 | "id": 3 33 | }, 34 | { 35 | "type": "comment", 36 | "id": 4 37 | } 38 | ] 39 | } 40 | }, 41 | "meta": { 42 | "blah": "Just another meta info for post #1" 43 | } 44 | }, 45 | { 46 | "type": "post", 47 | "id": 2, 48 | "attributes": { 49 | "id": 2, 50 | "title": "Title for post #2", 51 | "body": "Body for post #2" 52 | }, 53 | "relationships": { 54 | "author": { 55 | "data": [ 56 | { 57 | "type": "user", 58 | "id": 4 59 | } 60 | ] 61 | }, 62 | "comments": { 63 | "data": [ 64 | { 65 | "type": "comment", 66 | "id": 1 67 | }, 68 | { 69 | "type": "comment", 70 | "id": 2 71 | }, 72 | { 73 | "type": "comment", 74 | "id": 3 75 | }, 76 | { 77 | "type": "comment", 78 | "id": 4 79 | } 80 | ] 81 | } 82 | }, 83 | "meta": { 84 | "blah": "Just another meta info for post #2" 85 | } 86 | } 87 | ], 88 | "included": [ 89 | { 90 | "type": "user", 91 | "id": 1, 92 | "attributes": { 93 | "id": 1, 94 | "name": "Name for user #1" 95 | } 96 | }, 97 | { 98 | "type": "comment", 99 | "id": 1, 100 | "attributes": { 101 | "id": 1, 102 | "body": "Body for comment #1" 103 | }, 104 | "relationships": { 105 | "user": { 106 | "data": [ 107 | { 108 | "type": "user", 109 | "id": 2 110 | } 111 | ] 112 | } 113 | } 114 | }, 115 | { 116 | "type": "comment", 117 | "id": 2, 118 | "attributes": { 119 | "id": 2, 120 | "body": "Body for comment #2" 121 | }, 122 | "relationships": { 123 | "user": { 124 | "data": [ 125 | { 126 | "type": "user", 127 | "id": 3 128 | } 129 | ] 130 | } 131 | } 132 | }, 133 | { 134 | "type": "comment", 135 | "id": 3, 136 | "attributes": { 137 | "id": 3, 138 | "body": "Body for comment #3" 139 | }, 140 | "relationships": { 141 | "user": { 142 | "data": [ 143 | { 144 | "type": "user", 145 | "id": 5 146 | } 147 | ] 148 | } 149 | } 150 | }, 151 | { 152 | "type": "comment", 153 | "id": 4, 154 | "attributes": { 155 | "id": 4, 156 | "body": "Body for comment #4" 157 | }, 158 | "relationships": { 159 | "user": { 160 | "data": [ 161 | { 162 | "type": "user", 163 | "id": 6 164 | } 165 | ] 166 | } 167 | } 168 | }, 169 | { 170 | "type": "user", 171 | "id": 4, 172 | "attributes": { 173 | "id": 4, 174 | "name": "Name for user #4" 175 | } 176 | } 177 | ] 178 | } -------------------------------------------------------------------------------- /lib/jbuilder/json_api.rb: -------------------------------------------------------------------------------- 1 | require 'jbuilder' 2 | require 'jbuilder/json_api/version' 3 | 4 | module JsonAPI 5 | # Returns a valid-formatted JSON which follows JSON-API specifications: 6 | # http://jsonapi.org/ 7 | # 8 | # ==== Arguments 9 | # * +resources+ - list of resources to render (may be even one or nil); 10 | # * +errors+ - array of hashes in the below format: 11 | # [{ status: 422, detail: 'This error occurs because...' }, {...}] 12 | # * +meta+ - a hash representing any meta (additional) information. 13 | # 14 | # ==== Options 15 | # Any information can be passed as +options+ argument; resources' class methods 16 | # +json_api_attrs+, +json_api_relations+ and +json_api_meta+ 17 | # will be invoked with this argument. 18 | # 19 | def api_format! (resources = nil, errors = nil, meta = nil, options = {}) 20 | begin 21 | # Firstly, print meta 22 | # http://jsonapi.org/format/#document-meta 23 | # 24 | if meta && !meta.empty? 25 | meta meta 26 | end 27 | 28 | # Secondly, take care of errors. If there are any, 29 | # no 'data' section should be represented. 30 | # http://jsonapi.org/format/#document-top-level 31 | # 32 | # Read more at 33 | # http://jsonapi.org/format/#errors 34 | # 35 | if errors && !errors.empty? 36 | ignore_nil! true 37 | errors errors do |error| 38 | id error[:id] 39 | status error[:status] 40 | detail error[:detail] 41 | code error[:code] 42 | title error[:title] 43 | 44 | if error[:source] 45 | source do 46 | pointer error[:source][:pointer] 47 | paramater error[:source][:parameter] 48 | end 49 | end 50 | 51 | if error[:links] 52 | links do 53 | about error[:links][:about] 54 | end 55 | end 56 | end 57 | return self 58 | end 59 | 60 | resources = [*resources] 61 | 62 | # http://jsonapi.org/format/#document-links 63 | # 64 | if @context 65 | links do 66 | set! 'self', @context.request.path 67 | end 68 | end 69 | 70 | data do 71 | resources.blank? ? array! : _api_resource_objects(resources, options) 72 | end 73 | 74 | included = [] 75 | resources.each do |resource| 76 | next unless resource.respond_to?'json_api_relations' 77 | resource.json_api_relations(options).each do |relationship| 78 | included += [*(resource.send(relationship))] 79 | end 80 | end 81 | included.uniq! 82 | 83 | included do 84 | _api_resource_objects(included, options, resources) unless included.blank? 85 | end 86 | 87 | self 88 | rescue Exception => e 89 | @attributes = {} 90 | message = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.3.0') ? e.original_message : e.message 91 | return api_format! nil, [{ status: 500, title: e.class.to_s, detail: message }] 92 | end 93 | end 94 | 95 | private 96 | 97 | # Formats a resources array properly 98 | # http://jsonapi.org/format/#document-resource-objects 99 | # 100 | def _api_resource_objects (resources, options, parent_resources = nil) 101 | resources.each do |resource| 102 | child! do 103 | type resource.class.name.demodulize.to_s.downcase 104 | id resource.id 105 | 106 | # http://jsonapi.org/format/#document-resource-object-attributes 107 | # 108 | if resource.respond_to?'json_api_attrs' 109 | attributes do 110 | resource.json_api_attrs(options).each do |attribute| 111 | set! attribute, resource.send(attribute) 112 | end 113 | end 114 | end 115 | 116 | # http://jsonapi.org/format/#document-resource-object-relationships 117 | # 118 | if resource.respond_to?'json_api_relations' 119 | unless resource.json_api_relations(options).blank? 120 | relationships do 121 | resource.json_api_relations(options).each do |relationship| 122 | set! relationship do 123 | if @context 124 | links do 125 | related @context.send("#{ relationship.pluralize }_path") 126 | # TODO add a link to the relationship itself 127 | end 128 | end 129 | 130 | data do 131 | [*(resource.send(relationship))].each do |relationship_instance| 132 | # Relationships shouldn't ever link to the parent resource 133 | # 134 | next if !parent_resources.nil? && parent_resources.include?(relationship_instance) 135 | child! do 136 | type relationship_instance.class.name.demodulize.to_s.downcase 137 | id relationship_instance.id 138 | end 139 | end 140 | end 141 | end 142 | end 143 | end 144 | end 145 | end 146 | 147 | if resource.respond_to?'json_api_meta' 148 | # We don't want to see 'meta': null 149 | ignore_nil! true 150 | meta resource.json_api_meta(options) 151 | ignore_nil! @ignore_nil 152 | end 153 | 154 | if @context 155 | links do 156 | set! 'self', @context.send("#{ resource.class.name.demodulize.to_s.downcase }_path", resource) 157 | end 158 | end 159 | end 160 | end 161 | end 162 | end 163 | 164 | Jbuilder.include JsonAPI 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jbuilder::JsonApi | [![Gem Version](https://badge.fury.io/rb/jbuilder-json_api.svg)](https://badge.fury.io/rb/jbuilder-json_api) ![[RubyGems Page](https://rubygems.org/gems/jbuilder-json_api)](http://ruby-gem-downloads-badge.herokuapp.com/jbuilder-json_api?color=brightgreen&type=total) [![Build Status](https://travis-ci.org/vladfaust/jbuilder-json_api.svg?branch=master)](https://travis-ci.org/vladfaust/jbuilder-json_api) [![Dependency Status](https://gemnasium.com/vladfaust/jbuilder-json_api.svg)](https://gemnasium.com/vladfaust/jbuilder-json_api) 2 | 3 | Adds a `json.api_format!(resources)` method to quickly represent a resource or collection in a valid [JSON API](http://jsonapi.org/) format without any new superclasses or weird setups. Set'n'go! :rocket: 4 | 5 | ## Motivation 6 | 7 | Official JSON API [implementations page](http://jsonapi.org/implementations/#server-libraries-ruby) shows us a variety of different serializers and other heavy-weight stuff. I'm in love with [Jbuilder](https://github.com/rails/jbuilder), as it allows to format json responses with ease. Therefore I wanted to connect Jbuilder and JsonApi.org specs. 8 | 9 | I'd like to notice that there already is one gem called [jbuilder-jsonapi](https://github.com/csexton/jbuilder-jsonapi) by [csexton](https://github.com/csexton), but it adds a links helper only. It's not enough for me! :facepunch: 10 | 11 | As a result, I've created a **very** lightweight & flexible solution - all you need is Jbuilder and this gem. Then you should delete everything within your `*.json.jbuilder` files and replace it with below recommendations (just one line! :flushed:). After you are free to customize parsed attributes and relationships with three tiny methods. 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'jbuilder-json_api' 19 | ``` 20 | 21 | And then execute: 22 | 23 | $ bundle 24 | 25 | Or install it yourself as: 26 | 27 | $ gem install jbuilder-json_api 28 | 29 | ## Usage 30 | 31 | **Firstly, read JSON API specifications: http://jsonapi.org/format/** 32 | 33 | The gem adds method `#api_format!(resources = nil, errors = nil, meta = nil, options = {})` to `Jbuilder` class and therefore to all its children, e.g. `JbuilderTemplate`. Possible arguments are: 34 | - `resources` goes for a collection of objects, e.g. array of Articles. It can also be a single object or just an empty array; 35 | - `errors` goes for a collection of errors. It **must** be an array of hashes; 36 | - `meta` is any hash representing some additional request-level information; 37 | - `options` can be any object. It will be passed to resources while the method is being executed. 38 | 39 | Replace any content within any `*.json.jbuilder` file with the code below: 40 | ```ruby 41 | # Common example 42 | json.api_format! @resources, @errors, meta, options 43 | 44 | # Items example 45 | json.api_format! @items, @errors, nil, access_level: :admin 46 | 47 | # A simple items example 48 | json.api_format! @items 49 | ``` 50 | You can also render formatted JSON straight from controller actions: 51 | ```ruby 52 | respond_to do |f| 53 | f.json { render layout: false, json: JSON.parse(JbuilderTemplate.new(view_context).api_format!(@item).target!) } 54 | f.html { render nothing: true, status: :bad_request } 55 | end 56 | ``` 57 | Each resource instance, as well as the included one, will be invoked with `json_api_attrs (options)`, `json_api_relations (options)` & `json_api_meta (options)` methods. These methods **MAY** be implemented within each model. `api_format!` method will try to get an object's permitted attributes (**remember, you are free do define authentication logic yourself!**) and relations and meta information via those three methods. 58 | 59 | Here is an example of implementation: 60 | ```ruby 61 | # Inside Item model 62 | 63 | def json_api_attrs (options = {}) 64 | attrs = [] 65 | attrs += %w(name description price buyoutable item_type category) if %i(user admin).include?options[:access_level] 66 | attrs += %w(real_price in_stock) if options[:access_level] == :admin 67 | attrs 68 | end 69 | 70 | def json_api_relations (options = {}) 71 | %w(category orders) 72 | end 73 | 74 | def json_api_meta (options = {}) 75 | { foo: :bar } 76 | end 77 | ``` 78 | **Note** that the gem will call methods pulled via `json_api_relations and _attrs`. As for the above example, methods like `:name`, `:description`, `:real_price`, `:orders` will be invoked for an Item instance. And yes, relations are fetched properly and recursively if the object responds to `orders`. 79 | 80 | ## Development 81 | 82 | 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. 83 | 84 | 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](https://rubygems.org). 85 | 86 | ## Contributing 87 | 88 | Bug reports and pull requests are welcome on GitHub at [https://github.com/vladfaust/jbuilder-json_api](https://github.com/vladfaust/jbuilder-json_api). It would be really good if someone contributes. :smile: 89 | 90 | ## ToDo 91 | 92 | - [ ] Maybe add `Content-Type: application/vnd.api+json`. This spec is ignored right now :smirk: 93 | - [ ] Add links tests and improve them. Links now work only within views (where `@context` is present). 94 | - [ ] Somehow implement `[fields]` parameter 95 | 96 | ## Versions 97 | 98 | #### 0.0.1 -> 1.0.0 99 | 100 | **Breaking:** 101 | - [x] Now any value can be forwarded to resources' methods via last `options` argument. 102 | - [x] Added third argument `meta`, which is used to show meta information in the context of request 103 | 104 | **Not breaking:** 105 | - [x] Added support for `json_api_meta (options)` method. 106 | - [x] Any internal error is now properly handled. 107 | --------------------------------------------------------------------------------