├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin ├── audit ├── ci ├── quality └── spec ├── gemfiles ├── rails42.gemfile ├── rails5.gemfile ├── rails60.gemfile └── rails61.gemfile ├── jsonapi_parameters.gemspec ├── lib ├── jsonapi_parameters.rb └── jsonapi_parameters │ ├── core_ext.rb │ ├── core_ext │ ├── action_controller │ │ └── parameters.rb │ └── action_dispatch │ │ └── http │ │ └── mime_type.rb │ ├── default_handlers │ ├── base_handler.rb │ ├── nil_relation_handler.rb │ ├── to_many_relation_handler.rb │ └── to_one_relation_handler.rb │ ├── handlers.rb │ ├── parameters.rb │ ├── stack_limit.rb │ ├── translator.rb │ └── version.rb └── spec ├── app ├── Rakefile ├── app │ ├── controllers │ │ ├── application_controller.rb │ │ ├── authors_camel_controller.rb │ │ ├── authors_controller.rb │ │ ├── authors_dashed_controller.rb │ │ ├── authors_deprecated_to_jsonapi_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── pong_controller.rb │ │ ├── posts_controller.rb │ │ └── stack_tester_controller.rb │ ├── models │ │ ├── application_record.rb │ │ ├── author.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── post.rb │ │ └── scissors.rb │ └── serializers │ │ ├── author_serializer.rb │ │ ├── post_serializer.rb │ │ └── scissors_serializer.rb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ ├── update │ └── yarn ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── jsonapi_parameters.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── secrets.yml │ ├── spring.rb │ └── storage.yml ├── db │ ├── development.sqlite3 │ ├── migrate │ │ ├── 20190115235549_create_posts.rb │ │ ├── 20190115235603_create_authors.rb │ │ ├── 20190423142019_add_category_name_to_posts.rb │ │ └── 20191206150129_create_scissors.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── spec │ ├── rails_helper.rb │ └── spec_helper.rb └── storage │ └── .keep ├── core_ext └── action_controller │ └── parameters_spec.rb ├── factories ├── authors_factory.rb └── posts_factory.rb ├── integration ├── authors_camel_controller_spec.rb ├── authors_controller_spec.rb ├── authors_dashed_controller_spec.rb ├── authors_deprecated_to_jsonapi_controller_spec.rb ├── pong_controller_spec.rb └── stack_tester_controller_spec.rb ├── lib └── jsonapi_parameters │ ├── handlers_spec.rb │ ├── mime_type_spec.rb │ ├── stack_limit_spec.rb │ └── translator_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support └── inputs_outputs_pairs.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | references: 3 | attach_workspace_to_tmp: &attach_workspace_to_tmp 4 | attach_workspace: 5 | at: ~/project/tmp 6 | bundle: &bundle 7 | run: 8 | name: Bundle 9 | command: | 10 | if [ $BUNDLE_GEMFILE = ./gemfiles/rails42.gemfile ]; then 11 | gem install -v '1.17.3' bundler; 12 | bundle _1.17.3_ install; 13 | else 14 | gem install bundler; 15 | bundle install; 16 | fi 17 | gemfile_lock_audit: &gemfile_lock_audit 18 | run: 19 | name: audit 20 | command: | 21 | if [ $BUNDLE_GEMFILE != ./gemfiles/rails42.gemfile ]; then 22 | bin/audit; 23 | fi 24 | specs: &specs 25 | run: 26 | name: bin/ci 27 | command: bin/ci 28 | generate_coverage: &generate_coverage 29 | run: 30 | name: Generate coverage 31 | command: ./tmp/cc-test-reporter format-coverage -t simplecov -o ./tmp/codeclimate.$CIRCLE_JOB.json coverage/.resultset.json 32 | persist_to_workspace_coverage: &persist_to_workspace_coverage 33 | persist_to_workspace: 34 | root: ~/project/tmp 35 | paths: 36 | - ./*.json 37 | 38 | jobs: 39 | download-and-persist-cc-test-reporter: 40 | docker: 41 | - image: circleci/ruby:2.6 42 | steps: 43 | - run: 44 | name: Download cc-test-reporter 45 | command: | 46 | mkdir -p tmp/ 47 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter 48 | chmod +x ./tmp/cc-test-reporter 49 | - persist_to_workspace: 50 | root: tmp 51 | paths: 52 | - cc-test-reporter 53 | upload-test-coverage: 54 | docker: 55 | - image: circleci/ruby:2.6 56 | steps: 57 | - attach_workspace: 58 | at: ~/project/tmp 59 | - run: 60 | name: Upload coverage results to Code Climate 61 | command: | 62 | ./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p $(ls -la |grep -i ruby |wc -l |awk '{print $1}') -o tmp/codeclimate.total.json 63 | ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json 64 | test: 65 | parameters: 66 | ruby_version: 67 | type: string 68 | gemfile: 69 | type: string 70 | docker: 71 | - image: "circleci/ruby:<>" 72 | environment: 73 | BUNDLE_GEMFILE: "./gemfiles/<>" 74 | steps: 75 | - checkout 76 | - <<: *attach_workspace_to_tmp 77 | - <<: *bundle 78 | - <<: *gemfile_lock_audit 79 | - <<: *specs 80 | - <<: *generate_coverage 81 | - <<: *persist_to_workspace_coverage 82 | 83 | workflows: 84 | version: 2 85 | 86 | commit: 87 | jobs: 88 | - download-and-persist-cc-test-reporter 89 | - upload-test-coverage: 90 | filters: 91 | branches: 92 | only: 93 | - master 94 | requires: 95 | - test 96 | - test: 97 | matrix: 98 | parameters: 99 | ruby_version: ["2.5", "2.6", "2.7", "3.0"] 100 | gemfile: ["rails61.gemfile", "rails60.gemfile", "rails5.gemfile", "rails42.gemfile"] 101 | exclude: 102 | - ruby_version: "3.0" 103 | gemfile: rails5.gemfile 104 | - ruby_version: "3.0" 105 | gemfile: rails42.gemfile 106 | - ruby_version: "2.7" 107 | gemfile: rails42.gemfile 108 | - ruby_version: "2.6" 109 | gemfile: rails42.gemfile 110 | requires: 111 | - download-and-persist-cc-test-reporter 112 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | gemfiles/*.lock 3 | *.gem 4 | .bundle/ 5 | log/*.log 6 | pkg/ 7 | 8 | spec/app/db/*.sqlite3 9 | spec/app/db/*.sqlite3-journal 10 | spec/app/log/*.log 11 | spec/app/node_modules/ 12 | spec/app/yarn-error.log 13 | spec/app/storage/ 14 | spec/app/tmp/ 15 | spec/app/log/* 16 | spec/app/.generators 17 | spec/app/.rakeTasks 18 | 19 | .ruby-version 20 | 21 | .idea 22 | jsonapi_parameters.iml 23 | 24 | coverage -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.3.0 5 | DisplayCopNames: true 6 | Exclude: 7 | - '**/config.ru' 8 | - '**/Rakefile' 9 | - 'bin/**/*' 10 | - 'tmp/**/*' 11 | - '*.gemspec' 12 | - '**/spec/support/inputs_outputs_pairs.rb' 13 | - '**/spec/app/**/*' 14 | - 'gemfiles/**/*' 15 | - 'vendor/**/*' 16 | Documentation: 17 | Enabled: false 18 | Lint/ShadowingOuterLocalVariable: 19 | Enabled: false 20 | Lint/AmbiguousBlockAssociation: 21 | Enabled: false 22 | RSpec/AnyInstance: 23 | Enabled: false 24 | RSpec/MessageSpies: 25 | Enabled: false 26 | RSpec/ContextWording: 27 | Enabled: false 28 | RSpec/MultipleDescribes: 29 | Enabled: false 30 | RSpec/ExampleLength: 31 | Max: 80 32 | RSpec/MultipleExpectations: 33 | Enabled: false 34 | RSpec/NestedGroups: 35 | Enabled: false 36 | Metrics/BlockLength: 37 | Exclude: 38 | - 'spec/**/*_spec.rb' 39 | Metrics/AbcSize: 40 | Max: 23 41 | Metrics/CyclomaticComplexity: 42 | Max: 8 43 | Metrics/PerceivedComplexity: 44 | Max: 8 45 | Metrics/LineLength: 46 | Max: 150 47 | Exclude: 48 | - 'spec/**/*_spec.rb' 49 | Metrics/ModuleLength: 50 | Max: 120 51 | Metrics/MethodLength: 52 | Max: 25 53 | Style/ClassAndModuleChildren: 54 | Enabled: false 55 | Style/EmptyMethod: 56 | Enabled: false 57 | Style/FrozenStringLiteralComment: 58 | Enabled: false 59 | Style/PercentLiteralDelimiters: 60 | PreferredDelimiters: 61 | default: () 62 | '%i': () 63 | '%w': () 64 | Style/RaiseArgs: 65 | EnforcedStyle: compact 66 | Style/RegexpLiteral: 67 | StyleGuide: slashes 68 | Style/SymbolArray: 69 | EnforcedStyle: brackets 70 | Style/StringLiterals: 71 | EnforcedStyle: single_quotes 72 | RSpec/HookArgument: 73 | Enabled: false 74 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | gemspec name: 'jsonapi_parameters' 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 marahin 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JsonApi::Parameters 2 | Simple [JSON:API](https://jsonapi.org/) compliant parameters translator. 3 | 4 | [![Gem Version](https://badge.fury.io/rb/jsonapi_parameters.svg)](https://badge.fury.io/rb/jsonapi_parameters) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/84fd5b548eea8d7e18af/maintainability)](https://codeclimate.com/github/visualitypl/jsonapi_parameters/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/84fd5b548eea8d7e18af/test_coverage)](https://codeclimate.com/github/visualitypl/jsonapi_parameters/test_coverage) 7 | [![CircleCI](https://circleci.com/gh/visualitypl/jsonapi_parameters.svg?style=svg)](https://circleci.com/gh/visualitypl/jsonapi_parameters) 8 | 9 | [Documentation](https://github.com/visualitypl/jsonapi_parameters/wiki) 10 | 11 | ## Usage 12 | 13 | ### Installation 14 | Add this line to your application's Gemfile: 15 | 16 | ```ruby 17 | gem 'jsonapi_parameters' 18 | ``` 19 | 20 | And then execute: 21 | 22 | ```bash 23 | $ bundle 24 | ``` 25 | 26 | Or install it yourself as: 27 | 28 | ```bash 29 | $ gem install jsonapi_parameters 30 | ``` 31 | 32 | ### Rails 33 | 34 | Usually your strong parameters in controller are invoked this way: 35 | 36 | ```ruby 37 | def create 38 | model = Model.new(create_params) 39 | 40 | if model.save 41 | ... 42 | else 43 | head 500 44 | end 45 | end 46 | 47 | private 48 | 49 | def create_params 50 | params.require(:model).permit(:name) 51 | end 52 | ``` 53 | 54 | With jsonapi_parameters, the difference is just the params: 55 | 56 | ```ruby 57 | def create_params 58 | params.from_jsonapi.require(:model).permit(:name) 59 | end 60 | ``` 61 | 62 | #### Relationships 63 | 64 | JsonApi::Parameters supports ActiveRecord relationship parameters, including [nested attributes](https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html). 65 | 66 | Relationship parameters are being read from two optional trees: 67 | 68 | * `relationships`, 69 | * `included` 70 | 71 | If you provide any related resources in the `relationships` table, this gem will also look for corresponding, `included` resources and their attributes. Thanks to that this gem supports nested attributes, and will try to translate these included resources and pass them along. 72 | 73 | For more examples take a look at [Relationships](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationships) in the wiki documentation. 74 | 75 | 76 | ### Plain Ruby / outside Rails 77 | 78 | ```ruby 79 | 80 | params = { # JSON:API compliant parameters here 81 | # ... 82 | } 83 | 84 | class Translator 85 | include JsonApi::Parameters 86 | end 87 | translator = Translator.new 88 | 89 | translator.jsonapify(params) 90 | ``` 91 | 92 | ## Mime Type 93 | 94 | As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) correct mime type for JSON:API input should be [`application/vnd.api+json`](http://www.iana.org/assignments/media-types/application/vnd.api+json). 95 | 96 | This gem's intention is to make input consumption as easy as possible. Hence, it [registers this mime type for you](lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb). 97 | 98 | ## Stack limit 99 | 100 | In theory, any payload may consist of infinite amount of relationships (and so each relationship may have its own, included, infinite amount of nested relationships). 101 | Because of that, it is a potential vector of attack. 102 | 103 | For this reason we have introduced a default limit of stack levels that JsonApi::Parameters will go down through while parsing the payloads. 104 | 105 | This default limit is 3, and can be overwritten by specifying the custom limit. 106 | 107 | #### Ruby 108 | ```ruby 109 | class Translator 110 | include JsonApi::Parameters 111 | end 112 | 113 | translator = Translator.new 114 | 115 | translator.jsonapify(custom_stack_limit: 4) 116 | 117 | # OR 118 | 119 | translator.stack_limit = 4 120 | translator.jsonapify.(...) 121 | ``` 122 | 123 | #### Rails 124 | ```ruby 125 | def create_params 126 | params.from_jsonapi(custom_stack_limit: 4).require(:user).permit( 127 | entities_attributes: { subentities_attributes: { ... } } 128 | ) 129 | end 130 | 131 | # OR 132 | 133 | def create_params 134 | params.stack_level = 4 135 | 136 | params.from_jsonapi.require(:user).permit(entities_attributes: { subentities_attributes: { ... } }) 137 | ensure 138 | params.reset_stack_limit! 139 | end 140 | ``` 141 | 142 | ## Customization 143 | 144 | If you need custom relationship handling (for instance, if you have a relationship named `scissors` that is plural, but it actually is a single entity), you can use Handlers to define appropriate behaviour. 145 | 146 | Read more at [Relationship Handlers](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationship-handlers). 147 | 148 | ## Team 149 | 150 | Project started by [Jasiek Matusz](https://github.com/Marahin). 151 | 152 | Currently, jsonapi_parameters is maintained by Visuality's [Open Source Commitee](https://www.visuality.pl/open-source). 153 | 154 | ## License 155 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 156 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'JsonApiParameters' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.md') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | require 'bundler/gem_tasks' 18 | require 'rspec/core/rake_task' 19 | 20 | task :default => :spec 21 | -------------------------------------------------------------------------------- /bin/audit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'shellwords' 4 | 5 | gemfile_option = %W[--gemfile-lock #{ENV['BUNDLE_GEMFILE']}.lock] if ENV['BUNDLE_GEMFILE'].to_s != '' 6 | command_to_execute = 7 | Shellwords.split("bundle exec bundle-audit check --update") 8 | .concat(Array.new(gemfile_option || [])) 9 | .compact 10 | .shelljoin 11 | puts command_to_execute 12 | system(command_to_execute) || raise('Error on bundle audit') 13 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | bundle exec bin/quality 6 | bundle exec bin/spec 7 | -------------------------------------------------------------------------------- /bin/quality: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | bundle exec rubocop --config .rubocop.yml 6 | -------------------------------------------------------------------------------- /bin/spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle exec rspec spec --format documentation -------------------------------------------------------------------------------- /gemfiles/rails42.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | gem 'rails', '~> 4.2.0' 5 | gem 'sqlite3', '~> 1.3.6' 6 | 7 | gemspec name: 'jsonapi_parameters', path: '../' 8 | -------------------------------------------------------------------------------- /gemfiles/rails5.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | gem 'rails', '~> 5.0' 5 | gem 'sqlite3', '~> 1.4.0' 6 | 7 | gemspec name: 'jsonapi_parameters', path: '../' 8 | -------------------------------------------------------------------------------- /gemfiles/rails60.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | gem 'rails', '~> 6.0.0' 5 | gem 'sqlite3', '~> 1.4.0' 6 | 7 | gemspec name: 'jsonapi_parameters', path: '../' 8 | -------------------------------------------------------------------------------- /gemfiles/rails61.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | gem 'rails', '~> 6.1.0' 5 | gem 'sqlite3', '~> 1.4.0' 6 | 7 | gemspec name: 'jsonapi_parameters', path: '../' 8 | -------------------------------------------------------------------------------- /jsonapi_parameters.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('lib', __dir__) 2 | 3 | require 'jsonapi_parameters/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'jsonapi_parameters' 7 | spec.version = JsonApi::Parameters::VERSION 8 | spec.authors = ['Visuality', 'marahin'] 9 | spec.email = ['contact@visuality.pl', 'me@marahin.pl'] 10 | spec.homepage = 'https://github.com/visualitypl/jsonapi_parameters' 11 | spec.summary = 'Translator for JSON:API compliant parameters' 12 | spec.description = 'JsonApi::Parameters allows you to easily translate JSON:API compliant parameters to a structure expected by Rails.' 13 | spec.license = 'MIT' 14 | 15 | spec.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md'] 16 | spec.required_ruby_version = '>= 2.5.0' 17 | 18 | spec.add_runtime_dependency 'activesupport', '>= 4.1.8' 19 | spec.add_runtime_dependency 'actionpack', '>= 4.1.8' 20 | 21 | spec.add_development_dependency 'sqlite3' 22 | spec.add_development_dependency 'database_cleaner' 23 | spec.add_development_dependency 'rails', '>= 4.1.8', '< 6.1' 24 | spec.add_development_dependency 'rspec', '~> 3.8' 25 | spec.add_development_dependency 'rspec-rails', '~> 3.8' 26 | spec.add_development_dependency 'pry' 27 | spec.add_development_dependency 'rubocop', '~> 0.62.0' 28 | spec.add_development_dependency 'rubocop-rspec', '~> 1.31.0' 29 | spec.add_development_dependency 'bundler-audit', '~> 0.8.0' 30 | spec.add_development_dependency 'fast_jsonapi', '~> 1.5' 31 | spec.add_development_dependency 'factory_bot', '~> 4.11.1' 32 | spec.add_development_dependency 'faker', '~> 1.9.1' 33 | spec.add_development_dependency 'hashdiff', '~> 0.3.8' 34 | spec.add_development_dependency 'simplecov', '~> 0.16.1' 35 | spec.add_development_dependency 'simplecov-console', '~> 0.4.2' 36 | end 37 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi_parameters/parameters' 2 | require 'jsonapi_parameters/handlers' 3 | require 'jsonapi_parameters/translator' 4 | require 'jsonapi_parameters/core_ext' 5 | require 'jsonapi_parameters/stack_limit' 6 | require 'jsonapi_parameters/version' 7 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi_parameters/core_ext/action_controller/parameters' 2 | require 'jsonapi_parameters/core_ext/action_dispatch/http/mime_type' 3 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/core_ext/action_controller/parameters.rb: -------------------------------------------------------------------------------- 1 | require 'action_controller' 2 | 3 | class ActionController::Parameters 4 | include JsonApi::Parameters 5 | 6 | def to_jsonapi(*args) 7 | warn "WARNING: #to_jsonapi method is deprecated. Please use #from_jsonapi instead.\n#{caller(1..1).first}" 8 | 9 | from_jsonapi(*args) 10 | end 11 | 12 | def from_jsonapi(naming_convention = :snake, custom_stack_limit: stack_limit) 13 | @from_jsonapi ||= self.class.new jsonapify(self, naming_convention: naming_convention, custom_stack_limit: custom_stack_limit) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb: -------------------------------------------------------------------------------- 1 | require 'action_dispatch/http/mime_type' 2 | 3 | API_MIME_TYPES = %w( 4 | application/vnd.api+json 5 | text/x-json 6 | application/json 7 | ).freeze 8 | 9 | Mime::Type.register API_MIME_TYPES.first, :json, API_MIME_TYPES 10 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/default_handlers/base_handler.rb: -------------------------------------------------------------------------------- 1 | module JsonApi 2 | module Parameters 3 | module Handlers 4 | module DefaultHandlers 5 | class BaseHandler 6 | attr_reader :relationship_key, :relationship_value, :included 7 | 8 | def self.call(key, val, included) 9 | new(key, val, included).handle 10 | end 11 | 12 | def initialize(relationship_key, relationship_value, included) 13 | @relationship_key = relationship_key 14 | @relationship_value = relationship_value 15 | @included = included 16 | end 17 | 18 | def find_included_object(related_id:, related_type:) 19 | included.find do |included_object_enum| 20 | included_object_enum[:id] && 21 | included_object_enum[:id] == related_id && 22 | included_object_enum[:type] && 23 | included_object_enum[:type] == related_type 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/default_handlers/nil_relation_handler.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | require_relative './base_handler' 4 | 5 | module JsonApi 6 | module Parameters 7 | module Handlers 8 | module DefaultHandlers 9 | class NilRelationHandler < BaseHandler 10 | include ActiveSupport::Inflector 11 | 12 | def handle 13 | # Graceful fail if nil on to-many association 14 | # in case the relationship key is, for instance, `billable_hours`, 15 | # we have to assume that it is a to-many relationship. 16 | if pluralize(relationship_key).to_sym == relationship_key 17 | raise NotImplementedError.new( 18 | 'plural resource cannot be nullified - please create a custom handler for this relation' 19 | ) 20 | end 21 | 22 | # Handle with empty hash. 23 | ToOneRelationHandler.new(relationship_key, {}, {}).handle 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | module JsonApi 4 | module Parameters 5 | module Handlers 6 | module DefaultHandlers 7 | class ToManyRelationHandler < BaseHandler 8 | include ActiveSupport::Inflector 9 | 10 | attr_reader :with_inclusion, :vals, :key 11 | 12 | def handle 13 | @with_inclusion = !relationship_value.empty? 14 | 15 | prepare_relationship_vals 16 | 17 | generate_key 18 | 19 | [key, vals] 20 | end 21 | 22 | private 23 | 24 | def prepare_relationship_vals 25 | @vals = relationship_value.map do |relationship| 26 | related_id = relationship.dig(:id) 27 | related_type = relationship.dig(:type) 28 | 29 | included_object = find_included_object( 30 | related_id: related_id, related_type: related_type 31 | ) || {} 32 | 33 | # If at least one related object has not been found in `included` tree, 34 | # we should not attempt to "#{relationship_key}_attributes" but 35 | # "#{relationship_key}_ids" instead. 36 | @with_inclusion &= !included_object.empty? 37 | 38 | if with_inclusion 39 | { **(included_object[:attributes] || {}), id: related_id }.tap do |body| 40 | body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships 41 | end 42 | else 43 | relationship.dig(:id) 44 | end 45 | end 46 | end 47 | 48 | def generate_key 49 | @key = (with_inclusion ? "#{pluralize(relationship_key)}_attributes" : "#{singularize(relationship_key)}_ids").to_sym 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | module JsonApi 4 | module Parameters 5 | module Handlers 6 | module DefaultHandlers 7 | class ToOneRelationHandler < BaseHandler 8 | include ActiveSupport::Inflector 9 | 10 | def handle 11 | related_id = relationship_value.dig(:id) 12 | related_type = relationship_value.dig(:type) 13 | 14 | included_object = find_included_object( 15 | related_id: related_id, related_type: related_type 16 | ) || {} 17 | 18 | return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty? 19 | 20 | included_object = { **(included_object[:attributes] || {}), id: related_id }.tap do |body| 21 | body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships 22 | end 23 | 24 | ["#{singularize(relationship_key)}_attributes".to_sym, included_object] 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/handlers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'default_handlers/nil_relation_handler' 2 | require_relative 'default_handlers/to_many_relation_handler' 3 | require_relative 'default_handlers/to_one_relation_handler' 4 | 5 | module JsonApi 6 | module Parameters 7 | module Handlers 8 | include DefaultHandlers 9 | 10 | DEFAULT_HANDLER_SET = { 11 | to_many: ToManyRelationHandler, 12 | to_one: ToOneRelationHandler, 13 | nil: NilRelationHandler 14 | }.freeze 15 | 16 | module_function 17 | 18 | def add_handler(handler_name, klass) 19 | handlers[handler_name.to_sym] = klass 20 | end 21 | 22 | def set_resource_handler(resource_key, handler_key) 23 | unless handlers.key?(handler_key) 24 | raise NotImplementedError.new( 25 | 'handler_key does not match any registered handlers' 26 | ) 27 | end 28 | 29 | resource_handlers[resource_key.to_sym] = handler_key.to_sym 30 | end 31 | 32 | def reset_handlers 33 | @handlers = DEFAULT_HANDLER_SET.dup 34 | @resource_handlers = {} 35 | end 36 | 37 | def resource_handlers 38 | @resource_handlers ||= {} 39 | end 40 | 41 | def handlers 42 | @handlers ||= DEFAULT_HANDLER_SET.dup 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/parameters.rb: -------------------------------------------------------------------------------- 1 | module JsonApi 2 | module Parameters 3 | @ensure_underscore_translation = false 4 | 5 | class << self 6 | attr_accessor :ensure_underscore_translation 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/stack_limit.rb: -------------------------------------------------------------------------------- 1 | module JsonApi 2 | module Parameters 3 | LIMIT = 3 4 | 5 | class StackLevelTooDeepError < StandardError 6 | end 7 | 8 | def stack_limit=(val) 9 | @stack_limit = val 10 | end 11 | 12 | def stack_limit 13 | @stack_limit || LIMIT 14 | end 15 | 16 | def reset_stack_limit 17 | @stack_limit = LIMIT 18 | end 19 | 20 | private 21 | 22 | def initialize_stack(custom_stack_limit) 23 | @current_stack_level = 0 24 | @stack_limit = custom_stack_limit 25 | end 26 | 27 | def increment_stack_level! 28 | @current_stack_level += 1 29 | 30 | raise StackLevelTooDeepError.new(stack_exception_message) if @current_stack_level > stack_limit 31 | end 32 | 33 | def decrement_stack_level 34 | @current_stack_level -= 1 35 | end 36 | 37 | def reset_stack_level 38 | @current_stack_level = 0 39 | end 40 | 41 | def stack_exception_message 42 | "Stack level of nested payload is too deep: #{@current_stack_level}/#{stack_limit}. Please see the documentation on how to overwrite the limit." 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/translator.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | 3 | module JsonApi::Parameters 4 | include ActiveSupport::Inflector 5 | 6 | def jsonapify(params, naming_convention: :snake, custom_stack_limit: stack_limit) 7 | initialize_stack(custom_stack_limit) 8 | 9 | jsonapi_translate(params, naming_convention: naming_convention) 10 | end 11 | 12 | private 13 | 14 | def jsonapi_translate(params, naming_convention:) 15 | params = params.to_unsafe_h if params.is_a?(ActionController::Parameters) 16 | 17 | return params if params.nil? || params.empty? 18 | 19 | @jsonapi_unsafe_hash = if naming_convention != :snake || JsonApi::Parameters.ensure_underscore_translation 20 | params = params.deep_transform_keys { |key| key.to_s.underscore.to_sym } 21 | params[:data][:type] = params[:data][:type].underscore if params.dig(:data, :type) 22 | params 23 | else 24 | params.deep_symbolize_keys 25 | end 26 | 27 | formed_parameters 28 | end 29 | 30 | def formed_parameters 31 | @formed_parameters ||= {}.tap do |param| 32 | param[jsonapi_main_key.to_sym] = jsonapi_main_body 33 | end 34 | end 35 | 36 | def jsonapi_main_key 37 | @jsonapi_unsafe_hash.dig(:data, :type)&.singularize || '' 38 | end 39 | 40 | def jsonapi_main_body 41 | jsonapi_unsafe_params.tap do |param| 42 | jsonapi_relationships.each do |relationship_key, relationship_value| 43 | param = handle_relationships(param, relationship_key, relationship_value) 44 | end 45 | end 46 | ensure 47 | reset_stack_level 48 | end 49 | 50 | def jsonapi_unsafe_params 51 | @jsonapi_unsafe_params ||= (@jsonapi_unsafe_hash.dig(:data, :attributes) || {}).tap do |param| 52 | id = @jsonapi_unsafe_hash.dig(:data, :id) 53 | 54 | param[:id] = id if id.present? 55 | end 56 | end 57 | 58 | def jsonapi_relationships 59 | @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || [] 60 | end 61 | 62 | def jsonapi_included 63 | @jsonapi_included ||= @jsonapi_unsafe_hash[:included] || [] 64 | end 65 | 66 | def handle_relationships(param, relationship_key, relationship_value) 67 | increment_stack_level! 68 | 69 | relationship_value = relationship_value[:data] 70 | handler_args = [relationship_key, relationship_value, jsonapi_included] 71 | handler = if Handlers.resource_handlers.key?(relationship_key) 72 | Handlers.handlers[Handlers.resource_handlers[relationship_key]] 73 | else 74 | case relationship_value 75 | when Array 76 | Handlers.handlers[:to_many] 77 | when Hash 78 | Handlers.handlers[:to_one] 79 | when nil 80 | Handlers.handlers[:nil] 81 | else 82 | raise NotImplementedError.new('relationship resource linkage has to be a type of Array, Hash or nil') 83 | end 84 | end 85 | 86 | key, val = handler.call(*handler_args) 87 | 88 | param[key] = handle_nested_relationships(val) 89 | 90 | param 91 | ensure 92 | decrement_stack_level 93 | end 94 | 95 | def handle_nested_relationships(val) 96 | # We can only consider Hash relationships (which imply to-one relationship) and Array relationships (which imply to-many). 97 | # Each type has a different handling method, though in both cases we end up passing the nested relationship recursively to handle_relationship 98 | # (and yes, this may go on indefinitely, basically we're going by the relationship levels, deeper and deeper) 99 | case val 100 | when Array 101 | relationships_with_nested_relationships = val.select { |rel| rel.is_a?(Hash) && rel.dig(:relationships) } 102 | relationships_with_nested_relationships.each do |relationship_with_nested_relationship| 103 | relationship_with_nested_relationship.delete(:relationships).each do |rel_key, rel_val| 104 | relationship_with_nested_relationship = handle_relationships(relationship_with_nested_relationship, rel_key, rel_val) 105 | end 106 | end 107 | when Hash 108 | if val.key?(:relationships) 109 | val.delete(:relationships).each do |rel_key, rel_val| 110 | val = handle_relationships(val, rel_key, rel_val) 111 | end 112 | end 113 | end 114 | 115 | val 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/jsonapi_parameters/version.rb: -------------------------------------------------------------------------------- 1 | module JsonApi 2 | module Parameters 3 | VERSION = '2.3.0'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/app/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/app/app/controllers/authors_camel_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthorsCamelController < ApplicationController 2 | def create 3 | author = Author.new(author_params) 4 | 5 | if author.save 6 | render json: AuthorSerializer.new(author).serializable_hash 7 | else 8 | head 500 9 | end 10 | end 11 | 12 | private 13 | 14 | def author_params 15 | params.from_jsonapi(:camel).require(:author).permit( 16 | :name, posts_attributes: [:title, :body, :category_name] 17 | ) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/app/app/controllers/authors_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthorsController < ApplicationController 2 | def create 3 | author = Author.new(author_params) 4 | 5 | if author.save 6 | render json: AuthorSerializer.new(author).serializable_hash 7 | else 8 | head 500 9 | end 10 | end 11 | 12 | def update 13 | author = Author.find(params[:id]) 14 | 15 | if author.update(author_params) 16 | render json: AuthorSerializer.new(author).serializable_hash, status: :ok 17 | else 18 | head 500 19 | end 20 | end 21 | 22 | private 23 | 24 | def author_params 25 | params.from_jsonapi.require(:author).permit( 26 | :name, :scissors_id, 27 | posts_attributes: [:title, :body, :category_name], post_ids: [], 28 | scissors_attributes: [:sharp], 29 | ) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/app/app/controllers/authors_dashed_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthorsDashedController < ApplicationController 2 | def create 3 | author = Author.new(author_params) 4 | 5 | if author.save 6 | render json: AuthorSerializer.new(author).serializable_hash 7 | else 8 | head 500 9 | end 10 | end 11 | 12 | private 13 | 14 | def author_params 15 | params.from_jsonapi(:dashed).require(:author).permit( 16 | :name, posts_attributes: [:title, :body, :category_name] 17 | ) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/app/app/controllers/authors_deprecated_to_jsonapi_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthorsDeprecatedToJsonapiController < ApplicationController 2 | def create 3 | author = Author.new(author_params) 4 | 5 | if author.save 6 | render json: AuthorSerializer.new(author).serializable_hash 7 | else 8 | head 500 9 | end 10 | end 11 | 12 | private 13 | 14 | def author_params 15 | params.to_jsonapi.require(:author).permit( 16 | :name, posts_attributes: [:title, :body, :category_name] 17 | ) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/app/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualitypl/jsonapi_parameters/8aac7fbd52a98d55a875c97e8abfaf4a02dc86ae/spec/app/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/app/app/controllers/pong_controller.rb: -------------------------------------------------------------------------------- 1 | class PongController < ApplicationController 2 | def pong 3 | head 200 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/app/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /spec/app/app/controllers/stack_tester_controller.rb: -------------------------------------------------------------------------------- 1 | class StackTesterController < ApplicationController 2 | def stack_default 3 | params.from_jsonapi # Try parsing! 4 | 5 | head 200 6 | end 7 | 8 | def stack_custom_limit 9 | parse_params_custom 10 | 11 | head 200 12 | end 13 | 14 | def short_stack_custom_limit 15 | params.from_jsonapi(custom_stack_limit: 4) 16 | 17 | head 200 18 | end 19 | 20 | private 21 | 22 | def parse_params_custom 23 | params.stack_limit = 4 24 | 25 | params.from_jsonapi 26 | ensure 27 | params.reset_stack_limit 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/app/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/app/app/models/author.rb: -------------------------------------------------------------------------------- 1 | class Author < ApplicationRecord 2 | has_many :posts 3 | has_one :scissors 4 | 5 | accepts_nested_attributes_for :posts 6 | accepts_nested_attributes_for :scissors 7 | end 8 | -------------------------------------------------------------------------------- /spec/app/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualitypl/jsonapi_parameters/8aac7fbd52a98d55a875c97e8abfaf4a02dc86ae/spec/app/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/app/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ApplicationRecord 2 | belongs_to :author 3 | 4 | accepts_nested_attributes_for :author 5 | end 6 | -------------------------------------------------------------------------------- /spec/app/app/models/scissors.rb: -------------------------------------------------------------------------------- 1 | class Scissors < ApplicationRecord 2 | belongs_to :author 3 | end 4 | -------------------------------------------------------------------------------- /spec/app/app/serializers/author_serializer.rb: -------------------------------------------------------------------------------- 1 | class AuthorSerializer 2 | include FastJsonapi::ObjectSerializer 3 | attributes :name 4 | 5 | has_many :posts 6 | has_one :scissors 7 | end 8 | -------------------------------------------------------------------------------- /spec/app/app/serializers/post_serializer.rb: -------------------------------------------------------------------------------- 1 | class PostSerializer 2 | include FastJsonapi::ObjectSerializer 3 | attributes :title, :body 4 | 5 | belongs_to :author 6 | end 7 | -------------------------------------------------------------------------------- /spec/app/app/serializers/scissors_serializer.rb: -------------------------------------------------------------------------------- 1 | class PostSerializer 2 | include FastJsonapi::ObjectSerializer 3 | attributes :sharp 4 | 5 | belongs_to :author 6 | end 7 | -------------------------------------------------------------------------------- /spec/app/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/app/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/app/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/app/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /spec/app/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /spec/app/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /spec/app/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "active_model/railtie" 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | 8 | Bundler.require(*Rails.groups) 9 | require "jsonapi_parameters" 10 | 11 | module App 12 | class Application < Rails::Application 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration can go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded after loading 16 | # the framework and any gems in your application. 17 | config.paths.add Rails.root.join('app', 'serializers').to_s, eager_load: true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/app/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /spec/app/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: app_production 11 | -------------------------------------------------------------------------------- /spec/app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: ":memory:" 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | require 'fast_jsonapi' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /spec/app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Store uploaded files on the local file system (see config/storage.yml for options) 16 | config.active_storage.service = :local 17 | 18 | # Don't care if the mailer can't send. 19 | config.action_mailer.raise_delivery_errors = false 20 | 21 | # Print deprecation notices to the Rails logger. 22 | config.active_support.deprecation = :log 23 | 24 | # Raise an error on page load if there are pending migrations. 25 | config.active_record.migration_error = :page_load 26 | 27 | # Highlight code that triggered database queries in logs. 28 | config.active_record.verbose_query_logs = true 29 | 30 | # Raises error for missing translations 31 | # config.action_view.raise_on_missing_translations = true 32 | 33 | # Use an evented file watcher to asynchronously detect changes in source code, 34 | # routes, locales, etc. This feature depends on the listen gem. 35 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 36 | end 37 | -------------------------------------------------------------------------------- /spec/app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 14 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 15 | # config.require_master_key = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 22 | # config.action_controller.asset_host = 'http://assets.example.com' 23 | 24 | # Specifies the header that your server uses for sending files. 25 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 26 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 27 | 28 | # Store uploaded files on the local file system (see config/storage.yml for options) 29 | config.active_storage.service = :local 30 | 31 | # Mount Action Cable outside main process or domain 32 | # config.action_cable.mount_path = nil 33 | # config.action_cable.url = 'wss://example.com/cable' 34 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 35 | 36 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 37 | # config.force_ssl = true 38 | 39 | # Use the lowest log level to ensure availability of diagnostic information 40 | # when problems arise. 41 | config.log_level = :debug 42 | 43 | # Prepend all log lines with the following tags. 44 | config.log_tags = [ :request_id ] 45 | 46 | # Use a different cache store in production. 47 | # config.cache_store = :mem_cache_store 48 | 49 | # Use a real queuing backend for Active Job (and separate queues per environment) 50 | # config.active_job.queue_adapter = :resque 51 | # config.active_job.queue_name_prefix = "app_#{Rails.env}" 52 | 53 | config.action_mailer.perform_caching = false 54 | 55 | # Ignore bad email addresses and do not raise email delivery errors. 56 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 57 | # config.action_mailer.raise_delivery_errors = false 58 | 59 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 60 | # the I18n.default_locale when a translation cannot be found). 61 | config.i18n.fallbacks = true 62 | 63 | # Send deprecation notices to registered listeners. 64 | config.active_support.deprecation = :notify 65 | 66 | # Use default logging formatter so that PID and timestamp are not suppressed. 67 | config.log_formatter = ::Logger::Formatter.new 68 | 69 | # Use a different logger for distributed setups. 70 | # require 'syslog/logger' 71 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 72 | 73 | if ENV["RAILS_LOG_TO_STDOUT"].present? 74 | logger = ActiveSupport::Logger.new(STDOUT) 75 | logger.formatter = config.log_formatter 76 | config.logger = ActiveSupport::TaggedLogging.new(logger) 77 | end 78 | 79 | # Do not dump schema after migrations. 80 | config.active_record.dump_schema_after_migration = false 81 | end 82 | -------------------------------------------------------------------------------- /spec/app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Show full error reports and disable caching. 16 | config.consider_all_requests_local = true 17 | 18 | # Raise exceptions instead of rendering exception templates. 19 | config.action_dispatch.show_exceptions = true 20 | 21 | # Disable request forgery protection in test environment. 22 | config.action_controller.allow_forgery_protection = false 23 | 24 | # Tell Action Mailer not to deliver emails to the real world. 25 | # The :test delivery method accumulates sent emails in the 26 | # ActionMailer::Base.deliveries array. 27 | config.action_mailer.delivery_method = :test 28 | 29 | # Print deprecation notices to the stderr. 30 | config.active_support.deprecation = :stderr 31 | 32 | # Raises error for missing translations 33 | # config.action_view.raise_on_missing_translations = true 34 | end 35 | -------------------------------------------------------------------------------- /spec/app/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /spec/app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/app/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /spec/app/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /spec/app/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/app/config/initializers/jsonapi_parameters.rb: -------------------------------------------------------------------------------- 1 | require 'jsonapi_parameters' 2 | 3 | JsonApi::Parameters::Handlers.add_handler(:scissors_handler, ->(k, v, incl) { 4 | if v.nil? 5 | return ["#{k}_id", v] 6 | end 7 | 8 | _, value = JsonApi::Parameters::Handlers.handlers[:to_one].call(k, v, incl) 9 | 10 | ['scissors_attributes', value] 11 | }) 12 | JsonApi::Parameters::Handlers.set_resource_handler(:scissors, :scissors_handler) 13 | -------------------------------------------------------------------------------- /spec/app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /spec/app/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. 30 | # 31 | # preload_app! 32 | 33 | # Allow puma to be restarted by `rails restart` command. 34 | plugin :tmp_restart 35 | -------------------------------------------------------------------------------- /spec/app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get 'pong' => 'pong#pong' 3 | 4 | post :stack_default, controller: 'stack_tester' 5 | post :stack_custom_limit, controller: 'stack_tester' 6 | post :short_stack_custom_limit, controller: 'stack_tester' 7 | 8 | resources :authors, only: [:create, :update] 9 | resources :authors_deprecated_to_jsonapi, only: [:create] 10 | resources :authors_camel, only: [:create] 11 | resources :authors_dashed, only: [:create] 12 | end 13 | -------------------------------------------------------------------------------- /spec/app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_key_base: e71bab4498bc7660b375b0eaa7d77f47342692a2370a37d75a47aa729223dbd85bb05e00254e8cc9c2350049aae3d3168db3beafce1c26fc4085d749006b5976 3 | 4 | test: 5 | secret_key_base: 98a18426d3c4e28685665e7e5c11fb565e337af627ece0e6633b3e7eb950457a407e5c7bdb3ada15e38f9a7286be5eaff9ce0f76d963eb3fa0127c07 6 | 7 | production: 8 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 9 | -------------------------------------------------------------------------------- /spec/app/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w[ 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ].each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /spec/app/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /spec/app/db/development.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualitypl/jsonapi_parameters/8aac7fbd52a98d55a875c97e8abfaf4a02dc86ae/spec/app/db/development.sqlite3 -------------------------------------------------------------------------------- /spec/app/db/migrate/20190115235549_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :posts do |t| 4 | t.string :title 5 | t.string :body 6 | t.references :author, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/app/db/migrate/20190115235603_create_authors.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthors < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :authors do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/app/db/migrate/20190423142019_add_category_name_to_posts.rb: -------------------------------------------------------------------------------- 1 | class AddCategoryNameToPosts < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :posts, :category_name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/app/db/migrate/20191206150129_create_scissors.rb: -------------------------------------------------------------------------------- 1 | class CreateScissors < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :scissors do |t| 4 | t.boolean :sharp 5 | t.references :author, null: false, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2019_12_06_150129) do 14 | 15 | create_table "authors", force: :cascade do |t| 16 | t.string "name" 17 | t.datetime "created_at", null: false 18 | t.datetime "updated_at", null: false 19 | end 20 | 21 | create_table "posts", force: :cascade do |t| 22 | t.string "title" 23 | t.string "body" 24 | t.integer "author_id" 25 | t.datetime "created_at", null: false 26 | t.datetime "updated_at", null: false 27 | t.string "category_name" 28 | t.index ["author_id"], name: "index_posts_on_author_id" 29 | end 30 | 31 | create_table "scissors", force: :cascade do |t| 32 | t.boolean "sharp" 33 | t.integer "author_id", null: false 34 | t.datetime "created_at", precision: 6, null: false 35 | t.datetime "updated_at", precision: 6, null: false 36 | t.index ["author_id"], name: "index_scissors_on_author_id" 37 | end 38 | 39 | add_foreign_key "posts", "authors" 40 | add_foreign_key "scissors", "authors" 41 | end 42 | -------------------------------------------------------------------------------- /spec/app/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualitypl/jsonapi_parameters/8aac7fbd52a98d55a875c97e8abfaf4a02dc86ae/spec/app/lib/assets/.keep -------------------------------------------------------------------------------- /spec/app/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ../../rails_helper.rb -------------------------------------------------------------------------------- /spec/app/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ../../spec_helper.rb -------------------------------------------------------------------------------- /spec/app/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visualitypl/jsonapi_parameters/8aac7fbd52a98d55a875c97e8abfaf4a02dc86ae/spec/app/storage/.keep -------------------------------------------------------------------------------- /spec/core_ext/action_controller/parameters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ActionController::Parameters do 4 | it 'responds to #from_jsonapi' do 5 | expect(described_class.new.respond_to?(:from_jsonapi)).to eq(true) 6 | end 7 | 8 | describe '#from_jsonapi' do 9 | it 'returns new object' do 10 | params = described_class.new test: 1 11 | 12 | expect(params.from_jsonapi.hash).not_to eq(params.hash) 13 | end 14 | 15 | it 'is memoized' do 16 | params = described_class.new test: 1 17 | 18 | expect(params.from_jsonapi.hash).to eq(params.from_jsonapi.hash) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/factories/authors_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :author, class: Author do 3 | name { Faker::Name.name } 4 | association :posts, factory: :post 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/posts_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :post, class: Post do 3 | title { Faker::Book.title } 4 | body { Faker::GameOfThrones.quote } 5 | association :author, factory: :author 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/integration/authors_camel_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe AuthorsCamelController, type: :controller do 4 | describe 'create' do 5 | it 'creates an author' do 6 | params = { 7 | data: { 8 | type: 'authors', 9 | attributes: { 10 | name: 'John Doe' 11 | } 12 | } 13 | } 14 | 15 | post_with_rails_fix :create, params: params 16 | 17 | expect(jsonapi_response).to eq( 18 | data: { 19 | id: '1', 20 | type: 'author', 21 | attributes: { 22 | name: 'John Doe' 23 | }, 24 | relationships: { 25 | scissors: { data: nil }, 26 | posts: { 27 | data: [] 28 | } 29 | } 30 | } 31 | ) 32 | end 33 | 34 | it 'creates an author with posts' do 35 | params = { 36 | data: { 37 | type: 'authors', 38 | attributes: { 39 | name: 'John Doe' 40 | }, 41 | relationships: { 42 | posts: { 43 | data: [ 44 | { 45 | id: '123', 46 | type: 'post' 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | included: [ 53 | { 54 | type: 'post', 55 | id: '123', 56 | attributes: { 57 | title: 'Some title', 58 | body: 'Some body that I used to love', 59 | categoryName: 'Some category' 60 | } 61 | } 62 | ] 63 | } 64 | 65 | post_with_rails_fix :create, params: params 66 | 67 | expect(jsonapi_response[:data]).to eq( 68 | id: '1', 69 | type: 'author', 70 | attributes: { 71 | name: 'John Doe' 72 | }, 73 | relationships: { 74 | scissors: { data: nil }, 75 | posts: { 76 | data: [ 77 | { 78 | id: '1', 79 | type: 'post' 80 | } 81 | ] 82 | } 83 | } 84 | ) 85 | expect(Post.find(1).category_name).to eq('Some category') 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/integration/authors_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe AuthorsController, type: :controller do 4 | describe 'create' do 5 | it 'creates an author' do 6 | params = { 7 | data: { 8 | type: 'authors', 9 | attributes: { 10 | name: 'John Doe' 11 | } 12 | } 13 | } 14 | 15 | post_with_rails_fix :create, params: params 16 | 17 | expect(jsonapi_response).to eq( 18 | data: { 19 | id: '1', 20 | type: 'author', 21 | attributes: { 22 | name: 'John Doe' 23 | }, 24 | relationships: { 25 | posts: { 26 | data: [] 27 | }, 28 | scissors: { data: nil } 29 | } 30 | } 31 | ) 32 | end 33 | 34 | it 'creates an author with posts' do 35 | params = { 36 | data: { 37 | type: 'authors', 38 | attributes: { 39 | name: 'John Doe' 40 | }, 41 | relationships: { 42 | posts: { 43 | data: [ 44 | { 45 | id: '123', 46 | type: 'post' 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | included: [ 53 | { 54 | type: 'post', 55 | id: '123', 56 | attributes: { 57 | title: 'Some title', 58 | body: 'Some body that I used to love', 59 | category_name: 'Some category' 60 | } 61 | } 62 | ] 63 | } 64 | 65 | post_with_rails_fix :create, params: params 66 | 67 | expect(jsonapi_response[:data]).to eq( 68 | id: '1', 69 | type: 'author', 70 | attributes: { 71 | name: 'John Doe' 72 | }, 73 | relationships: { 74 | posts: { 75 | data: [ 76 | { 77 | id: '1', 78 | type: 'post' 79 | } 80 | ] 81 | }, 82 | scissors: { data: nil } 83 | } 84 | ) 85 | expect(Post.find(1).category_name).to eq('Some category') 86 | end 87 | 88 | it 'creates an author with a post, and then removes all his related posts' do 89 | params = { 90 | data: { 91 | type: 'authors', 92 | attributes: { 93 | name: 'John Doe' 94 | }, 95 | relationships: { 96 | posts: { 97 | data: [ 98 | { 99 | id: '123', 100 | type: 'post' 101 | } 102 | ] 103 | } 104 | } 105 | }, 106 | included: [ 107 | { 108 | type: 'post', 109 | id: '123', 110 | attributes: { 111 | title: 'Some title', 112 | body: 'Some body that I used to love', 113 | category_name: 'Some category' 114 | } 115 | } 116 | ] 117 | } 118 | 119 | post_with_rails_fix :create, params: params 120 | 121 | author_id = jsonapi_response[:data][:id] 122 | params = { 123 | id: author_id, 124 | data: { 125 | type: 'authors', 126 | id: author_id, 127 | relationships: { 128 | posts: { 129 | data: [] 130 | } 131 | } 132 | } 133 | } 134 | 135 | patch_with_rails_fix :update, params: params, as: :json 136 | 137 | expect(jsonapi_response[:data][:relationships][:posts][:data]).to eq([]) 138 | end 139 | 140 | it 'creates an author with a pair of sharp scissors' do 141 | params = { 142 | data: { 143 | type: 'authors', 144 | attributes: { 145 | name: 'John Doe' 146 | }, 147 | relationships: { 148 | scissors: { 149 | data: { 150 | id: '123', 151 | type: 'scissors' 152 | } 153 | } 154 | } 155 | }, 156 | included: [ 157 | { 158 | type: 'scissors', 159 | id: '123', 160 | attributes: { 161 | sharp: true 162 | } 163 | } 164 | ] 165 | } 166 | 167 | post_with_rails_fix :create, params: params 168 | 169 | expect(jsonapi_response[:data]).to eq( 170 | id: '1', 171 | type: 'author', 172 | attributes: { 173 | name: 'John Doe' 174 | }, 175 | relationships: { 176 | posts: { data: [] }, 177 | scissors: { 178 | data: { 179 | id: '1', 180 | type: 'scissors' 181 | } 182 | } 183 | } 184 | ) 185 | expect(Scissors.find(1).sharp).to eq(true) 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/integration/authors_dashed_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe AuthorsDashedController, type: :controller do 4 | describe 'create' do 5 | it 'creates an author' do 6 | params = { 7 | data: { 8 | type: 'authors', 9 | attributes: { 10 | name: 'John Doe' 11 | } 12 | } 13 | } 14 | 15 | post_with_rails_fix :create, params: params 16 | 17 | expect(jsonapi_response).to eq( 18 | data: { 19 | id: '1', 20 | type: 'author', 21 | attributes: { 22 | name: 'John Doe' 23 | }, 24 | relationships: { 25 | scissors: { data: nil }, 26 | posts: { 27 | data: [] 28 | } 29 | } 30 | } 31 | ) 32 | end 33 | 34 | it 'creates an author with posts' do 35 | params = { 36 | data: { 37 | type: 'authors', 38 | attributes: { 39 | name: 'John Doe' 40 | }, 41 | relationships: { 42 | posts: { 43 | data: [ 44 | { 45 | id: '123', 46 | type: 'post' 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | included: [ 53 | { 54 | type: 'post', 55 | id: '123', 56 | attributes: { 57 | title: 'Some title', 58 | body: 'Some body that I used to love', 59 | 'category-name': 'Some category' 60 | } 61 | } 62 | ] 63 | } 64 | 65 | post_with_rails_fix :create, params: params 66 | 67 | expect(jsonapi_response[:data]).to eq( 68 | id: '1', 69 | type: 'author', 70 | attributes: { 71 | name: 'John Doe' 72 | }, 73 | relationships: { 74 | scissors: { data: nil }, 75 | posts: { 76 | data: [ 77 | { 78 | id: '1', 79 | type: 'post' 80 | } 81 | ] 82 | } 83 | } 84 | ) 85 | expect(Post.find(1).category_name).to eq('Some category') 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/integration/authors_deprecated_to_jsonapi_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe AuthorsDeprecatedToJsonapiController, type: :controller do 4 | describe 'create' do 5 | it 'creates an author' do 6 | params = { 7 | data: { 8 | type: 'authors', 9 | attributes: { 10 | name: 'John Doe' 11 | } 12 | } 13 | } 14 | 15 | post_with_rails_fix :create, params: params 16 | 17 | expect(jsonapi_response).to eq( 18 | data: { 19 | id: '1', 20 | type: 'author', 21 | attributes: { 22 | name: 'John Doe' 23 | }, 24 | relationships: { 25 | scissors: { data: nil }, 26 | posts: { 27 | data: [] 28 | } 29 | } 30 | } 31 | ) 32 | end 33 | 34 | it 'creates an author with posts' do 35 | params = { 36 | data: { 37 | type: 'authors', 38 | attributes: { 39 | name: 'John Doe' 40 | }, 41 | relationships: { 42 | posts: { 43 | data: [ 44 | { 45 | id: '123', 46 | type: 'post' 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | included: [ 53 | { 54 | type: 'post', 55 | id: '123', 56 | attributes: { 57 | title: 'Some title', 58 | body: 'Some body that I used to love', 59 | category_name: 'Some category' 60 | } 61 | } 62 | ] 63 | } 64 | 65 | post_with_rails_fix :create, params: params 66 | 67 | expect(jsonapi_response[:data]).to eq( 68 | id: '1', 69 | type: 'author', 70 | attributes: { 71 | name: 'John Doe' 72 | }, 73 | relationships: { 74 | scissors: { data: nil }, 75 | posts: { 76 | data: [ 77 | { 78 | id: '1', 79 | type: 'post' 80 | } 81 | ] 82 | } 83 | } 84 | ) 85 | expect(Post.find(1).category_name).to eq('Some category') 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/integration/pong_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe PongController, type: :controller do 4 | it 'returns pong' do 5 | get :pong 6 | 7 | expect(response.code).to eq('200') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/integration/stack_tester_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe 'Stack Tester requests', type: :request do 4 | describe 'stack_default' do 5 | it 'fails when exceeding stack limit' do 6 | payload = select_input_by_name('POST create payloads', 'triple-nested payload') 7 | 8 | payload[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 9 | 10 | post_with_rails_fix stack_default_path, params: payload 11 | 12 | expect(response).to have_http_status(500) 13 | end 14 | 15 | it 'passes when stack limit is not exceeded' do 16 | payload = select_input_by_name('POST create payloads', 'triple-nested payload') 17 | 18 | post stack_default_path, params: payload 19 | 20 | expect(response).to have_http_status(200) 21 | end 22 | end 23 | 24 | describe 'stack custom' do 25 | it 'passes when stack limit is above default' do 26 | payload = select_input_by_name('POST create payloads', 'triple-nested payload') 27 | 28 | payload[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 29 | 30 | post_with_rails_fix stack_custom_limit_path, params: payload 31 | 32 | expect(response).to have_http_status(200) 33 | end 34 | 35 | it 'passes when stack limit is above default using short notation' do 36 | payload = select_input_by_name('POST create payloads', 'triple-nested payload') 37 | 38 | payload[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 39 | 40 | post_with_rails_fix short_stack_custom_limit_path, params: payload 41 | 42 | expect(response).to have_http_status(200) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/lib/jsonapi_parameters/handlers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | #### 4 | # Sample klass 5 | #### 6 | class Translator 7 | include JsonApi::Parameters 8 | end 9 | 10 | describe JsonApi::Parameters::Handlers do 11 | before(:each) do 12 | described_class.reset_handlers 13 | end 14 | 15 | describe 'reset_handlers!' do 16 | it 'sets all the handlers to the default handlers' do 17 | handler = described_class.add_handler(:test, -> { puts 'Hello!' }) 18 | 19 | expect(described_class::DEFAULT_HANDLER_SET).not_to include(handler) 20 | 21 | described_class.reset_handlers 22 | 23 | expect(described_class.handlers).not_to include(handler) 24 | expect(described_class.handlers).to eq(described_class::DEFAULT_HANDLER_SET) 25 | end 26 | 27 | it 'empties the resource handlers' do 28 | described_class.add_handler(:test, -> { puts 'Hello!' }) 29 | described_class.set_resource_handler(:test, :test) 30 | 31 | described_class.reset_handlers 32 | 33 | expect(described_class.resource_handlers).to eq({}) 34 | end 35 | end 36 | 37 | describe 'custom handlers' do 38 | it 'allows to add a custom handler' do 39 | handler = described_class.add_handler(:test, -> { puts 'Hello!' }) 40 | 41 | expect(handler).to be_an_instance_of(Proc) 42 | expect(described_class.handlers.values).to include(handler) 43 | end 44 | end 45 | 46 | describe 'custom resource handler' do 47 | it 'allows to set a custom resource handler when the handler is present' do 48 | expect(described_class.add_handler(:test, -> { puts 'Hello!' })).to be_an_instance_of(Proc) 49 | expect { described_class.set_resource_handler(:test, :test) }.not_to raise_error 50 | end 51 | 52 | it 'disallows setting a custom resource handler when the handler is not present' do 53 | expect { described_class.set_resource_handler(:test2, :test2) }.to raise_error(NotImplementedError) 54 | end 55 | end 56 | end 57 | 58 | describe Translator do 59 | before(:each) do 60 | JsonApi::Parameters::Handlers.reset_handlers 61 | end 62 | 63 | context 'edge case of singular resource with a plural name (scissors)' do 64 | it 'properly translates with custom resource handler' do 65 | translator = described_class.new 66 | params = { 67 | data: { 68 | type: 'users', 69 | id: '666', 70 | attributes: { 71 | first_name: 'John' 72 | }, 73 | relationships: { 74 | scissors: { data: nil } 75 | } 76 | } 77 | } 78 | 79 | scissors_handler = ->(relationship_key, _, _) { ["#{relationship_key}_id".to_sym, nil] } 80 | 81 | JsonApi::Parameters::Handlers.add_handler(:handle_plural_nil_as_belongs_to_nil, scissors_handler) 82 | JsonApi::Parameters::Handlers.set_resource_handler(:scissors, :handle_plural_nil_as_belongs_to_nil) 83 | 84 | result = translator.jsonapify(params) 85 | 86 | expect(result).to eq( 87 | user: { first_name: 'John', id: '666', scissors_id: nil } 88 | ) 89 | end 90 | 91 | it 'would fail with NotImplementedError if no customizations present' do 92 | translator = described_class.new 93 | params = { 94 | data: { 95 | type: 'users', 96 | id: '666', 97 | attributes: { 98 | first_name: 'John' 99 | }, 100 | relationships: { 101 | scissors: { data: nil } 102 | } 103 | } 104 | } 105 | 106 | expect { translator.jsonapify(params) }.to raise_error(NotImplementedError) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/lib/jsonapi_parameters/mime_type_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mime::Type do # rubocop:disable RSpec/FilePath 4 | result = described_class.lookup_by_extension(:json).instance_variable_get(:@synonyms) 5 | 6 | it 'includes JSON:API application/vnd.api+json mime type registered as json' do 7 | expect(result.include?('application/vnd.api+json')).to eq(true) 8 | end 9 | 10 | it 'includes standard application/json mime type registered as json' do 11 | expect(result.include?('application/json')).to eq(true) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/lib/jsonapi_parameters/stack_limit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | #### 4 | # Sample klass 5 | #### 6 | class Translator 7 | include JsonApi::Parameters 8 | end 9 | 10 | describe Translator do # rubocop:disable RSpec/FilePath 11 | context 'with default stack limit' do 12 | it 'works properly if the stack level is the same as the limit' do 13 | input = select_input_by_name('POST create payloads', 'triple-nested payload') 14 | 15 | translator = described_class.new 16 | 17 | expect { translator.jsonapify(input) }.not_to raise_error(JsonApi::Parameters::StackLevelTooDeepError) 18 | expect { translator.jsonapify(input) }.not_to raise_error # To ensure this is passing 19 | end 20 | 21 | it 'raises an error if the stack level is above the limit' do 22 | input = select_input_by_name('POST create payloads', 'triple-nested payload') 23 | 24 | input[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 25 | 26 | translator = described_class.new 27 | 28 | expect { translator.jsonapify(input) }.to raise_error(JsonApi::Parameters::StackLevelTooDeepError) 29 | end 30 | end 31 | 32 | context 'stack limit' do 33 | it 'can be overwritten' do 34 | input = select_input_by_name('POST create payloads', 'triple-nested payload') 35 | input[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 36 | translator = described_class.new 37 | translator.stack_limit = 4 38 | 39 | expect { translator.jsonapify(input) }.not_to raise_error(JsonApi::Parameters::StackLevelTooDeepError) 40 | expect { translator.jsonapify(input) }.not_to raise_error # To ensure this is passing 41 | end 42 | 43 | it 'can be overwritten using short notation' do 44 | input = select_input_by_name('POST create payloads', 'triple-nested payload') 45 | input[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 46 | translator = described_class.new 47 | 48 | expect { translator.jsonapify(input, custom_stack_limit: 4) }.not_to raise_error(JsonApi::Parameters::StackLevelTooDeepError) 49 | expect { translator.jsonapify(input, custom_stack_limit: 4) }.not_to raise_error # To ensure this is passing 50 | end 51 | 52 | it 'can be reset' do 53 | input = select_input_by_name('POST create payloads', 'triple-nested payload') 54 | input[:included] << { id: 3, type: 'entity', relationships: { subentity: { data: { type: 'entity', id: 4 } } } } 55 | translator = described_class.new 56 | translator.stack_limit = 4 57 | 58 | translator.reset_stack_limit 59 | 60 | expect { translator.jsonapify(input) }.to raise_error(JsonApi::Parameters::StackLevelTooDeepError) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/lib/jsonapi_parameters/translator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | #### 4 | # Sample klass 5 | #### 6 | class Translator 7 | include JsonApi::Parameters 8 | end 9 | 10 | describe Translator do 11 | context 'without enforced underscore translation' do 12 | describe 'plain hash parameters' do 13 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 14 | describe case_type_name do 15 | kases.each do |kase| 16 | kase.each do |case_name, case_data| 17 | it "matches #{case_name}" do 18 | input, predicted_output = case_data 19 | 20 | translated_input = described_class.new.jsonapify(input) 21 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | 29 | describe 'parameters instantiated as ActionController::Parameters' do 30 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 31 | describe case_type_name do 32 | kases.each do |kase| 33 | kase.each do |case_name, case_data| 34 | it "matches #{case_name}" do 35 | input, predicted_output = case_data 36 | input = ActionController::Parameters.new(input) 37 | 38 | translated_input = described_class.new.jsonapify(input) 39 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | 47 | describe 'camelCased parameters' do 48 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 49 | describe case_type_name do 50 | kases.each do |kase| 51 | kase.each do |case_name, case_data| 52 | it "matches #{case_name}" do 53 | input, predicted_output = case_data 54 | input[:data][:type] = input[:data][:type].camelize 55 | input = input.deep_transform_keys { |key| key.to_s.camelize.to_sym } 56 | input = ActionController::Parameters.new(input) 57 | 58 | translated_input = described_class.new.jsonapify(input, naming_convention: :camel) 59 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | 67 | describe 'dasherized parameters' do 68 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 69 | describe case_type_name do 70 | kases.each do |kase| 71 | kase.each do |case_name, case_data| 72 | it "matches #{case_name}" do 73 | input, predicted_output = case_data 74 | input[:data][:type] = input[:data][:type].dasherize 75 | input = input.deep_transform_keys { |key| key.to_s.dasherize.to_sym } 76 | input = ActionController::Parameters.new(input) 77 | 78 | translated_input = described_class.new.jsonapify(input, naming_convention: :dashed) 79 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | 88 | context 'with enforced underscore translation' do 89 | describe 'plain hash parameters' do 90 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 91 | describe case_type_name do 92 | kases.each do |kase| 93 | kase.each do |case_name, case_data| 94 | it "matches #{case_name}" do 95 | JsonApi::Parameters.ensure_underscore_translation = true 96 | 97 | input, predicted_output = case_data 98 | 99 | translated_input = described_class.new.jsonapify(input) 100 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 101 | 102 | JsonApi::Parameters.ensure_underscore_translation = false 103 | end 104 | end 105 | end 106 | end 107 | end 108 | end 109 | 110 | describe 'parameters instantiated as ActionController::Parameters' do 111 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 112 | describe case_type_name do 113 | kases.each do |kase| 114 | kase.each do |case_name, case_data| 115 | it "matches #{case_name}" do 116 | JsonApi::Parameters.ensure_underscore_translation = true 117 | 118 | input, predicted_output = case_data 119 | input = ActionController::Parameters.new(input) 120 | 121 | translated_input = described_class.new.jsonapify(input) 122 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 123 | 124 | JsonApi::Parameters.ensure_underscore_translation = false 125 | end 126 | end 127 | end 128 | end 129 | end 130 | end 131 | 132 | describe 'camelCased parameters' do 133 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 134 | describe case_type_name do 135 | kases.each do |kase| 136 | kase.each do |case_name, case_data| 137 | it "matches #{case_name}" do 138 | JsonApi::Parameters.ensure_underscore_translation = true 139 | 140 | input, predicted_output = case_data 141 | input[:data][:type] = input[:data][:type].camelize 142 | input = input.deep_transform_keys { |key| key.to_s.camelize.to_sym } 143 | input = ActionController::Parameters.new(input) 144 | 145 | translated_input = described_class.new.jsonapify(input) 146 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 147 | 148 | JsonApi::Parameters.ensure_underscore_translation = false 149 | end 150 | end 151 | end 152 | end 153 | end 154 | end 155 | 156 | describe 'dasherized parameters' do 157 | JsonApi::Parameters::Testing::PAIRS.each do |case_type_name, kases| 158 | describe case_type_name do 159 | kases.each do |kase| 160 | kase.each do |case_name, case_data| 161 | it "matches #{case_name}" do 162 | JsonApi::Parameters.ensure_underscore_translation = true 163 | 164 | input, predicted_output = case_data 165 | input[:data][:type] = input[:data][:type].dasherize 166 | input = input.deep_transform_keys { |key| key.to_s.dasherize.to_sym } 167 | input = ActionController::Parameters.new(input) 168 | 169 | translated_input = described_class.new.jsonapify(input) 170 | expect(HashDiff.diff(translated_input, predicted_output)).to eq([]) 171 | 172 | JsonApi::Parameters.ensure_underscore_translation = false 173 | end 174 | end 175 | end 176 | end 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rails' 3 | require 'rspec/rails' 4 | require 'json' 5 | 6 | load "#{Rails.root}/db/schema.rb" 7 | 8 | def jsonapi_response 9 | JSON.parse(response.body, symbolize_names: true) 10 | end 11 | 12 | # rails changed controller params from positional to keywords in rails 5.1.7 13 | # 5.1.6>= rails >=5.0 consumes both ways. 14 | # https://apidock.com/rails/v5.1.7/ActionController/TestCase/Behavior/process 15 | # others keywords can be: session, body, flash, format, xhr, as. 16 | # We are using `as: json` in specs (rails 5+), 17 | # but rails 4 does not require providing that and consumes only params, session and flash. 18 | # Integration specs does not have session and flash but we don't require them so we will skip them. 19 | # Integration requests can also pass headers, but we don't use it yet. 20 | # https://github.com/rails/rails/blob/v6.0.3.6/actionpack/lib/action_dispatch/testing/integration.rb#L211 21 | # Please use this helper methods and use keyword arguments. 22 | # For get requests you should provide another helper integrating params, etc. 23 | def post_with_rails_fix(url, params: {}, **others) 24 | if Rails::VERSION::MAJOR >= 5 25 | post url, params: params, **others 26 | else 27 | post url, params, **others 28 | end 29 | end 30 | 31 | # Similar helper to post, providing compability to controller/request specs for rails 4 32 | def patch_with_rails_fix(url, params: {}, **others) 33 | if Rails::VERSION::MAJOR >= 5 34 | patch url, params: params, **others 35 | else 36 | patch url, params, **others 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | Bundler.setup 3 | 4 | require 'simplecov' 5 | require 'simplecov-console' 6 | SimpleCov.start do 7 | add_filter 'spec/app/' 8 | end 9 | 10 | ENV['RAILS_ENV'] = 'test' 11 | 12 | require_relative 'app/config/environment' 13 | ActiveRecord::Migrator.migrations_paths = [File.expand_path('app/db/migrate', __dir__)] 14 | 15 | require 'jsonapi_parameters' 16 | require_relative './support/inputs_outputs_pairs' 17 | require 'database_cleaner' 18 | require 'factory_bot' 19 | require 'hashdiff' 20 | 21 | RSpec.configure do |config| 22 | config.include FactoryBot::Syntax::Methods 23 | 24 | config.before(:suite) do 25 | DatabaseCleaner.strategy = :transaction 26 | DatabaseCleaner.clean_with(:truncation) 27 | end 28 | 29 | config.around(:each) do |example| 30 | DatabaseCleaner.cleaning do 31 | example.run 32 | end 33 | end 34 | end 35 | 36 | def select_io_pair_by_name(category, name) 37 | JsonApi::Parameters::Testing::PAIRS[category].find do |example| 38 | example.key?(name) 39 | end[name].deep_dup 40 | end 41 | 42 | def select_input_by_name(category, name) 43 | select_io_pair_by_name(category, name)[0] 44 | end 45 | -------------------------------------------------------------------------------- /spec/support/inputs_outputs_pairs.rb: -------------------------------------------------------------------------------- 1 | module JsonApi::Parameters::Testing 2 | PAIRS = { 3 | 'POST create payloads' => [ 4 | { 'single root, single attribute' => [ 5 | { data: { type: 'tests', attributes: { name: 'test name' } } }, 6 | { test: { name: 'test name' } } 7 | ] }, 8 | { 'single root, client generated id' => [ 9 | { data: { id: 22, type: 'tests', attributes: { name: 'test name' } } }, 10 | { test: { id: 22, name: 'test name' } } 11 | ] }, 12 | { 'single root, multiple attributes' => [ 13 | { data: { type: 'tests', attributes: { name: 'test name', age: 21 } } }, 14 | { test: { name: 'test name', age: 21 } } 15 | ] }, 16 | { 'single root, single relationship, multiple related resources' => [ 17 | { 18 | data: { 19 | type: "contract", 20 | attributes: { 21 | total_amount: "40.0", 22 | start_date: "2018-10-18" 23 | }, 24 | relationships: { 25 | products: { 26 | data: [ 27 | { 28 | id: "4", type: "product" 29 | }, { 30 | id: "5", type: "product" 31 | }, { 32 | id: "6", type: "product" 33 | } 34 | ] 35 | }, 36 | customer: { 37 | data: { 38 | id: "1", type: "customer" 39 | } 40 | } 41 | } 42 | }, 43 | included: [ 44 | { 45 | id: "4", 46 | type: "product", 47 | attributes: { 48 | category: "first_category", 49 | amount: "15.0", 50 | name: "First product", 51 | note: "" 52 | } 53 | }, 54 | { 55 | id: "5", 56 | type: "product", 57 | attributes: { 58 | category: "first_category", 59 | amount: "10.0", 60 | name: "Second Product", 61 | } 62 | }, 63 | { 64 | id: "6", 65 | type: "product", 66 | attributes: { 67 | category: "second_category", 68 | amount: "15.0", 69 | name: "Third Product", 70 | } 71 | } 72 | ] 73 | }, 74 | { 75 | contract: { 76 | start_date: '2018-10-18', 77 | total_amount: '40.0', 78 | customer_id: '1', 79 | products_attributes: [ 80 | { 81 | id: '4', 82 | category: "first_category", 83 | amount: "15.0", 84 | name: "First product", 85 | note: '' 86 | }, 87 | { 88 | id: '5', 89 | category: "first_category", 90 | amount: "10.0", 91 | name: "Second Product" 92 | }, 93 | { 94 | id: '6', 95 | category: "second_category", 96 | amount: "15.0", 97 | name: "Third Product" 98 | } 99 | ] 100 | } 101 | } 102 | ] }, 103 | { 'https://jsonapi.org/format/#crud example' => [ 104 | { 105 | data: { 106 | type: 'photos', 107 | attributes: { 108 | title: 'Ember Hamster', 109 | src: 'http://example.com/images/productivity.png' 110 | }, 111 | relationships: { 112 | photographer: { 113 | data: { 114 | type: 'people', 115 | id: 9 116 | } 117 | } 118 | } 119 | } 120 | }, 121 | { 122 | photo: { 123 | title: 'Ember Hamster', 124 | src: 'http://example.com/images/productivity.png', 125 | photographer_id: 9 126 | } 127 | } 128 | ] }, 129 | { 'https://jsonapi.org/format/#crud example (modified, multiple photographers)' => [ 130 | { 131 | data: { 132 | type: 'photos', 133 | attributes: { 134 | title: 'Ember Hamster', 135 | src: 'http://example.com/images/productivity.png' 136 | }, 137 | relationships: { 138 | photographers: { 139 | data: [ 140 | { 141 | id: 9, 142 | type: 'people' 143 | }, 144 | { 145 | id: 10, 146 | type: 'people' 147 | } 148 | ] 149 | } 150 | } 151 | }, 152 | included: [ 153 | { 154 | type: 'people', 155 | id: 10, 156 | attributes: { 157 | name: 'Some guy' 158 | } 159 | }, 160 | { 161 | type: 'people', 162 | id: 9, 163 | attributes: { 164 | name: 'Some other guy' 165 | } 166 | } 167 | ] 168 | }, 169 | { 170 | photo: { 171 | title: 'Ember Hamster', 172 | src: 'http://example.com/images/productivity.png', 173 | photographers_attributes: [ 174 | { 175 | id: 9, 176 | name: 'Some other guy' 177 | }, 178 | { 179 | id: 10, 180 | name: 'Some guy' 181 | } 182 | ] 183 | } 184 | } 185 | ] }, 186 | { 'relationships without included (issue #13 - https://github.com/visualitypl/jsonapi_parameters/issues/13)' => [ 187 | { 188 | data: { 189 | type: 'movies', 190 | attributes: { 191 | title: 'The Terminator', 192 | released_at: '1984-10-26', 193 | runtime: 107, 194 | content_rating: 'restricted', 195 | storyline: 'A seemingly indestructible android is sent from 2029 to 1984 to assassinate a waitress, whose unborn son will lead humanity in a war against the machines, while a soldier from that war is sent to protect her at all costs.', 196 | budget: 6400000 197 | }, 198 | relationships: { 199 | genres: { 200 | data: [ 201 | { 202 | id: 74, type: 'genres' 203 | } 204 | ] 205 | }, 206 | director: { 207 | data: { 208 | id: 682, type: 'directors' 209 | } 210 | } 211 | } 212 | } 213 | }, 214 | { 215 | movie: { 216 | title: 'The Terminator', 217 | released_at: '1984-10-26', 218 | runtime: 107, 219 | content_rating: 'restricted', 220 | storyline: 'A seemingly indestructible android is sent from 2029 to 1984 to assassinate a waitress, whose unborn son will lead humanity in a war against the machines, while a soldier from that war is sent to protect her at all costs.', 221 | budget: 6400000, 222 | director_id: 682, 223 | genre_ids: [74] 224 | } 225 | } 226 | ] }, 227 | { 'relationships with included director (issue #13 - https://github.com/visualitypl/jsonapi_parameters/issues/13)' => [ 228 | { 229 | data: { 230 | type: 'movies', 231 | attributes: { 232 | title: 'The Terminator', 233 | released_at: '1984-10-26', 234 | runtime: 107, 235 | content_rating: 'restricted', 236 | storyline: 'A seemingly indestructible android is sent from 2029 to 1984 to assassinate a waitress, whose unborn son will lead humanity in a war against the machines, while a soldier from that war is sent to protect her at all costs.', 237 | budget: 6400000 238 | }, 239 | relationships: { 240 | genres: { 241 | data: [ 242 | { 243 | id: 74, type: 'genres' 244 | } 245 | ] 246 | }, 247 | director: { 248 | data: { 249 | id: 682, type: 'directors' 250 | } 251 | } 252 | } 253 | }, 254 | included: [ 255 | { 256 | type: 'directors', 257 | id: 682, 258 | attributes: { 259 | name: 'Some guy' 260 | } 261 | } 262 | ] 263 | }, 264 | { 265 | movie: { 266 | title: 'The Terminator', 267 | released_at: '1984-10-26', 268 | runtime: 107, 269 | content_rating: 'restricted', 270 | storyline: 'A seemingly indestructible android is sent from 2029 to 1984 to assassinate a waitress, whose unborn son will lead humanity in a war against the machines, while a soldier from that war is sent to protect her at all costs.', 271 | budget: 6400000, 272 | director_attributes: { id: 682, name: 'Some guy' }, 273 | genre_ids: [74] 274 | } 275 | } 276 | ] }, 277 | { 'long type name - type casing translation (https://github.com/visualitypl/jsonapi_parameters/pull/31)' => [ 278 | { 279 | data: { 280 | type: 'message_board_threads', 281 | attributes: { 282 | thread_title: 'Introductory Thread' 283 | } 284 | } 285 | }, 286 | { message_board_thread: { thread_title: 'Introductory Thread' } } 287 | ] }, 288 | { 'triple-nested payload' => [ 289 | { 290 | data: { 291 | type: 'entity', 292 | relationships: { 293 | subentity: { 294 | data: { 295 | type: 'entity', 296 | id: 1 297 | } 298 | } 299 | } 300 | }, 301 | included: [ 302 | { 303 | id: 1, type: 'entity', relationships: { 304 | subentity: { 305 | data: { 306 | type: 'entity', 307 | id: 2 308 | } 309 | } 310 | } 311 | }, 312 | { 313 | id: 2, type: 'entity', relationships: { 314 | subentity: { 315 | data: { 316 | type: 'entity', 317 | id: 3 318 | } 319 | } 320 | } 321 | } 322 | ] 323 | }, 324 | { 325 | entity: { 326 | subentity_attributes: { 327 | id: 1, 328 | subentity_attributes: { 329 | id: 2, 330 | subentity_id: 3 331 | } 332 | } 333 | } 334 | } 335 | ] } 336 | ], 337 | 'PATCH update payloads' => [ 338 | { 'https://jsonapi.org/format/#crud example (modified, multiple photographers)' => [ 339 | { 340 | data: { 341 | type: 'photos', 342 | attributes: { 343 | title: 'Ember Hamster', 344 | src: 'http://example.com/images/productivity.png' 345 | }, 346 | relationships: { 347 | photographers: { 348 | data: [ 349 | { 350 | type: 'people', 351 | id: 9 352 | }, 353 | { 354 | type: 'people', 355 | id: 10 356 | } 357 | ] 358 | } 359 | } 360 | }, 361 | included: [ 362 | { 363 | type: 'people', 364 | id: 10, 365 | attributes: { 366 | name: 'Some guy' 367 | } 368 | }, 369 | { 370 | type: 'people', 371 | id: 9, 372 | attributes: { 373 | name: 'Some other guy' 374 | } 375 | } 376 | ] 377 | }, 378 | { 379 | photo: { 380 | title: 'Ember Hamster', 381 | src: 'http://example.com/images/productivity.png', 382 | photographers_attributes: [ 383 | { 384 | id: 9, 385 | name: 'Some other guy' 386 | }, 387 | { 388 | id: 10, 389 | name: 'Some guy' 390 | } 391 | ] 392 | } 393 | } 394 | ] }, 395 | { 'https://jsonapi.org/format/#crud-updating-to-many-relationships example (removal, all photographers)' => [ 396 | { 397 | data: { 398 | type: 'photos', 399 | attributes: { 400 | title: 'Ember Hamster', 401 | src: 'http://example.com/images/productivity.png' 402 | }, 403 | relationships: { 404 | photographers: { 405 | data: [] 406 | } 407 | } 408 | } 409 | }, 410 | { 411 | photo: { 412 | title: 'Ember Hamster', 413 | src: 'http://example.com/images/productivity.png', 414 | photographer_ids: [] 415 | } 416 | } 417 | ] }, 418 | { 'https://jsonapi.org/format/#crud-updating-to-one-relationships example (removal, single owner)' => [ 419 | { 420 | data: { 421 | type: 'account', 422 | attributes: { 423 | name: 'Bob Loblaw', 424 | profile_url: 'http://example.com/images/no-nonsense.png' 425 | }, 426 | relationships: { 427 | owner: { 428 | data: nil 429 | } 430 | } 431 | } 432 | }, 433 | { 434 | account: { 435 | name: 'Bob Loblaw', 436 | profile_url: 'http://example.com/images/no-nonsense.png', 437 | owner_id: nil 438 | } 439 | } 440 | ] }, 441 | { 'https://github.com/pstrzalk case of emptying has_many relationship (w/ empty included)' => [ 442 | { 443 | data: { 444 | type: 'users', 445 | attributes: { 446 | name: 'Adam Joe' 447 | }, 448 | relationships: { 449 | practice_areas: { 450 | data: [] 451 | } 452 | }, 453 | included: [] 454 | } 455 | }, 456 | { 457 | user: { 458 | name: 'Adam Joe', practice_area_ids: [], 459 | } 460 | } 461 | ] }, 462 | { 'https://github.com/pstrzalk case of emptying has_many relationship (w/o included)' => [ 463 | { 464 | data: { 465 | type: 'users', 466 | attributes: { 467 | name: 'Adam Joe' 468 | }, 469 | relationships: { 470 | practice_areas: { 471 | data: [] 472 | } 473 | } 474 | } 475 | }, 476 | { 477 | user: { 478 | name: 'Adam Joe', practice_area_ids: [], 479 | } 480 | } 481 | ] }, 482 | { 'os-15 case - nested relationship within a relationship' => [ 483 | { 484 | data: { 485 | id: "1", type: "contacts", 486 | relationships: { 487 | contacts_employment_statuses: { 488 | data: [ 489 | { id: 444, type: "contact_employment_statuses" } 490 | ] 491 | } 492 | } 493 | }, 494 | included: [ 495 | { 496 | id: 444, type: "contact_employment_statuses", 497 | attributes: { 498 | involved_in: true, receives_submissions: false 499 | }, 500 | relationships: { 501 | employment_status: { data: { id: 110, type: "employment_statuses" } } 502 | } 503 | } 504 | ] 505 | }, 506 | { 507 | contact: { 508 | id: "1", 509 | contacts_employment_statuses_attributes: [ 510 | { id: 444, involved_in: true, receives_submissions: false, employment_status_id: 110 } 511 | ] 512 | } 513 | } 514 | ] }, 515 | { 'os-15 case extended - nested relationship within a to-many relationship, with an included object' => [ 516 | { 517 | data: { 518 | id: "1", type: "contacts", 519 | relationships: { 520 | contacts_employment_statuses: { 521 | data: [ 522 | { id: 444, type: "contact_employment_statuses" } 523 | ] 524 | } 525 | } 526 | }, 527 | included: [ 528 | { 529 | id: 444, type: "contact_employment_statuses", 530 | attributes: { 531 | involved_in: true, receives_submissions: false 532 | }, 533 | relationships: { 534 | employment_status: { data: { id: 110, type: "employment_statuses" } } 535 | } 536 | }, 537 | { 538 | id: 110, type: "employment_statuses", 539 | attributes: { 540 | status: "yes", 541 | } 542 | } 543 | ] 544 | }, 545 | { 546 | contact: { 547 | id: "1", 548 | contacts_employment_statuses_attributes: [ 549 | { id: 444, involved_in: true, receives_submissions: false, employment_status_attributes: { id: 110, status: "yes" } } 550 | ] 551 | } 552 | } 553 | ] }, 554 | { 'os-15 case extended - nested relationship within a to-many relationship, with a to-many relationship' => [ 555 | { 556 | data: { 557 | id: "1", type: "contacts", 558 | relationships: { 559 | contacts_employment_statuses: { 560 | data: [ 561 | { id: 444, type: "contact_employment_statuses" } 562 | ] 563 | } 564 | } 565 | }, 566 | included: [ 567 | { 568 | id: 444, type: "contact_employment_statuses", 569 | attributes: { 570 | involved_in: true, receives_submissions: false 571 | }, 572 | relationships: { 573 | employment_status: { data: [{ id: 110, type: "employment_statuses" }] } 574 | } 575 | }, 576 | ] 577 | }, 578 | { 579 | contact: { 580 | id: "1", 581 | contacts_employment_statuses_attributes: [ 582 | { id: 444, involved_in: true, receives_submissions: false, employment_status_ids: [110] } 583 | ] 584 | } 585 | } 586 | ] }, 587 | { 'os-15 case extended - nested relationship within a to-one relationship' => [ 588 | { 589 | data: { 590 | id: "1", type: "contacts", 591 | relationships: { 592 | contacts_employment_status: { 593 | data: { id: 444, type: "contact_employment_status" } 594 | } 595 | } 596 | }, 597 | included: [ 598 | { 599 | id: 444, type: "contact_employment_status", 600 | attributes: { 601 | involved_in: true, receives_submissions: false 602 | }, 603 | relationships: { 604 | employment_status: { data: { id: 110, type: "employment_statuses" } } 605 | } 606 | } 607 | ] 608 | }, 609 | { 610 | contact: { 611 | id: "1", 612 | contacts_employment_status_attributes: { 613 | id: 444, involved_in: true, receives_submissions: false, employment_status_id: 110 614 | } 615 | } 616 | } 617 | ] }, 618 | ] 619 | } 620 | end 621 | --------------------------------------------------------------------------------