├── .dockerignore ├── .github └── workflows │ ├── codecov.yml │ └── publish.yml ├── .gitignore ├── .rspec ├── .rspec_status ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── bundle ├── console ├── htmldiff ├── kramdown ├── ldiff ├── license_finder ├── license_finder_pip.py ├── maruku ├── marutex ├── nokogiri ├── racc ├── rackup ├── rake ├── redcarpet ├── reverse_markdown ├── rspec ├── rubocop ├── ruby-parse ├── ruby-rewrite ├── setup ├── solargraph ├── thor ├── tilt ├── yard ├── yardoc └── yri ├── docs ├── UsingTheRequestObject.md └── UsingUserConfigurations.md ├── easy-jsonapi.gemspec ├── lib └── easy │ ├── jsonapi.rb │ └── jsonapi │ ├── collection.rb │ ├── config_manager.rb │ ├── config_manager │ └── config.rb │ ├── document.rb │ ├── document │ ├── error.rb │ ├── error │ │ └── error_member.rb │ ├── jsonapi.rb │ ├── jsonapi │ │ └── jsonapi_member.rb │ ├── links.rb │ ├── links │ │ └── link.rb │ ├── meta.rb │ ├── meta │ │ └── meta_member.rb │ ├── resource.rb │ ├── resource │ │ ├── attributes.rb │ │ ├── attributes │ │ │ └── attribute.rb │ │ ├── relationships.rb │ │ └── relationships │ │ │ └── relationship.rb │ └── resource_id.rb │ ├── exceptions.rb │ ├── exceptions │ ├── document_exceptions.rb │ ├── headers_exceptions.rb │ ├── json_parse_error.rb │ ├── naming_exceptions.rb │ ├── query_params_exceptions.rb │ └── user_defined_exceptions.rb │ ├── field.rb │ ├── header_collection.rb │ ├── header_collection │ └── header.rb │ ├── item.rb │ ├── middleware.rb │ ├── name_value_pair.rb │ ├── name_value_pair_collection.rb │ ├── parser.rb │ ├── parser │ ├── document_parser.rb │ ├── headers_parser.rb │ ├── json_parser.rb │ └── rack_req_params_parser.rb │ ├── request.rb │ ├── request │ ├── query_param_collection.rb │ └── query_param_collection │ │ ├── fields_param.rb │ │ ├── fields_param │ │ └── fieldset.rb │ │ ├── filter_param.rb │ │ ├── filter_param │ │ └── filter.rb │ │ ├── include_param.rb │ │ ├── page_param.rb │ │ ├── query_param.rb │ │ └── sort_param.rb │ ├── response.rb │ ├── utility.rb │ └── version.rb └── spec ├── collection_spec.rb ├── config_manager └── config_spec.rb ├── config_manager_spec.rb ├── document ├── error │ └── error_member_spec.rb ├── error_spec.rb ├── jsonapi │ └── jsonapi_member_spec.rb ├── jsonapi_spec.rb ├── links │ └── link_spec.rb ├── links_spec.rb ├── meta │ └── meta_member_spec.rb ├── meta_spec.rb ├── resource │ ├── attributes │ │ └── attribute_spec.rb │ ├── attributes_spec.rb │ ├── relationships │ │ └── relationship_spec.rb │ └── relationships_spec.rb ├── resource_id_spec.rb └── resource_spec.rb ├── document_spec.rb ├── exceptions ├── document_exceptions_spec.rb ├── headers_exceptions_spec.rb ├── naming_exceptions_spec.rb ├── query_params_exceptions_spec.rb └── user_define_exceptions_spec.rb ├── field_spec.rb ├── header_collection └── header_spec.rb ├── header_collection_spec.rb ├── item_spec.rb ├── middleware_spec.rb ├── name_value_pair_collection_spec.rb ├── name_value_pair_spec.rb ├── parser ├── document_parser_spec.rb ├── header_parser_spec.rb ├── json_parser_spec.rb └── rack_req_params_parser_spec.rb ├── parser_spec.rb ├── rack_app.rb ├── request ├── query_param_collection │ ├── fields_param │ │ └── fieldset_spec.rb │ ├── fields_param_spec.rb │ ├── filter_param │ │ └── fitler_spec.rb │ ├── filter_param_spec.rb │ ├── include_param_spec.rb │ ├── page_param_spec.rb │ ├── query_param_spec.rb │ └── sort_param_spec.rb └── query_param_collection_spec.rb ├── request_spec.rb ├── response_spec.rb ├── shared_contexts ├── document_exceptions_shared_context.rb └── headers_exceptions_shared_context.rb ├── shared_examples ├── collection_like_classes.rb ├── document_collections.rb ├── item_shared_tests.rb ├── name_value_and_query_shared_tests.rb ├── name_value_pair_collections.rb ├── name_value_pair_tests.rb └── query_param_tests.rb ├── spec_helper.rb └── utility_spec.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .gitignore 3 | .rspec 4 | .rspec_status 5 | .solargraph.yml 6 | .travis.yml 7 | *.md 8 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: [production, dev] 5 | pull_request: 6 | branches: [production, dev] 7 | jobs: 8 | test: 9 | name: Test 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest] 13 | ruby: ['2.5', '2.6', '2.7', '3.0'] 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@master 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ${{ matrix.ruby }} 21 | - run: | 22 | gem install bundler 23 | bundle install 24 | bundle exec rspec 25 | bash <(curl -s https://codecov.io/bash) 26 | env: 27 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Test, Publish, and Release 2 | 3 | on: 4 | pull_request: 5 | branches: [production] 6 | types: [closed] 7 | 8 | jobs: 9 | build_and_publish: 10 | runs-on: ubuntu-latest 11 | if: github.event.pull_request.merged 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: 3.0.1 20 | 21 | - name: Build gem 22 | run: | 23 | gem install bundler 24 | bundler install 25 | gem build easy-jsonapi.gemspec 26 | 27 | - name: Publish to GPR 28 | run: | 29 | mkdir -p $HOME/.gem 30 | touch $HOME/.gem/credentials 31 | chmod 0600 $HOME/.gem/credentials 32 | printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 33 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem 34 | env: 35 | GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}" 36 | OWNER: ${{ github.repository_owner }} 37 | 38 | - name: Publish to RubyGems 39 | run: | 40 | mkdir -p $HOME/.gem 41 | touch $HOME/.gem/credentials 42 | chmod 0600 $HOME/.gem/credentials 43 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 44 | gem push *.gem 45 | env: 46 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 47 | 48 | - name: Create Release 49 | id: create_release 50 | uses: actions/create-release@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 53 | with: 54 | tag_name: 1.0.11 # update this to release 55 | release_name: Release 1.0.11 # update this to release 56 | draft: false 57 | prerelease: false 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /doc/ 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /tmp/ 9 | 10 | # Used by dotenv library to load environment variables. 11 | # .env 12 | 13 | ## Documentation cache and generated files: 14 | /.yardoc/ 15 | /_yardoc/ 16 | 17 | ## Environment normalization: 18 | /.bundle/ 19 | /vendor/bundle 20 | /lib/bundler/man/ 21 | 22 | # for a library or gem, you might want to ignore these files since the code is 23 | # intended to run in multiple environments; otherwise, check them in: 24 | # Gemfile.lock 25 | # .ruby-version 26 | # .ruby-gemset 27 | 28 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 29 | .rvmrc 30 | 31 | # /bin/ 32 | 33 | # for vscode 34 | /.vscode/ 35 | 36 | .solargraph.yml 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | TargetRubyVersion: '3.0.1' 4 | SuggestExtensions: false 5 | 6 | Metrics/AbcSize: 7 | Enabled: false 8 | 9 | Layout/TrailingWhitespace: 10 | Enabled: false 11 | 12 | Layout/EmptyLinesAroundModuleBody: 13 | Enabled: false 14 | 15 | Layout/EmptyLines: 16 | Enabled: false 17 | 18 | Layout/EmptyLinesAroundClassBody: 19 | Enabled: false 20 | 21 | Metrics/BlockLength: 22 | Enabled: false 23 | 24 | Metrics/MethodLength: 25 | Enabled: false 26 | 27 | Metrics/ParameterLists: 28 | Enabled: false 29 | 30 | Bundler/OrderedGems: 31 | Enabled: false 32 | 33 | Layout/EmptyLinesAroundBlockBody: 34 | Enabled: false 35 | 36 | Layout/EmptyLineAfterGuardClause: 37 | Enabled: false 38 | 39 | Style/WordArray: 40 | Enabled: false 41 | 42 | Style/StringConcatenation: 43 | Enabled: false 44 | 45 | Layout/EmptyLinesAroundMethodBody: 46 | Enabled: false 47 | 48 | Style/StringLiterals: 49 | Enabled: false 50 | 51 | Style/IfUnlessModifier: 52 | Enabled: false 53 | 54 | Layout/LineLength: 55 | Enabled: false 56 | 57 | Gemspec/OrderedDependencies: 58 | Enabled: false 59 | 60 | Metrics/ModuleLength: 61 | Enabled: false 62 | 63 | Layout/ArgumentAlignment: 64 | Enabled: false 65 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | easy-jsonapi 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 3.0.1 5 | before_install: 6 | - gem install bundler 7 | - bundler install 8 | script: bundle exec rake build 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.0.11 - 2021-10-8 4 | - Updated dependencies to fix security vulnerability 5 | 6 | ## 1.0.10 - 2021-09-29 7 | - Updated dependencies to fix security vulnerability 8 | 9 | ## 1.0.9 - 2021-05-04 10 | 11 | - Updated dependencies to fix security vulnerability in rexml 12 | 13 | ## 1.0.8 - 2021-05-04 14 | 15 | - Updated dependencies to fix security vulnerability in rexml 16 | 17 | ## 1.0.7 - 2021-03-31 18 | 19 | - Fixed bug in JSONAPI::Parser::JSONParser that would serialize hashes with symbol key values instead of string 20 | 21 | ## 1.0.6 - 2021-03-30 22 | 23 | - Fixed bug in JSONAPI::Middleware that was not checking for environment variables properly 24 | 25 | ## 1.0.5 - 2021-03-30 26 | 27 | - Fixed bug in JSONAPI::Exceptions::HeadersExceptions that didn't check for user required headers requirements 28 | - Fixed bug in JSONAPI::Exceptions::QueryParamExceptions that didn't check for user required query param requirements 29 | - Added more tests to the middleware 30 | - Updated Documentation 31 | 32 | ## 1.0.4 - 2021-03-28 33 | 34 | - Fixed JSONAPI::ExceptionsHeadersExceptions bug 35 | - Updated README files 36 | 37 | ## 1.0.3 - 2021-03-25 38 | 39 | - Updated JSONAPI::Exceptions::HeadersExceptions to allow wildcard matching for Accept header 40 | 41 | ## 1.0.2 - 2021-03-25 42 | 43 | - Updated README and fix READE broken links 44 | - Reorganization of README files into docs file. 45 | - Make easy-jsonapi compatible with ruby versions >= 2.5 46 | - Added wrapper around Oj usage so all raised errors are found in the JSONAPI::Exceptions module 47 | 48 | ## 1.0.0 - 2021-03-24 49 | 50 | - This is the first release with a version of 1.0.0. All main features supported, but user configurations can be developed to provide greater adherence to the spec and more developer features. 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at joshua.demoss@curatess.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | easy-jsonapi (1.0.11) 5 | oj (~> 3.10) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ast (2.4.2) 11 | backport (1.2.0) 12 | benchmark (0.1.1) 13 | codecov (0.6.0) 14 | simplecov (>= 0.15, < 0.22) 15 | diff-lcs (1.4.4) 16 | docile (1.4.0) 17 | e2mmap (0.1.0) 18 | jaro_winkler (1.5.4) 19 | kramdown (2.3.1) 20 | rexml 21 | kramdown-parser-gfm (1.1.0) 22 | kramdown (~> 2.0) 23 | nokogiri (1.12.5-x86_64-darwin) 24 | racc (~> 1.4) 25 | oj (3.13.8) 26 | parallel (1.21.0) 27 | parser (3.0.2.0) 28 | ast (~> 2.4.1) 29 | racc (1.5.2) 30 | rack (2.2.3) 31 | rainbow (3.0.0) 32 | rake (13.0.6) 33 | redcarpet (3.5.1) 34 | regexp_parser (2.1.1) 35 | reverse_markdown (2.0.0) 36 | nokogiri 37 | rexml (3.2.5) 38 | rspec (3.10.0) 39 | rspec-core (~> 3.10.0) 40 | rspec-expectations (~> 3.10.0) 41 | rspec-mocks (~> 3.10.0) 42 | rspec-core (3.10.1) 43 | rspec-support (~> 3.10.0) 44 | rspec-expectations (3.10.1) 45 | diff-lcs (>= 1.2.0, < 2.0) 46 | rspec-support (~> 3.10.0) 47 | rspec-mocks (3.10.2) 48 | diff-lcs (>= 1.2.0, < 2.0) 49 | rspec-support (~> 3.10.0) 50 | rspec-support (3.10.2) 51 | rubocop (1.22.1) 52 | parallel (~> 1.10) 53 | parser (>= 3.0.0.0) 54 | rainbow (>= 2.2.2, < 4.0) 55 | regexp_parser (>= 1.8, < 3.0) 56 | rexml 57 | rubocop-ast (>= 1.12.0, < 2.0) 58 | ruby-progressbar (~> 1.7) 59 | unicode-display_width (>= 1.4.0, < 3.0) 60 | rubocop-ast (1.12.0) 61 | parser (>= 3.0.1.1) 62 | ruby-progressbar (1.11.0) 63 | simplecov (0.21.2) 64 | docile (~> 1.1) 65 | simplecov-html (~> 0.11) 66 | simplecov_json_formatter (~> 0.1) 67 | simplecov-html (0.12.3) 68 | simplecov_json_formatter (0.1.3) 69 | solargraph (0.44.0) 70 | backport (~> 1.2) 71 | benchmark 72 | bundler (>= 1.17.2) 73 | diff-lcs (~> 1.4) 74 | e2mmap 75 | jaro_winkler (~> 1.5) 76 | kramdown (~> 2.3) 77 | kramdown-parser-gfm (~> 1.1) 78 | parser (~> 3.0) 79 | reverse_markdown (>= 1.0.5, < 3) 80 | rubocop (>= 0.52) 81 | thor (~> 1.0) 82 | tilt (~> 2.0) 83 | yard (~> 0.9, >= 0.9.24) 84 | thor (1.1.0) 85 | tilt (2.0.10) 86 | unicode-display_width (2.1.0) 87 | yard (0.9.26) 88 | 89 | PLATFORMS 90 | x86_64-darwin-20 91 | 92 | DEPENDENCIES 93 | codecov (~> 0.4) 94 | easy-jsonapi! 95 | rack (~> 2.2) 96 | rake (~> 13.0) 97 | redcarpet (~> 3.5) 98 | rspec (~> 3.9) 99 | rubocop (~> 1.11) 100 | solargraph (~> 0.39) 101 | 102 | BUNDLED WITH 103 | 2.2.15 104 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Joshua DeMoss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | require 'yard' 5 | 6 | task default: %i[test build document] 7 | 8 | RSpec::Core::RakeTask.new(:test) do |t| 9 | t.verbose = false 10 | end 11 | 12 | task :build do 13 | system('gem build easy-jsonapi.gemspec') 14 | end 15 | 16 | YARD::Rake::YardocTask.new(:document) do |t| 17 | t.files = ['lib/**/*.rb'] # optional 18 | t.options = ['--title', "YARD #{YARD::VERSION} Documentation", '--markup=markdown'] # optional 19 | t.stats_options = ['--list-undoc'] # optional 20 | end 21 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'easy-jsonapi' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/htmldiff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'htmldiff' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("diff-lcs", "htmldiff") 30 | -------------------------------------------------------------------------------- /bin/kramdown: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'kramdown' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("kramdown", "kramdown") 30 | -------------------------------------------------------------------------------- /bin/ldiff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'ldiff' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("diff-lcs", "ldiff") 30 | -------------------------------------------------------------------------------- /bin/license_finder: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'license_finder' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("license_finder", "license_finder") 30 | -------------------------------------------------------------------------------- /bin/license_finder_pip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'license_finder_pip.py' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("license_finder", "license_finder_pip.py") 30 | -------------------------------------------------------------------------------- /bin/maruku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'maruku' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path('bundle', __dir__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("maruku", "maruku") 30 | -------------------------------------------------------------------------------- /bin/marutex: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'marutex' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path('bundle', __dir__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("maruku", "marutex") 30 | -------------------------------------------------------------------------------- /bin/nokogiri: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'nokogiri' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("nokogiri", "nokogiri") 30 | -------------------------------------------------------------------------------- /bin/racc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'racc' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("racc", "racc") 30 | -------------------------------------------------------------------------------- /bin/rackup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rackup' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rack", "rackup") 30 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/redcarpet: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'redcarpet' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("redcarpet", "redcarpet") 30 | -------------------------------------------------------------------------------- /bin/reverse_markdown: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'reverse_markdown' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("reverse_markdown", "reverse_markdown") 30 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rspec-core", "rspec") 30 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rubocop' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rubocop", "rubocop") 30 | -------------------------------------------------------------------------------- /bin/ruby-parse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'ruby-parse' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("parser", "ruby-parse") 30 | -------------------------------------------------------------------------------- /bin/ruby-rewrite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'ruby-rewrite' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("parser", "ruby-rewrite") 30 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/solargraph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'solargraph' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("solargraph", "solargraph") 30 | -------------------------------------------------------------------------------- /bin/thor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'thor' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("thor", "thor") 30 | -------------------------------------------------------------------------------- /bin/tilt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'tilt' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("tilt", "tilt") 30 | -------------------------------------------------------------------------------- /bin/yard: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'yard' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("yard", "yard") 30 | -------------------------------------------------------------------------------- /bin/yardoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'yardoc' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("yard", "yardoc") 30 | -------------------------------------------------------------------------------- /bin/yri: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'yri' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("yard", "yri") 30 | -------------------------------------------------------------------------------- /docs/UsingTheRequestObject.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Accessing Different Parts of The Request 7 | 8 | ```ruby 9 | j_req = JSONAPI::Parser.parse(env) 10 | ``` 11 | 12 | ## Quick Access Methods 13 | 14 | ```ruby 15 | j_req.path # Gives path info 16 | j_req.http_method # GET, POST, PUT, etc 17 | j_req.host # localhost 18 | j_req.port # 8080 19 | j_req.query_string # query string 20 | ``` 21 | 22 | ## Accessing the Query Params 23 | 24 | ```ruby 25 | j_req.params # returns enumerable JSONAPI::Request::QueryParamCollection 26 | 27 | q_param = JSONAPI::Request::QueryParamCollection::QueryParam.new 'new_name' 'value' 28 | j_req.params.add(q_param) # add a query param to the collection 29 | j_req.params.get('new_name') # get param 30 | j_req.params.new_name # dynamically get param 31 | j_req.params.remove('new_name') # remove header 32 | j_req.params.to_h # { new_name: ['value'] } 33 | 34 | # given ?include=author,comments&filter[author]=name&sort=alpha 35 | j_req.params.includes # includes 36 | j_req.paras.filters # resource filters 37 | j_req.params.sorts # resource ordering 38 | j_req.params.page # page / offset 39 | j_req.params.fields # sparse fieldsets 40 | j_req.params.to_s # include=author,comments&filter[name]=test&new_name=new_val 41 | ``` 42 | 43 | ## Accessing the Headers 44 | 45 | ```ruby 46 | j_req.headers # returns enumerable JSONAPI::HeaderCollection 47 | 48 | h = JSONAPI::HeaderCollection::Header.new 'Content-Type', 'text/html' 49 | j_req.headers.add(h) 50 | j_req.headers.get('content-type') # retrieves header 51 | j_req.headers.content_type # dynamically retrieves header 52 | j_req.headers.remove('content-type') # remove header 53 | j_req.headers.to_h # { CONTENT_TYPE: 'text/html' } 54 | j_req.headers.to_s # (JSON compliant) { "CONTENT_TYPE": "text/html" } 55 | ``` 56 | 57 | ## Accessing the Request Body 58 | 59 | ```ruby 60 | j_req.body # returns JSONAPI::Document 61 | 62 | j_req.body.data # The JSONAPI data member 63 | j_req.body.meta # The JSONAPI meta member 64 | j_req.body.links # The JSONAPI links member 65 | j_req.body.included # The JSONAPI included member 66 | j_req.body.errors # The JSONAPI errors member 67 | j_req.body.jsonapi # The JSONAPI jsonapi member 68 | 69 | j_req.body.to_s # serialized JSONAPI 70 | j_req.body.to_h # ruby hash representation of JSONAPI 71 | 72 | # NOTE: j_req.body.data returns a resource or an array of resources depending on the request 73 | j_req.body.data # JSONAPI::Document::Resource or [JSONAPI::Document::Resource] 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/UsingUserConfigurations.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Configuring the Middleware 7 | 8 | ## Quick Start 9 | 10 | To add custom checks to the middleware, modify a Config object and pass it to the Config Manager. 11 | 12 | The Config Manager is accessible through a block passed to the middleware upon initialization. 13 | 14 | ```ruby 15 | use JSONAPI::Middleware do |config_manager| 16 | # ... 17 | end 18 | ``` 19 | 20 | To add restrictions to ALL requests use the default global config included with the Config Manager: 21 | 22 | ```ruby 23 | use JSONAPI::Middleware do |config_manager| 24 | config_manager.global.allow_client_ids = true 25 | end 26 | ``` 27 | 28 | To set up a restrictions for a specific resource type, create and configure a new Config object and add it to the Config Manager: 29 | 30 | ```ruby 31 | use JSONAPI::Middleware do |config_manager| 32 | config = JSONAPI::ConfigManager::Config.new 33 | config.allow_client_ids = true 34 | config_manager[:person] = config 35 | end 36 | ``` 37 | 38 | ## Available Config Options 39 | 40 | ### Document Checking Customization 41 | 42 | To specify required members in a document, create a hash in the structure of the expected JSON document, and list required members as nil. Members that are not required do not have to be listed. 43 | 44 | ```ruby 45 | config.required_document_members = 46 | { 47 | data: { 48 | attributes: { 49 | this_is_required: nil 50 | } 51 | }, 52 | meta: nil 53 | } 54 | ``` 55 | 56 | You can go even further by adding a proc instead of nil to provide a custom way of determining whether a value (and request) is valid. 57 | 58 | ```ruby 59 | config.required_document_members = 60 | { 61 | data: { 62 | attributes: { 63 | this_is_required: proc { |value| ['im_allowed', 'me_too', 'also_me'].include?(value) } 64 | } 65 | }, 66 | meta: proc { |value_hash| value_hash.keys?(:count) } 67 | } 68 | ``` 69 | 70 | To allow for client generated ids, set the method to true. 71 | 72 | ```ruby 73 | config.allow_client_ids = true 74 | ``` 75 | 76 | ### Header Checking Customization 77 | 78 | Specify a list of required headers: 79 | 80 | ```ruby 81 | config.required_headers = %w[content-type xxx-authentication] 82 | ``` 83 | 84 | ### Query Param Customization 85 | 86 | Specify a list of required query params: 87 | 88 | ```ruby 89 | config.required_query_params = 90 | { 91 | fields: { people: nil }, 92 | include: nil, 93 | custom_param: nil 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /easy-jsonapi.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = 'easy-jsonapi' 5 | spec.version = '1.0.11' # update this upon release 6 | spec.authors = ['Joshua DeMoss, Joe Viscomi'] 7 | spec.email = ['demoss.joshua@gmail.com'] 8 | 9 | spec.summary = 'Middleware, Parser, and Validator for JSONAPI requests and serialized resopnses' 10 | spec.description = 'Middleware to screen non-JSONAPI-compliant requests, a parser to provide Object Oriented access to requests, and a validator for validating JSONAPI Serialized responses.' 11 | spec.homepage = 'https://rubygems.org/gems/easy-jsonapi' 12 | spec.required_ruby_version = '>= 2.5' 13 | 14 | spec.metadata["source_code_uri"] = "https://github.com/Curatess/easy-jsonapi" 15 | spec.metadata["changelog_uri"] = "https://github.com/Curatess/easy-jsonapi/CHANGELOG.mg" 16 | 17 | # Specify which files should be added to the gem when it is released. 18 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 19 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 20 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | end 22 | spec.bindir = 'exe' 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ['lib'] 25 | 26 | # Dev Dependencies 27 | spec.add_development_dependency 'rack', '~> 2.2' 28 | spec.add_development_dependency 'rake', '~> 13.0' 29 | spec.add_development_dependency 'redcarpet', '~> 3.5' 30 | spec.add_development_dependency 'rspec', '~> 3.9' 31 | spec.add_development_dependency 'rubocop', '~> 1.11' 32 | spec.add_development_dependency 'solargraph', '~> 0.39' 33 | spec.add_development_dependency 'codecov', '~> 0.4' 34 | 35 | # Dependencies 36 | spec.add_dependency 'oj', '~> 3.10' 37 | 38 | spec.license = 'MIT' 39 | end 40 | -------------------------------------------------------------------------------- /lib/easy/jsonapi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/middleware' 4 | require 'easy/jsonapi/parser' 5 | require 'easy/jsonapi/response' 6 | 7 | # This module is the top level namespace for the curatess jsonapi middleware gem 8 | # 9 | # @author Joshua DeMoss 10 | # @see https://app.lucidchart.com/invitations/accept/e24c2cfe-78f1-4192-8e88-6dbc4454a5ea UML Class Diagram 11 | module JSONAPI 12 | end 13 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | # Models a collection of items 5 | class Collection 6 | include Enumerable 7 | 8 | # Assume collection is empty not innitialized with an array of objects. 9 | # @param arr_of_obj [Object] The objects to be stored 10 | # for block { |item| item[:name] } 11 | # @yield [item] Determines what should be used as keys when storing objects in collection's internal hash 12 | def initialize(arr_of_obj = [], item_type: Object, &block) 13 | @item_type = item_type 14 | @collection = {} 15 | 16 | return unless (arr_of_obj != []) && block_given? 17 | 18 | arr_of_obj.each do |obj| 19 | add(obj, &block) 20 | end 21 | end 22 | 23 | # Collection.new([ 24 | # { key: 'include', value: 'authors,comments,likes' }, 25 | # { key: 'lebron', value: 'james' }, 26 | # { key: 'charles', value: 'barkley' }, 27 | # { key: 'michael', value: 'jordan,jackson' }, 28 | # { key: 'kobe', value: 'bryant' } 29 | # ] 30 | 31 | # Checks to see if the collection is empty 32 | # @return [TrueClass | FalseClass] 33 | def empty? 34 | @collection == {} 35 | end 36 | 37 | # Does the collection's internal hash include this key? 38 | # @param key [String | Symbol] The key to search for in the hash 39 | def include?(key) 40 | @collection.include?(key.to_sym) 41 | end 42 | 43 | # Add an item to the collection, giving a block to indicate how the 44 | # collection should create a hash key for the item. 45 | # @param item [Object] 46 | def add(item, &block) 47 | raise 'a block must be passed to #add indicating what should be used as a key' unless block_given? 48 | raise "Cannot add an item that is not #{@item_type}" unless item.is_a? @item_type 49 | insert(block.call(item), item) 50 | end 51 | 52 | # Adds an item to Collection's internal hash 53 | def insert(key, item) 54 | if include?(key) 55 | raise 'The hash key given already has an Item associated with it. ' \ 56 | 'Remove existing item first.' 57 | end 58 | set(key, item) 59 | end 60 | 61 | # Overwrites the item associated w a given key, or adds an association if no item is already associated. 62 | def set(key, item) 63 | raise "Cannot add an item that is not #{@item_type}" unless item.is_a? @item_type 64 | @collection[key.to_sym] = item 65 | end 66 | 67 | # Yield the block given on all the items in the collection 68 | def each 69 | return @collection.each { |_, item| yield(item) } if block_given? 70 | to_enum(:each) 71 | end 72 | 73 | # Remove an item from the collection 74 | # @param (see #include) 75 | # @return [Item | nil] the deleted item object if it exists 76 | def remove(key) 77 | return nil if @collection[key.to_sym].nil? 78 | @collection.delete(key.to_sym) 79 | end 80 | 81 | # @param (see #remove) 82 | # @return [Item | nil] The appropriate Item object if it exists 83 | def get(key) 84 | @collection[key.to_sym] 85 | end 86 | 87 | # Alias to #get 88 | # @param (see #get) 89 | # @param (see #get) 90 | def [](key) 91 | get(key) 92 | end 93 | 94 | # Alias to #set 95 | # @param (see #set) 96 | # @param (see #set) 97 | def []=(key, item) 98 | set(key, item) 99 | end 100 | 101 | # Allows the developer to treat the Collection class as a hash, retrieving all keys mapped to Items. 102 | # @return [Array] An array of all the item keys stored in the Collection object. 103 | def keys 104 | @collection.keys 105 | end 106 | 107 | # @return [Integer] The number of items in the collection 108 | def size 109 | @collection.size 110 | end 111 | 112 | # Used to print out the Collection object with better formatting 113 | # return [String] The collection object contents represented as a formatted string 114 | def to_s 115 | to_return = '{ ' 116 | is_first = true 117 | @collection.each do |k, item| 118 | if is_first 119 | to_return += "\"#{k}\": #{item}" 120 | is_first = false 121 | else 122 | to_return += ", \"#{k}\": #{item}" 123 | end 124 | end 125 | to_return += ' }' 126 | end 127 | 128 | private 129 | 130 | # Gets the Collection object whose hash key matches the method_name called 131 | # @param method_name [Symbol] The name of the method called 132 | # @param args If any arguments were passed to the method called 133 | # @param block If a block was passed to the method called 134 | def method_missing(method_name, *args, &block) 135 | super unless @collection.include?(method_name) 136 | get(method_name) 137 | end 138 | 139 | # Whether or not method missing should be called. 140 | def respond_to_missing?(method_name, *) 141 | @collection.include?(method_name) || super 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/config_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/config_manager/config' 4 | 5 | module JSONAPI 6 | 7 | # Manages user configuration options 8 | class ConfigManager 9 | 10 | include Enumerable 11 | 12 | # Config Manager always has an internal global config 13 | def initialize 14 | @class_type = JSONAPI::ConfigManager::Config 15 | @config_manager = { global: JSONAPI::ConfigManager::Config.new } 16 | end 17 | 18 | # Yield the block given on all the config in the config_manager 19 | def each(&block) 20 | return @config_manager.each(&block) if block_given? 21 | to_enum(:each) 22 | end 23 | 24 | # Are any user configurations set? 25 | def default? 26 | (@config_manager.size == 1 && @config_manager[:global].default?) || all_configs_default? 27 | end 28 | 29 | # Does the config_manager's internal hash include this res_name? 30 | # @param res_name [String | Symbol] The res_name to search for in the hash 31 | def include?(res_name) 32 | @config_manager.include?(res_name.to_sym) 33 | end 34 | 35 | # Add an config to the config_manager 36 | # @param config [Object] 37 | def add(res_name, config) 38 | raise "Cannot add a config that is not #{@class_type}" unless config.is_a? @class_type 39 | insert(res_name, config) 40 | end 41 | 42 | # Adds an config to config_manager's internal hash 43 | def insert(res_name, config) 44 | if include?(res_name.to_sym) 45 | raise "The resource type: #{res_name}, already has an config associated with it. " \ 46 | 'Remove existing config first.' 47 | end 48 | set(res_name, config) 49 | end 50 | 51 | # Overwrites the config associated w a given res_name, or adds an association if no config is already associated. 52 | def set(res_name, config) 53 | raise "Cannot add a config that is not #{@class_type}" unless config.is_a? @class_type 54 | @config_manager[res_name.to_sym] = config 55 | end 56 | 57 | # Alias to #set 58 | # @param (see #set) 59 | # @param (see #set) 60 | def []=(res_name, config) 61 | set(res_name, config) 62 | end 63 | 64 | # @param (see #remove) 65 | # @return [JSONAPI::ConfigManager::Config | nil] The appropriate Item object if it exists 66 | def get(res_name) 67 | @config_manager[res_name.to_sym] 68 | end 69 | 70 | # Alias to #get 71 | # @param (see #get) 72 | # @param (see #get) 73 | def [](res_name) 74 | get(res_name) 75 | end 76 | 77 | # Remove an config from the config_manager 78 | # @param (see #include) 79 | # @return [JSONAPI::ConfigManager::Config | nil] the deleted config object if it exists 80 | def remove(res_name) 81 | if res_name.to_s == 'global' 82 | raise "Cannot remove global config" 83 | end 84 | @config_manager.delete(res_name.to_sym) 85 | end 86 | 87 | # @return [Integer] The number of config in the config_manager 88 | def size 89 | configs.size 90 | end 91 | 92 | # @return [Array] The names of the resource types the configs belong to 93 | def configs 94 | c_arr = [] 95 | @config_manager.each_key do |res_type| 96 | c = self[res_type] 97 | unless c.default? 98 | c_arr << res_type 99 | end 100 | end 101 | c_arr 102 | end 103 | 104 | # Used to print out the config_manager object with better formatting 105 | # return [String] The config_manager object contents represented as a formatted string 106 | def to_s 107 | to_return = '{ ' 108 | is_first = true 109 | each do |k, config| 110 | if is_first 111 | to_return += "#{k}: #{config}" 112 | is_first = false 113 | else 114 | to_return += ", #{k}: #{config}" 115 | end 116 | end 117 | to_return += ' }' 118 | end 119 | 120 | private 121 | 122 | # Gets the config_manager object whose hash res_name matches the method_name called 123 | # @param method_name [Symbol] The name of the method called 124 | # @param args If any arguments were passed to the method called 125 | # @param block If a block was passed to the method called 126 | def method_missing(method_name, *args, &block) 127 | super unless @config_manager.include?(method_name) 128 | get(method_name) 129 | end 130 | 131 | # Whether or not method missing should be called. 132 | def respond_to_missing?(method_name, *) 133 | @config_manager.include?(method_name) || super 134 | end 135 | 136 | # All of the included configs are set to default? 137 | # @return [TrueClass | FalseClass] 138 | def all_configs_default? 139 | res = true 140 | @config_manager.each_value { |c| res &= c.default? } 141 | res 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/config_manager/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | class ConfigManager 5 | 6 | # User configurations for the gem 7 | class Config 8 | 9 | attr_reader :required_document_members, :required_headers, :required_query_params, 10 | :allow_client_ids 11 | 12 | def initialize 13 | @allow_client_ids = false 14 | @default = true 15 | end 16 | 17 | # Performancewise, configs are all initialized as a startup cost, to change them you need to 18 | # restart the server. As a result of this, the #default? is used to process a request 19 | # faster if user-defined configs do not need to be checked when screening http requests. 20 | # Because @default is set to false upon config assignment (see #method missing in Config), 21 | # this allows the a user to potentially make the middleware screening less performant than necessary 22 | # by assigning config values to the default values, or assigning values to something not default, 23 | # and then assigning config values to the default again. If used as intended, however, this should make 24 | # the middleware screening faster. 25 | # @return [TrueClass | FalseClass] 26 | def default? 27 | @default 28 | end 29 | 30 | private 31 | 32 | READER_METHODS = %i[required_document_members required_headers required_query_params allow_client_ids].freeze 33 | 34 | # Only used if implementing Item directly. 35 | # dynamically creates accessor methods for instance variables 36 | # created in the initialize 37 | def method_missing(method_name, *args, &block) 38 | super unless READER_METHODS.include?(method_name.to_s[0..-2].to_sym) 39 | instance_variable_set("@#{method_name}"[0..-2].to_sym, args[0]) 40 | @default = false 41 | end 42 | 43 | # Needed when using #method_missing 44 | def respond_to_missing?(method_name, *args) 45 | methods.include?(method_name) || super 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/document.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # classes extending document: (require needed because parser requires document) 4 | require 'easy/jsonapi/document/resource' 5 | require 'easy/jsonapi/document/resource_id' 6 | require 'easy/jsonapi/document/error' 7 | require 'easy/jsonapi/document/jsonapi' 8 | require 'easy/jsonapi/document/links' 9 | require 'easy/jsonapi/document/meta' 10 | 11 | require 'easy/jsonapi/utility' 12 | require 'easy/jsonapi/exceptions/document_exceptions' 13 | 14 | module JSONAPI 15 | 16 | # Contains all objects relating to a JSONAPI Document 17 | class Document 18 | 19 | attr_reader :data, :meta, :links, :included, :errors, :jsonapi 20 | 21 | # @param document [Hash] A hash of the different possible document members 22 | # with the values being clases associated with those members 23 | # @data is either a JSONAPI::Document::Resource or a Array 24 | # or a JSONAPI::Document::ResourceId or a Array 25 | # @meta is JSONAPI::Document::Meta 26 | # @links is JSONAPI::Document::Links 27 | # @included is an Array 28 | # @errors is an Array 29 | # @jsonapi is JSONAPI::Document::Jsonapi 30 | # @raise RuntimeError A document must be initialized with a hash of its members. 31 | def initialize(document = {}) 32 | raise 'JSONAPI::Document parameter must be a Hash' unless document.is_a? Hash 33 | @data = document[:data] 34 | @meta = document[:meta] 35 | @links = document[:links] # software generated? 36 | @included = document[:included] 37 | @errors = document[:errors] 38 | @jsonapi = document[:jsonapi] # online documentation 39 | end 40 | 41 | # Represent as a string mimicing the JSONAPI format 42 | def to_s 43 | '{ ' \ 44 | "#{JSONAPI::Utility.member_to_s('data', @data, first_member: true)}" \ 45 | "#{JSONAPI::Utility.member_to_s('meta', @meta)}" \ 46 | "#{JSONAPI::Utility.member_to_s('links', @links)}" \ 47 | "#{JSONAPI::Utility.member_to_s('included', @included)}" \ 48 | "#{JSONAPI::Utility.member_to_s('errors', @errors)}" \ 49 | "#{JSONAPI::Utility.member_to_s('jsonapi', @jsonapi)}" \ 50 | ' }' 51 | end 52 | 53 | # Represent as a hash mimicing the JSONAPI format 54 | def to_h 55 | to_return = {} 56 | JSONAPI::Utility.to_h_member(to_return, @data, :data) 57 | JSONAPI::Utility.to_h_member(to_return, @meta, :meta) 58 | JSONAPI::Utility.to_h_member(to_return, @links, :links) 59 | JSONAPI::Utility.to_h_member(to_return, @included, :included) 60 | JSONAPI::Utility.to_h_member(to_return, @errors, :errors) 61 | JSONAPI::Utility.to_h_member(to_return, @jsonapi, :jsonapi) 62 | to_return 63 | end 64 | 65 | # Check if the document is JSONAPI compliant 66 | # @raise If the document's to_h does not comply 67 | def validate 68 | JSONAPI::Exceptions::DocumentExceptions.check_compliance(to_h) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/document/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair_collection' 4 | require 'easy/jsonapi/document/error/error_member' # extension 5 | require 'easy/jsonapi/utility' 6 | 7 | module JSONAPI 8 | class Document 9 | # An individual errors member in a jsonapi's document top level 'errors' member array 10 | class Error < JSONAPI::NameValuePairCollection 11 | 12 | # @param err_members [Array] 13 | # The error members that belong to this specific error. 14 | def initialize(err_members = []) 15 | super(err_members, item_type: JSONAPI::Document::Error::ErrorMember) 16 | end 17 | 18 | # #empyt? provided by super 19 | # #include provided by super 20 | 21 | # Add a error to the collection using it's name 22 | # @param error_mem [JSONAPI::Document::Error::ErrorMember] 23 | def add(error_mem) 24 | super(error_mem, &:name) 25 | end 26 | 27 | # Another way to call add 28 | # @param (see #add) 29 | def <<(error_mem) 30 | super(error_mem, &:name) 31 | end 32 | 33 | # #<< provided by super, but calls overriden #add 34 | # #each provided from super 35 | # #remove provided from super 36 | # #get provided by super 37 | # #keys provided by super 38 | # #size provided by super 39 | # #to_s provided by super 40 | 41 | # Represent an Error as a hash 42 | def to_h 43 | JSONAPI::Utility.to_h_collection(self) 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/document/error/error_member.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair' 4 | require 'easy/jsonapi/document/error' 5 | 6 | module JSONAPI 7 | class Document 8 | class Error < JSONAPI::NameValuePairCollection 9 | 10 | # An individual error member 11 | class ErrorMember < JSONAPI::NameValuePair 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/document/jsonapi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair_collection' 4 | require 'easy/jsonapi/document/jsonapi/jsonapi_member' # extension 5 | 6 | module JSONAPI 7 | class Document 8 | 9 | # The jsonapi top level member of a JSON:API document 10 | class Jsonapi < JSONAPI::NameValuePairCollection 11 | 12 | # @param jsonapi_member_arr [Array { 41 | links: @links.to_h, 42 | data: @data.to_h, 43 | meta: @meta.to_h 44 | } } 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/document/resource_id.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | class Document 5 | # A jsonapi resource identifier 6 | class ResourceId 7 | 8 | attr_accessor :type, :id 9 | 10 | # @param type [String | Symbol] The type of the resource identifier 11 | # @param id [String | Symbol] The id of the resource identifier 12 | def initialize(type:, id:) 13 | @type = type.to_s 14 | @id = id.to_s 15 | end 16 | 17 | # Represents ResourceId as a JSON parsable string 18 | def to_s 19 | "{ \"type\": \"#{@type}\", \"id\": \"#{@id}\" }" 20 | end 21 | 22 | # Represents ResourceID as a jsonapi hash 23 | def to_h 24 | { type: @type, id: @id } 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/exceptions/document_exceptions' 4 | require 'easy/jsonapi/exceptions/headers_exceptions' 5 | require 'easy/jsonapi/exceptions/naming_exceptions' 6 | require 'easy/jsonapi/exceptions/query_params_exceptions' 7 | require 'easy/jsonapi/exceptions/user_defined_exceptions' 8 | require 'easy/jsonapi/exceptions/json_parse_error' 9 | 10 | module JSONAPI 11 | # Namespace for the gem's Exceptions 12 | module Exceptions 13 | # Validates that the Query Parameters comply with the JSONAPI specification 14 | module QueryParamsExceptions 15 | end 16 | 17 | # Validates that Headers comply with the JSONAPI specification 18 | module HeadersExceptions 19 | end 20 | 21 | # Validates that the request or response document complies with the JSONAPI specification 22 | module DocumentExceptions 23 | end 24 | 25 | # Checking for JSONAPI naming rules compliance 26 | module NamingExceptions 27 | end 28 | 29 | # Checking for User Defined Exceptions 30 | module UserDefinedExceptions 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/exceptions/json_parse_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | module Exceptions 5 | 6 | # Error to raise when error found while parsing json 7 | class JSONParseError < StandardError 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/exceptions/naming_exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | module Exceptions 5 | 6 | # Checking for JSONAPI naming rules compliance 7 | module NamingExceptions 8 | 9 | # JSONAPI member names can only contain a-z, A-Z, 0-9, '-', '_', and the last two cannot be used 10 | # at the start or end of a member name. 11 | # @param name [String] The string to check for member name rule compliance 12 | # @return 13 | def self.check_member_constraints(name) 14 | name = name.to_s 15 | return 'Member names MUST contain at least one character' if name == '' 16 | unless (name =~ /[^a-zA-Z0-9_-]/).nil? 17 | return 'Member names MUST contain only the allowed characters: ' \ 18 | "a-z, A-Z, 0-9, '-', '_'" 19 | end 20 | unless (name[0] =~ /[-_]/).nil? && (name[-1] =~ /[-_]/).nil? 21 | return 'Member names MUST start and end with a globally allowed character' 22 | end 23 | nil 24 | end 25 | 26 | # JSONAPI implementation specific query parameters follow the same constraints as member names 27 | # with the additional requirement that they must also contain at least one non a-z character. 28 | # @param name [String] The string to check for 29 | def self.check_additional_constraints(name) 30 | name = name.to_s 31 | return nil unless (name =~ /[^a-z]/).nil? 32 | 'Implementation specific query parameters MUST contain at least one non a-z character' 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/exceptions/query_params_exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/exceptions/naming_exceptions' 4 | require 'easy/jsonapi/exceptions/user_defined_exceptions' 5 | 6 | module JSONAPI 7 | module Exceptions 8 | 9 | # Validates that the Query Parameters comply with the JSONAPI specification 10 | module QueryParamsExceptions 11 | 12 | # A more specific Standard Error to raise 13 | class InvalidQueryParameter < StandardError 14 | attr_accessor :status_code 15 | 16 | # Init w a status code, so that it can be accessed when rescuing an exception 17 | def initialize(status_code) 18 | @status_code = status_code 19 | super 20 | end 21 | end 22 | 23 | # The jsonapi specific query parameters. 24 | SPECIAL_QUERY_PARAMS = %i[include fields page sort filter].freeze 25 | 26 | # Checks to see if the query paramaters conform to the JSONAPI spec and raises InvalidQueryParameter 27 | # if any parts are found to be non compliant 28 | # @param rack_req_params [Hash] The hash of the query parameters given by Rack::Request 29 | def self.check_compliance(rack_req_params, config_manager = nil, opts = {}) 30 | impl_spec_names = rack_req_params.keys - %w[include fields page sort filter] 31 | impl_spec_names.each do |name| 32 | check_param_name(name) 33 | end 34 | 35 | err_msg = JSONAPI::Exceptions::UserDefinedExceptions.check_user_query_param_requirements(rack_req_params, config_manager, opts) 36 | raise err_msg unless err_msg.nil? 37 | 38 | nil 39 | end 40 | 41 | # Checks an implementation specific param name to see if it complies to the spec. 42 | def self.check_param_name(name) 43 | should_return = 44 | NamingExceptions.check_member_constraints(name).nil? && \ 45 | NamingExceptions.check_additional_constraints(name).nil? && \ 46 | !name.include?('-') 47 | return if should_return 48 | 49 | raise_error( 50 | 'Implementation specific query parameters MUST adhere to the same constraints ' \ 51 | 'as member names. Allowed characters are: a-z, A-Z, 0-9 for beginning, middle, or end characters, ' \ 52 | "and '_' is allowed for middle characters. (While the JSON:API spec also allows '-', it is not " \ 53 | 'recommended, and thus is prohibited in this implementation). ' \ 54 | 'Implementation specific query members MUST contain at least one non a-z character as well. ' \ 55 | "Param name given: \"#{name}\"" 56 | ) 57 | end 58 | 59 | # @param msg [String] The message to raise InvalidQueryParameter with. 60 | def self.raise_error(msg, status_code = 400) 61 | raise InvalidQueryParameter.new(status_code), msg 62 | end 63 | 64 | private_class_method :raise_error 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/field.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/item' 4 | 5 | module JSONAPI 6 | # Field is the name of key value pair 7 | class Field < JSONAPI::Item 8 | 9 | # @param name [String] The name of the field 10 | # @param type [String | nil] The type of the field 11 | def initialize(name, type: String) 12 | super({ name: name.to_s, type: type }) 13 | end 14 | 15 | # @return [String] The Field's name 16 | def name 17 | @item[:name] 18 | end 19 | 20 | # @raise RunTimeError You shoulddn't be able to update the name of a 21 | # Resource::Field 22 | def name=(_) 23 | raise 'Cannot change the name of a Resource::Field' 24 | end 25 | 26 | # @return [Object] The type of the field 27 | def type 28 | @item[:type] 29 | end 30 | 31 | # @param new_type [Object] The new type of field. 32 | def type=(new_type) 33 | @item[:type] = new_type 34 | end 35 | 36 | # @return [String] The name of the field. 37 | def to_s 38 | name 39 | end 40 | 41 | private :method_missing, :item, :item= 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/header_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair_collection' 4 | 5 | module JSONAPI 6 | # header_collection # { include: Include, sort: Sort, filter: Filter } 7 | class HeaderCollection < JSONAPI::NameValuePairCollection 8 | 9 | # Initialize as empty if a array of Header objects not passed to it. 10 | # @param header_arr [JSONAPI::HeaderCollection::Header] The array of Header objects that can be used to init 11 | # a Header collection 12 | # @return JSONAPI::HeaderCollection 13 | def initialize(header_arr = []) 14 | super(header_arr, item_type: JSONAPI::HeaderCollection::Header) 15 | end 16 | 17 | # Add a header to the collection. (CASE-INSENSITIVE). 18 | # @param header [JSONAPI::HeaderCollection::Header] The header to add 19 | def add(header) 20 | super(header) { |hdr| hdr.name.downcase.gsub(/-/, '_') } 21 | end 22 | 23 | # Call super's get but make it case insensitive 24 | # @param key [Symbol] The hash key associated with a header 25 | def get(key) 26 | super(key.to_s.downcase.gsub(/-/, '_')) 27 | end 28 | 29 | # #empyt? provided by super 30 | # #include provided by super 31 | # add provided by super 32 | # #each provided from super 33 | # #remove provided from super 34 | # #keys provided by super 35 | # #size provided by super 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/header_collection/header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/header_collection' 4 | 5 | module JSONAPI 6 | class HeaderCollection 7 | # A http request or response header 8 | class Header < JSONAPI::NameValuePair 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | 5 | # Models a Item's key -> value relationship 6 | class Item 7 | 8 | # @return the value of an Item 9 | attr_accessor :item 10 | 11 | # Able to take a hash and dynamically create instance variables using the hash keys 12 | # Ex: obj == { :name => 'fields', :value => {'articles' => 'title,body,author', 'people' => 'name' }} 13 | # @param obj [Object] Can be anything, but if a hash is provided, dynamic instance variable can be created 14 | # upon trying to access them. 15 | def initialize(obj) 16 | if obj.is_a? Hash 17 | ensure_keys_are_sym(obj) 18 | end 19 | @item = obj 20 | end 21 | 22 | # A special to_string method if @item is a hash. 23 | def to_s 24 | return @item.to_s unless @item.is_a? Hash 25 | tr = '{ ' 26 | first = true 27 | @item.each do |k, v| 28 | if first 29 | first = false 30 | tr += "\"#{k}\": \"#{v}\", " 31 | else 32 | tr += "\"#{k}\": \"#{v}\"" 33 | end 34 | end 35 | tr += ' }' 36 | end 37 | 38 | # Represent item as a hash 39 | def to_h 40 | @item.to_h 41 | end 42 | 43 | private 44 | 45 | # Only used if implementing Item directly. 46 | # dynamically creates accessor methods for instance variables 47 | # created in the initialize 48 | def method_missing(method_name, *args, &block) 49 | return super unless is_a? JSONAPI::Item 50 | return super unless @item.is_a? Hash 51 | if should_update_var?(method_name) 52 | @item[method_name[0..-2].to_sym] = args[0] 53 | elsif should_get_var?(method_name) 54 | @item[method_name] 55 | else 56 | super 57 | end 58 | end 59 | 60 | # Needed when using #method_missing 61 | def respond_to_missing?(method_name, *args) 62 | instance_variables.include?("@#{method_name}".to_sym) || super 63 | end 64 | 65 | # Ensures that hash keys are symbol (and not String) when passing a hash to item. 66 | # @param obj [Object] A hash that can represent an item. 67 | def ensure_keys_are_sym(obj) 68 | obj.each_key do |k| 69 | raise "All keys must be Symbols. '#{k}' was #{k.class}" unless k.is_a? Symbol 70 | end 71 | end 72 | 73 | # Checks to see if the method name has a '=' at the end and if the 74 | # prefix before the '=' has the same name as an existing instance 75 | # variable. 76 | # @param (see #method_missing) 77 | def should_update_var?(method_name) 78 | method_name.to_s[-1] == '=' && @item[method_name[0..-2].to_sym].nil? == false 79 | end 80 | 81 | # Checks to see if the method has the same name as an existing instance 82 | # variable 83 | # @param (see #method_missing) 84 | def should_get_var?(method_name) 85 | @item[method_name].nil? == false 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/name_value_pair.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/item' 4 | require 'easy/jsonapi/utility' 5 | 6 | module JSONAPI 7 | # A generic name->value query pair 8 | class NameValuePair < JSONAPI::Item 9 | 10 | # @param name The name of the pair 11 | # @param value The value of the pair 12 | def initialize(name, value) 13 | name = name.to_s.gsub('-', '_') 14 | super({ name: name.to_s, value: value }) 15 | end 16 | 17 | # @return [String] The name of the name->val pair 18 | def name 19 | @item[:name] 20 | end 21 | 22 | # @raise RunTimeError You shouldn't be able to update the name of a 23 | # NameValuePair 24 | def name=(_) 25 | raise 'Cannot change the name of NameValuePair Objects' 26 | end 27 | 28 | # @return [String] The value of the name->val pair 29 | def value 30 | @item[:value] 31 | end 32 | 33 | # @param new_value [String | Symbol] The name->val pair value 34 | def value=(new_value) 35 | @item[:value] = new_value 36 | end 37 | 38 | # Represents a pair as a string 39 | def to_s 40 | v = value 41 | val_str = case v 42 | when Array 43 | val_str = '[' 44 | first = true 45 | v.each do |val| 46 | if first 47 | val_str += "\"#{val}\"" 48 | first = false 49 | else 50 | val_str += ", \"#{val}\"" 51 | end 52 | end 53 | val_str += ']' 54 | when String 55 | "\"#{v}\"" 56 | when JSONAPI::NameValuePair 57 | "{ #{v} }" 58 | else 59 | v 60 | end 61 | "\"#{name}\": #{val_str}" 62 | end 63 | 64 | # Represents a pair as a hash 65 | def to_h 66 | { name.to_sym => JSONAPI::Utility.to_h_value(value) } 67 | end 68 | 69 | # prevent users and sublcasses from accessing Parent's #method_missing 70 | private :method_missing, :item, :item= 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/name_value_pair_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/collection' 4 | require 'easy/jsonapi/name_value_pair' 5 | require 'easy/jsonapi/utility' 6 | 7 | module JSONAPI 8 | 9 | # Collection of Items that all have names and values. 10 | class NameValuePairCollection < JSONAPI::Collection 11 | 12 | # Creates an empty collection by default 13 | # @param pair_arr [Array] The pairs to be initialized with. 14 | def initialize(pair_arr = [], item_type: JSONAPI::NameValuePair, &block) 15 | if block_given? 16 | super(pair_arr, item_type: item_type, &block) 17 | else 18 | super(pair_arr, item_type: item_type, &:name) 19 | end 20 | end 21 | 22 | # #empyt? provided by super 23 | # #include provided by super 24 | 25 | # Add a pair to the collection. (CASE-SENSITIVE) 26 | # @param pair [JSONAPI::NameValuePair] The pair to add 27 | def add(pair, &block) 28 | if block_given? 29 | super(pair, &block) 30 | else 31 | super(pair, &:name) 32 | end 33 | end 34 | 35 | # Another way to add a query_param 36 | # @oaram (see #add) 37 | def <<(pair, &block) 38 | add(pair, &block) 39 | end 40 | 41 | # #each provided from super 42 | # #remove provided from super 43 | # #get provided by super 44 | # #keys provided by super 45 | # #size provided by super 46 | 47 | # Represent the collection as a string 48 | # @return [String] The representation of the collection 49 | def to_s 50 | JSONAPI::Utility.to_string_collection(self, pre_string: '{ ', post_string: ' }') 51 | end 52 | 53 | # Represent the collection as a hash 54 | # @return [Hash] The representation of the collection 55 | def to_h 56 | JSONAPI::Utility.to_h_collection(self) 57 | end 58 | 59 | protected :insert 60 | 61 | private 62 | 63 | # Gets the NameValuePair object value whose name matches the method_name called 64 | # @param method_name [Symbol] The name of the method called 65 | # @param args If any arguments were passed to the method called 66 | # @param block If a block was passed to the method called 67 | def method_missing(method_name, *args, &block) 68 | super unless include?(method_name) 69 | get(method_name).value 70 | end 71 | 72 | # Whether or not method missing should be called. 73 | def respond_to_missing?(method_name, *) 74 | include?(method_name) || super 75 | end 76 | 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/parser/rack_req_params_parser' 4 | require 'easy/jsonapi/parser/headers_parser' 5 | require 'easy/jsonapi/parser/document_parser' 6 | require 'easy/jsonapi/request' 7 | 8 | require 'rack' 9 | 10 | module JSONAPI 11 | # Parsing logic in rack middleware 12 | module Parser 13 | # @param env [Hash] The rack envirornment hash 14 | # @return [JSONAPI::Request] the instantiated jsonapi request object 15 | def self.parse_request(env) 16 | req = Rack::Request.new(env) 17 | 18 | query_param_collection = RackReqParamsParser.parse(req.GET) 19 | header_collection = HeadersParser.parse(env) 20 | 21 | req_body = req.body.read # stored separately because can only read 1x 22 | req.body.rewind # rewind incase something else needs to read the body of the request 23 | document = includes_jsonapi_document?(env) ? DocumentParser.parse(req_body) : nil 24 | 25 | JSONAPI::Request.new(env, query_param_collection, header_collection, document) 26 | end 27 | 28 | # Is the content type jsonapi? 29 | # @param (see #parse_request) 30 | # @return [TrueClass | FalseClass] 31 | def self.includes_jsonapi_document?(env) 32 | env['CONTENT_TYPE'] == 'application/vnd.api+json' && 33 | env['REQUEST_METHOD'] != 'GET' 34 | end 35 | 36 | private_class_method :includes_jsonapi_document? 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/parser/headers_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/collection' 4 | require 'easy/jsonapi/header_collection' 5 | 6 | require 'easy/jsonapi/item' 7 | require 'easy/jsonapi/header_collection/header' 8 | 9 | require 'easy/jsonapi/exceptions/headers_exceptions' 10 | 11 | module JSONAPI 12 | module Parser 13 | 14 | # Header parsing logic 15 | module HeadersParser 16 | 17 | # @param env [Hash] The rack envirornment hash 18 | # @return [JSONAPI::HeaderCollection] The collection of parsed header objects 19 | def self.parse(env) 20 | h_collection = JSONAPI::HeaderCollection.new 21 | env.each_key do |k| 22 | if k.start_with?('HTTP_') && (k != 'HTTP_VERSION') 23 | h_collection << JSONAPI::HeaderCollection::Header.new(k[5..-1], env[k]) 24 | elsif k == 'CONTENT_TYPE' 25 | h_collection << JSONAPI::HeaderCollection::Header.new(k, env[k]) 26 | end 27 | end 28 | h_collection 29 | end 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/parser/json_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'oj' 4 | require 'easy/jsonapi/exceptions/json_parse_error' 5 | 6 | module JSONAPI 7 | module Parser 8 | # A wrapper class for OJ parser 9 | module JSONParser 10 | 11 | # Parse JSON string into a ruby hash 12 | # @param document [String] The JSON string to parse 13 | # @raise [JSONAPI::Exceptions::JSONParseError] 14 | def self.parse(document, symbol_keys: true) 15 | Oj.load(document, symbol_keys: symbol_keys) 16 | 17 | rescue Oj::ParseError => e 18 | raise JSONAPI::Exceptions::JSONParseError, e.message 19 | end 20 | 21 | # Convert ruby hash into JSON 22 | # @param ruby_hash [Hash] THe hash to convert into JSON 23 | def self.dump(ruby_hash) 24 | Oj.dump(ruby_hash, mode: :compat) 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/parser/rack_req_params_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/exceptions/query_params_exceptions' 4 | 5 | require 'easy/jsonapi/request/query_param_collection' 6 | 7 | require 'easy/jsonapi/request/query_param_collection/query_param' 8 | 9 | require 'easy/jsonapi/request/query_param_collection/filter_param' 10 | require 'easy/jsonapi/request/query_param_collection/filter_param/filter' 11 | 12 | require 'easy/jsonapi/request/query_param_collection/include_param' 13 | require 'easy/jsonapi/request/query_param_collection/page_param' 14 | require 'easy/jsonapi/request/query_param_collection/sort_param' 15 | 16 | require 'easy/jsonapi/request/query_param_collection/fields_param' 17 | require 'easy/jsonapi/request/query_param_collection/fields_param/fieldset' 18 | 19 | require 'easy/jsonapi/field' 20 | 21 | module JSONAPI 22 | module Parser 23 | 24 | # Used to parse the request params given from the Rack::Request object 25 | module RackReqParamsParser 26 | 27 | # @param rack_req_params [Hash] The parameter hash returned from Rack::Request.params 28 | # @return [JSONAPI::Request::QueryParamCollection] 29 | def self.parse(rack_req_params) 30 | 31 | # rack::request.params: (string keys) 32 | # { 33 | # 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 34 | # 'include' => 'author,comments-likers,comments.users', 35 | # 'josh_ua' => 'demoss,simpson', 36 | # 'page' => { 'offset' => '5', 'limit' => '20' }, 37 | # 'filter' => { 'comments' => '(author/age > 21)', 'users' => '(age < 15)' }, 38 | # 'sort' => 'age,title' 39 | # } 40 | 41 | query_param_collection = JSONAPI::Request::QueryParamCollection.new 42 | rack_req_params.each do |name, value| 43 | add_the_param(name, value, query_param_collection) 44 | end 45 | query_param_collection 46 | end 47 | 48 | class << self 49 | private 50 | 51 | # @param name [String] The name of the query param to add 52 | # @param value [String | Hash] The value of the query param to add 53 | # @param query_param_collection [JSONAPI::Request::QueryParamCollection] 54 | # The collection to add all the params to 55 | def add_the_param(name, value, query_param_collection) 56 | case name 57 | when 'include' 58 | query_param_collection.add(parse_include_param(value)) 59 | when 'fields' 60 | query_param_collection.add(parse_fields_param(value)) 61 | when 'sort' 62 | query_param_collection.add(parse_sort_param(value)) 63 | when 'page' 64 | query_param_collection.add(parse_page_param(value)) 65 | when 'filter' 66 | query_param_collection.add(parse_filter_param(value)) 67 | else 68 | query_param_collection.add(parse_query_param(name, value)) 69 | end 70 | end 71 | 72 | # @param value [String] The value to initialize with 73 | def parse_include_param(value) 74 | includes_arr = value.split(',') 75 | JSONAPI::Request::QueryParamCollection::IncludeParam.new(includes_arr) 76 | end 77 | 78 | # @param (see #parse_include_param) 79 | def parse_fields_param(value) 80 | fieldsets = [] 81 | value.each do |res_type, res_field_str| 82 | res_field_str_arr = res_field_str.split(',') 83 | res_field_arr = res_field_str_arr.map { |res_field| JSONAPI::Field.new(res_field) } 84 | fieldsets << JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new(res_type, res_field_arr) 85 | end 86 | JSONAPI::Request::QueryParamCollection::FieldsParam.new(fieldsets) 87 | end 88 | 89 | # @param (see #parse_include_param) 90 | def parse_sort_param(value) 91 | res_field_arr = value.split(',').map { |res_field| JSONAPI::Field.new(res_field) } 92 | JSONAPI::Request::QueryParamCollection::SortParam.new(res_field_arr) 93 | end 94 | 95 | # @param (see #parse_include_param) 96 | def parse_page_param(value) 97 | JSONAPI::Request::QueryParamCollection::PageParam.new(offset: value[:offset], limit: value[:limit]) 98 | end 99 | 100 | # @param (see #parse_include_param) 101 | def parse_filter_param(value) 102 | 103 | filter_arr = value.map do |res_name, filter| 104 | JSONAPI::Request::QueryParamCollection::FilterParam::Filter.new(res_name, filter) 105 | end 106 | JSONAPI::Request::QueryParamCollection::FilterParam.new(filter_arr) 107 | end 108 | 109 | # @param (see #parse_include_param) 110 | def parse_query_param(name, value) 111 | JSONAPI::Request::QueryParamCollection::QueryParam.new(name, value) 112 | end 113 | 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JSONAPI 4 | # Contains all objects relating to a HTTP request 5 | class Request 6 | attr_reader :path, :http_method, :host, :port, :query_string, :params, :headers, :body 7 | 8 | # @param env The rack envirornment hash 9 | # @param query_param_collection [QueryParamCollection] The already initialized QueryParamCollection class 10 | # @param header_collection [HeaderCollection] The already initialized HeaderCollection class 11 | # @param document [Document] The already initialized Document class 12 | def initialize(env, query_param_collection, header_collection, document) 13 | # from env hash 14 | @path = env['REQUEST_PATH'] 15 | @http_method = env['REQUEST_METHOD'] 16 | @host = env['SERVER_NAME'] 17 | @port = env['SERVER_PORT'].to_i 18 | @query_string = env['QUERY_STRING'] 19 | 20 | # parsed objects 21 | @params = query_param_collection 22 | @headers = header_collection 23 | @body = document 24 | end 25 | 26 | # Simple representation of a request object. 27 | def to_s 28 | "Quick Access Methods:\n\n" \ 29 | "\tpath: #{@path}\n" \ 30 | "\thttp: #{@http}\n" \ 31 | "\thost: #{@host}\n" \ 32 | "\tport: #{@port}\n" \ 33 | "\tquery_string: #{@query_string}\n\n" \ 34 | "Accessing main sections of request:\n\n" \ 35 | "\tparams: #{@params}\n" \ 36 | "\theaders: #{@headers}\n" \ 37 | "\tbody: #{@body}" \ 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair_collection' 4 | require 'easy/jsonapi/utility' 5 | 6 | module JSONAPI 7 | class Request 8 | # A collection of QueryParam objects 9 | class QueryParamCollection < JSONAPI::NameValuePairCollection 10 | 11 | # The special query params defined by the JSON:API specification 12 | SPECIAL_QUERY_PARAMS = %i[sorts filters fields page includes].freeze 13 | 14 | # @param param_arr [Array] 12 | # The array of fieldsets found in the query string. Ex: fields[resource]=res_field1,res_field2 13 | def initialize(fieldset_arr) 14 | super('fields', fieldset_arr) 15 | end 16 | 17 | # Alias to parent #value method 18 | # @return [Array] 19 | def fieldsets 20 | value 21 | end 22 | 23 | # @return The the query string representation of the included fieldsets 24 | # ex: "#{fieldset1.to_s}&{fieldset2.to_s}&..." 25 | def to_s 26 | JSONAPI::Utility.to_string_collection(value, delimiter: '&') 27 | end 28 | 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/fields_param/fieldset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair_collection' 4 | require 'easy/jsonapi/request/query_param_collection/fields_param' 5 | require 'easy/jsonapi/utility' 6 | 7 | module JSONAPI 8 | class Request 9 | class QueryParamCollection < NameValuePairCollection 10 | class FieldsParam < QueryParam 11 | # Collection of fields related to specific resource objects 12 | class Fieldset 13 | 14 | attr_reader :resource_type, :fields 15 | 16 | # @param field_arr [Array] 17 | # A fieldset is a collection of Resource Fields 18 | def initialize(resource_type, field_arr = []) 19 | @resource_type = resource_type 20 | @fields = field_arr 21 | end 22 | 23 | # Represention of Fieldset as a string where fields 24 | # are comma separated strings 25 | def to_s 26 | pre_string = "fields[#{@resource_type}]=" 27 | JSONAPI::Utility.to_string_collection(@fields, delimiter: ',', pre_string: pre_string) 28 | end 29 | 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/filter_param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/query_param' 4 | require 'easy/jsonapi/utility' 5 | 6 | module JSONAPI 7 | class Request 8 | class QueryParamCollection < JSONAPI::NameValuePairCollection 9 | 10 | # Used to create a unique Filter JSONAPI::Request::QueryParamCollection::QueryParam 11 | class FilterParam < QueryParam 12 | 13 | # @param filter_arr [Array] 14 | # The array of filters included in the query string. Ex: filter[articles]=(posted_date == today) 15 | def initialize(filter_arr) 16 | super('filters', filter_arr) 17 | end 18 | 19 | # Represent each filter separated by a & value 20 | # Ex: "#{filter1.to_s}&{filter2.to_s}&..." 21 | def to_s 22 | JSONAPI::Utility.to_string_collection(value, delimiter: '&') 23 | end 24 | 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/filter_param/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/filter_param' 4 | 5 | module JSONAPI 6 | class Request 7 | class QueryParamCollection < NameValuePairCollection 8 | class FilterParam < QueryParam 9 | # Represents an individual Filtering scheme for the filter query param(s) used. 10 | class Filter 11 | 12 | attr_reader :resource_type, :filter 13 | 14 | # @param resource_type [String] The type to filter 15 | # @param filter [String] The filter algorithm 16 | def initialize(resource_type, filter) 17 | @resource_type = resource_type 18 | @filter = filter 19 | end 20 | 21 | # @return [String] The value of the filter 22 | def value 23 | @filter 24 | end 25 | 26 | # Represent filter as an individual filter query param 27 | def to_s 28 | "filter[#{@resource_type}]=#{@filter}" 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/include_param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/query_param' 4 | require 'easy/jsonapi/utility' 5 | 6 | module JSONAPI 7 | class Request 8 | class QueryParamCollection < JSONAPI::NameValuePairCollection 9 | # The include query param 10 | class IncludeParam < QueryParam 11 | 12 | 13 | # @param includes_arr [Array] An array with each individual query include 14 | # Ex: incude=author,people => ['author', 'people'] 15 | def initialize(includes_arr) 16 | includes_hash_structure = store_includes(includes_arr) 17 | super('includes', includes_hash_structure) 18 | end 19 | 20 | # to string 21 | def to_s 22 | "include=#{stringify_includes_hash(value)}" 23 | end 24 | 25 | private 26 | 27 | # Represent include internal hash as query string 28 | # @param includes_hash [Hash] The internal structure 29 | def stringify_includes_hash(includes_hash) 30 | to_return = '' 31 | first = true 32 | includes_hash.each do |mem_name, mem_hash| 33 | if first 34 | to_return += to_s_mem(mem_name, mem_hash) 35 | first = false 36 | else 37 | to_return += ",#{to_s_mem(mem_name, mem_hash)}" 38 | end 39 | end 40 | to_return 41 | end 42 | 43 | # Depending on the delimiter stringify differently. 44 | # @param mem_name [Symbol] The name of the member to stringify 45 | # @param mem_hash [Hash] The information about that member 46 | def to_s_mem(mem_name, mem_hash) 47 | if mem_hash[:relationships] == {} 48 | mem_name.to_s 49 | else 50 | delimiter = mem_hash[:included] == true ? '.' : '-' 51 | prefix = "#{mem_name}#{delimiter}" 52 | to_return = '' 53 | first = true 54 | mem_hash[:relationships].each do |m_name, m_hash| 55 | if first 56 | to_return += "#{prefix}#{to_s_mem(m_name, m_hash)}" 57 | first = false 58 | else 59 | to_return += ",#{prefix}#{to_s_mem(m_name, m_hash)}" 60 | end 61 | end 62 | to_return 63 | end 64 | end 65 | 66 | # Helper for #initialize 67 | # @param includes_arr [Array] The array of includes to store 68 | def store_includes(includes_arr) 69 | incl_hash = {} 70 | includes_arr.each do |include_str| 71 | include_str_arr = include_str.scan(/\w+|-|\./) # split into array (word, -, or .) 72 | store_include(incl_hash, include_str_arr) 73 | end 74 | incl_hash 75 | end 76 | 77 | # @param loc_in_h [Hash] The location within the main hash 78 | # @param i_arr [Array] The array of include strings 79 | def store_include(loc_in_h, i_arr) 80 | res_name = i_arr[0].to_sym 81 | if i_arr.length == 1 82 | add_member(loc_in_h, res_name, included: true) 83 | else 84 | add_member(loc_in_h, res_name, included: res_included?(i_arr)) 85 | store_include(loc_in_h[res_name][:relationships], i_arr[2..-1]) 86 | end 87 | end 88 | 89 | # @param (see #store_include) 90 | def res_included?(i_arr) 91 | delim = i_arr[1] 92 | case delim 93 | when '.' 94 | true 95 | when '-' 96 | false 97 | else 98 | raise 'Syntax Error in include query string query param' 99 | end 100 | end 101 | 102 | # @param loc_in_h [Hash] The location within the main hash 103 | # @param res_name [Symbol] The name of the resource 104 | # @param included [TrueClass | FalseClass] Whether or not a resource 105 | # is being requested or not 106 | def add_member(loc_in_h, res_name, included:) 107 | if loc_in_h.key?(res_name) 108 | loc_in_h[res_name][:included] = included unless included == false 109 | else 110 | loc_in_h[res_name] = { 111 | included: included, 112 | relationships: {} 113 | } 114 | end 115 | end 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/page_param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/query_param' 4 | 5 | module JSONAPI 6 | class Request 7 | class QueryParamCollection < JSONAPI::NameValuePairCollection 8 | # Used to create a unique Page JSONAPI::Request::QueryParamCollection::QueryParam 9 | class PageParam < QueryParam 10 | 11 | # @param offset [Integer | String] the page offset 12 | # @param limit [Integer | String] the # of resources returned on a given page 13 | def initialize(offset:, limit:) 14 | super('page', { offset: offset.to_i, limit: limit.to_i }) 15 | end 16 | 17 | # @raise [RuntimeError] Informs user to use a different method 18 | def value 19 | raise 'PageParam does not provide a #value method, try #offset or #limit instead' 20 | end 21 | 22 | # @raise [RuntimeError] Informs user to use a different method 23 | def value=(_) 24 | raise 'PageParam does not provide a #value= method, try #offset= or #limit= instead' 25 | end 26 | 27 | # @return [Integer] The page offset 28 | def offset 29 | @item[:value][:offset] 30 | end 31 | 32 | # @param new_offset [Integer | String] The new page offset number 33 | def offset=(new_offset) 34 | @item[:value][:offset] = new_offset.to_i 35 | end 36 | 37 | # @return [Integer] The # of resources returned on a given page 38 | def limit 39 | @item[:value][:limit] 40 | end 41 | 42 | # @param new_limit [Integer] The new page limit number 43 | def limit=(new_limit) 44 | @item[:value][:limit] = new_limit.to_i 45 | end 46 | 47 | # Represents the Page class in a string format 48 | def to_s 49 | "page[offset]=#{offset}&page[limit]=#{limit}" 50 | end 51 | 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/query_param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection' 4 | require 'easy/jsonapi/name_value_pair' 5 | require 'easy/jsonapi/exceptions/query_params_exceptions' 6 | require 'easy/jsonapi/utility' 7 | 8 | 9 | module JSONAPI 10 | class Request 11 | class QueryParamCollection < JSONAPI::NameValuePairCollection 12 | # A generic name=value query parameter 13 | class QueryParam < JSONAPI::NameValuePair 14 | 15 | # @param name [String] The name of the parameter 16 | # @param value [String | Array] The value of the parameter 17 | def initialize(name, value) 18 | if instance_of?(QueryParam) 19 | JSONAPI::Exceptions::QueryParamsExceptions.check_param_name(name) 20 | end 21 | value = value.split(',') if value.is_a? String 22 | super(name, value) 23 | end 24 | 25 | # Update the query_param value, turning value into an array if it was given as a string 26 | # @param new_value [String, Array] The new value of the Parameter 27 | def value=(new_value) 28 | new_value = new_value.split(',') if new_value.is_a? String 29 | super(new_value) 30 | end 31 | 32 | # Represents a parameter as a string 33 | def to_s 34 | "#{name}=#{JSONAPI::Utility.to_string_collection(value, delimiter: ',')}" 35 | end 36 | 37 | # @raise RuntimeError Cannot change the name of a QueryParam object 38 | def name=(_) 39 | raise 'Cannot change the name of QueryParam Objects' 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | # include=author,comments-likers,comments.likes 47 | # author comments-likers comments.likes 48 | -------------------------------------------------------------------------------- /lib/easy/jsonapi/request/query_param_collection/sort_param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/query_param' 4 | 5 | module JSONAPI 6 | class Request 7 | class QueryParamCollection < JSONAPI::NameValuePairCollection 8 | # Used to create a unique Sort JSONAPI::Request::QueryParamCollection::QueryParam 9 | class SortParam < QueryParam 10 | 11 | # @param res_field_arr [Array 'author,comments.author', 11 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 12 | 'test' => 'ing', 13 | 'page' => { 'offset' => '1', 'limit' => '1' } 14 | } 15 | end 16 | 17 | # A hash should pass 18 | let(:rpa) do 19 | { 20 | 'include' => 'author,comments.author', 21 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 22 | 'testTest' => 'ing', 23 | 'page' => { 'offset' => '1', 'limit' => '1' } 24 | } 25 | end 26 | 27 | # A hash that should pass 28 | let(:rpb) do 29 | { 30 | 'include' => 'author,comments.author', 31 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 32 | 'test_test' => 'ing', 33 | 'page' => { 'offset' => '1', 'limit' => '1' } 34 | } 35 | end 36 | 37 | # A hash that should pass 38 | let(:rpc) do 39 | { 40 | 'include' => 'author,comments.author', 41 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 42 | 'test1' => 'ing', 43 | 'page' => { 'offset' => '1', 'limit' => '1' } 44 | } 45 | end 46 | 47 | # A hash that should pass 48 | let(:no_impl_sp_p) do 49 | { 50 | 'include' => 'author,comments.author', 51 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 52 | 'page' => { 'offset' => '1', 'limit' => '1' } 53 | } 54 | end 55 | 56 | # A hash that should pass 57 | let(:only_impl_sp_p) do 58 | { 59 | 'test1' => 'ing', 60 | 'test2' => 'what?' 61 | } 62 | end 63 | 64 | # The error class to return 65 | let(:error_class) { JSONAPI::Exceptions::QueryParamsExceptions::InvalidQueryParameter } 66 | 67 | describe '#check_compliance!' do 68 | it 'should raise a runtime error when test is all lowercase' do 69 | expect { JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(rp) }.to raise_error error_class 70 | end 71 | 72 | it 'should return nil when test has a non a-z character' do 73 | expect(JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(rpa)).to be nil 74 | expect(JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(rpb)).to be nil 75 | expect(JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(rpc)).to be nil 76 | end 77 | 78 | it 'should work given an empty rack_req_params' do 79 | expect(JSONAPI::Exceptions::QueryParamsExceptions.check_compliance({})).to be nil 80 | end 81 | 82 | it 'should work given a rack_req_params w no impl specific params' do 83 | expect(JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(no_impl_sp_p)).to be nil 84 | end 85 | 86 | it 'should work given a rack_req_params w only impl specific params' do 87 | expect(JSONAPI::Exceptions::QueryParamsExceptions.check_compliance(only_impl_sp_p)).to be nil 88 | end 89 | 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/field_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/field' 4 | 5 | describe JSONAPI::Field do 6 | 7 | let(:f1) { JSONAPI::Field.new('number', type: Integer) } 8 | let(:f2) { JSONAPI::Field.new('title') } 9 | 10 | describe '#initialize' do 11 | 12 | it 'should provide proper accessor methods' do 13 | expect(f1.name).to eq 'number' 14 | expect(f1.type).to eq Integer 15 | 16 | expect(f2.name).to eq 'title' 17 | expect(f2.type).to eq String 18 | 19 | error_msg = 'Cannot change the name of a Resource::Field' 20 | expect { f1.name = 'body' }.to raise_error error_msg 21 | expect { f2.name = 'authors' }.to raise_error error_msg 22 | 23 | f1.type = String 24 | f2.type = Array 25 | expect(f1.type).to eq String 26 | expect(f2.type).to eq Array 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/header_collection/header_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/header_collection/header' 4 | require 'shared_examples/name_value_pair_tests' 5 | 6 | describe JSONAPI::HeaderCollection::Header do 7 | it_behaves_like 'name value pair tests' do 8 | let(:pair) { JSONAPI::HeaderCollection::Header.new(:name, 'value') } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/header_collection_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/header_collection' 4 | require 'easy/jsonapi/header_collection/header' 5 | require 'shared_examples/name_value_pair_collections' 6 | 7 | describe JSONAPI::HeaderCollection do 8 | let(:item_class) { JSONAPI::HeaderCollection::Header } 9 | obj_arr = [ 10 | { name: 'CONTENT_TYPE', value: 'application/vnd.api+json' }, 11 | { name: 'ACCEPT', value: 'application/vnd.api+json, text/plain, text/html ; level=1 ; q=0.5, text/x-div; q=0.8, text/x-c, */*' }, 12 | { name: 'HOST', value: 'localhost:9292' }, 13 | { name: 'CONNECTION', value: 'keep-alive' }, 14 | { name: 'WWW_AUTHENTICATE', value: 'Basic realm="Access to the staging site", charset="UTF-8"' } 15 | ] 16 | 17 | let(:c_size) { 5 } 18 | let(:keys) { %i[content_type accept host connection www_authenticate] } 19 | let(:ex_item_key) { :content_type } 20 | let(:ex_item) { JSONAPI::HeaderCollection::Header.new('content-type', 'application/vnd.api+json') } 21 | 22 | let(:to_string) do 23 | '{ ' \ 24 | "\"CONTENT_TYPE\": \"application/vnd.api+json\", " \ 25 | "\"ACCEPT\": \"application/vnd.api+json, text/plain, text/html ; level=1 ; q=0.5, text/x-div; q=0.8, text/x-c, */*\", " \ 26 | "\"HOST\": \"localhost:9292\", " \ 27 | "\"CONNECTION\": \"keep-alive\", " \ 28 | "\"WWW_AUTHENTICATE\": \"Basic realm=\"Access to the staging site\", charset=\"UTF-8\"\"" \ 29 | ' }' 30 | end 31 | 32 | item_arr = obj_arr.map do |i| 33 | JSONAPI::HeaderCollection::Header.new(i[:name], i[:value]) 34 | end 35 | let(:c) { JSONAPI::HeaderCollection.new(item_arr, &:name) } 36 | let(:ec) { JSONAPI::HeaderCollection.new } 37 | 38 | it_behaves_like 'name value pair collections' 39 | 40 | context 'when checking dynamic access methods' do 41 | it 'should retrieve the value of the object specified' do 42 | expect(c.content_type).to eq obj_arr[0][:value] 43 | expect(c.accept).to eq obj_arr[1][:value] 44 | expect(c.host).to eq obj_arr[2][:value] 45 | expect(c.connection).to eq obj_arr[3][:value] 46 | expect(c.www_authenticate).to eq obj_arr[4][:value] 47 | end 48 | end 49 | 50 | context 'when checking if items are case insensitive' do 51 | it 'should be able to retrieve the same item with different case names' do 52 | i1 = c.get('host') 53 | i2 = c.get('HOST') 54 | expect(i1).to eq i2 55 | i3 = c.get(:host) 56 | expect(i1).to eq i3 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/item_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/item' 4 | require 'shared_examples/item_shared_tests' 5 | 6 | describe JSONAPI::Item do 7 | it_behaves_like 'item shared tests' do 8 | let(:item) { JSONAPI::Item.new({ name: 'name', value: 'value' }) } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/name_value_pair_collection_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair_collection' 4 | require 'easy/jsonapi/name_value_pair' 5 | require 'shared_examples/name_value_pair_collections' 6 | 7 | describe JSONAPI::NameValuePairCollection do 8 | let(:item_class) { JSONAPI::NameValuePair } 9 | let(:c_size) { 5 } 10 | let(:keys) { %i[includes lebron charles michael kobe] } 11 | let(:ex_item_key) { :includes } 12 | let(:ex_item) { JSONAPI::NameValuePair.new(:includes, 'author,comments.likes') } 13 | 14 | let(:to_string) do 15 | '{ ' \ 16 | "\"includes\": \"author,comments.likes\", " \ 17 | "\"lebron\": \"james\", " \ 18 | "\"charles\": \"barkley\", " \ 19 | "\"michael\": \"jordan,jackson\", " \ 20 | "\"kobe\": \"bryant\"" \ 21 | ' }' 22 | end 23 | 24 | let(:to_hash) do 25 | { 26 | includes: 'author,comments.likes', 27 | lebron: 'james', 28 | charles: 'barkley', 29 | michael: 'jordan,jackson', 30 | kobe: 'bryant' 31 | } 32 | end 33 | 34 | obj_arr = { 35 | includes: 'author,comments.likes', 36 | lebron: 'james', 37 | charles: 'barkley', 38 | michael: 'jordan,jackson', 39 | kobe: 'bryant' 40 | } 41 | 42 | pair_arr = obj_arr.map { |k, v| JSONAPI::NameValuePair.new(k, v) } 43 | let(:c) { JSONAPI::NameValuePairCollection.new(pair_arr) } 44 | let(:ec) { JSONAPI::NameValuePairCollection.new } 45 | 46 | it_behaves_like 'name value pair collections' 47 | 48 | context 'when checking dynamic accessor methods' do 49 | it 'should be able to access items by their names' do 50 | expect(c.includes).to eq 'author,comments.likes' 51 | expect(c.lebron).to eq 'james' 52 | expect(c.charles).to eq 'barkley' 53 | expect(c.michael).to eq 'jordan,jackson' 54 | expect(c.kobe).to eq 'bryant' 55 | end 56 | end 57 | 58 | context 'when checking if items are case insensitive' do 59 | it 'should treat cases differently' do 60 | expect(c.get('includes').class).to eq JSONAPI::NameValuePair 61 | expect(c.get('INCLUDES').class).to eq NilClass 62 | end 63 | it 'should be symbol/string insensitive though' do 64 | i1 = c.get('includes') 65 | i3 = c.get(:includes) 66 | expect(i1).to eq i3 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/name_value_pair_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/name_value_pair' 4 | require 'shared_examples/name_value_pair_tests' 5 | 6 | describe JSONAPI::NameValuePair do 7 | it_behaves_like 'name value pair tests' do 8 | let(:pair) { JSONAPI::NameValuePair.new(:name, 'value') } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/parser/document_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/document' 4 | 5 | require 'easy/jsonapi/parser' 6 | require 'easy/jsonapi/parser/json_parser' 7 | require 'easy/jsonapi/parser/document_parser' 8 | 9 | require 'easy/jsonapi/exceptions/document_exceptions' 10 | 11 | 12 | describe JSONAPI::Parser::DocumentParser do 13 | 14 | doc_hash = 15 | { 16 | 'data' => { 17 | 'type' => 'articles', 18 | 'id' => '1', 19 | 'attributes' => { 'title' => 'JSON API paints my bikeshed!' }, 20 | 'links' => { 'self' => 'http://example.com/articles/1' }, 21 | 'relationships' => { 22 | 'author' => { 23 | 'links' => { 24 | 'self' => 'http://example.com/articles/1/relationships/author', 25 | 'related' => 'http://example.com/articles/1/author' 26 | }, 27 | 'data' => { 'type' => 'people', 'id' => '9' } 28 | }, 29 | 'journal' => { 30 | 'data' => nil 31 | }, 32 | 'comments' => { 33 | 'links' => { 34 | 'self' => 'http://example.com/articles/1/relationships/comments', 35 | 'related' => 'http://example.com/articles/1/comments' 36 | }, 37 | 'data' => [ 38 | { 'type' => 'comments', 'id' => '5' }, 39 | { 'type' => 'comments', 'id' => '12' } 40 | ] 41 | } 42 | } 43 | }, 44 | 'meta' => { 'count' => '13' }, 45 | 'links' => { 'self' => 'url' } 46 | } 47 | 48 | body = JSONAPI::Parser::JSONParser.dump(doc_hash) 49 | 50 | let(:document) { JSONAPI::Parser::DocumentParser.parse(body) } 51 | 52 | describe '#parse' do 53 | it 'should return nil if given a nil document' do 54 | expect(JSONAPI::Parser::DocumentParser.parse(nil)).to be nil 55 | end 56 | it 'should return a Document object given a valid jsonapi document' do 57 | expect(document.class).to eq JSONAPI::Document 58 | end 59 | 60 | it 'the document classes instance variables should associate w the proper class' do 61 | expect(document.data.class).to eq JSONAPI::Document::Resource 62 | expect(document.meta.class).to eq JSONAPI::Document::Meta 63 | expect(document.links.class).to eq JSONAPI::Document::Links 64 | end 65 | 66 | end 67 | 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/parser/header_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/parser' 4 | require 'easy/jsonapi/parser/headers_parser' 5 | 6 | require 'easy/jsonapi/item' 7 | require 'easy/jsonapi/header_collection/header' 8 | 9 | require 'easy/jsonapi/collection' 10 | require 'easy/jsonapi/header_collection' 11 | 12 | describe JSONAPI::Parser::HeadersParser do 13 | 14 | let(:env) do 15 | { 16 | 'SERVER_SOFTWARE' => 'thin 1.7.2 codename Bachmanity', 17 | 'SERVER_NAME' => 'localhost', 18 | 'rack.version' => [1, 0], 19 | 'rack.multithread' => false, 20 | 'rack.multiprocess' => false, 21 | 'rack.run_once' => false, 22 | 'REQUEST_METHOD' => 'POST', 23 | 'REQUEST_PATH' => '/articles', 24 | 'PATH_INFO' => '/articles', 25 | 'QUERY_STRING' => 'include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 26 | 'REQUEST_URI' => '/articles?include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 27 | 'HTTP_VERSION' => 'HTTP/1.1', 28 | 'HTTP_ACCEPT' => 'application/vnd.api+json ; q=0.5, text/*, image/* ; q=.3', 29 | 'HTTP_POSTMAN_TOKEN' => 'de878a8f-917e-4016-b9f7-f723a6483f03', 30 | 'HTTP_HOST' => 'localhost:9292', 31 | 'CONTENT_TYPE' => 'application/vnd.api+json', 32 | 'GATEWAY_INTERFACE' => 'CGI/1.2', 33 | 'SERVER_PORT' => '9292', 34 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 35 | 'rack.url_scheme' => 'http', 36 | 'SCRIPT_NAME' => '', 37 | 'REMOTE_ADDR' => '::1' 38 | } 39 | end 40 | 41 | let(:env_bad_content_type) do 42 | { 43 | 'REQUEST_METHOD' => 'POST', 44 | 'REQUEST_PATH' => '/articles', 45 | 'PATH_INFO' => '/articles', 46 | 'HTTP_ACCEPT' => 'application/vnd.api+json ; q=0.5, text/*, image/* ; q=.3', 47 | 'CONTENT_TYPE' => 'text/*' 48 | } 49 | end 50 | 51 | let(:hc) { JSONAPI::Parser::HeadersParser.parse(env) } 52 | 53 | describe '#parse!' do 54 | 55 | it 'should return a header collection' do 56 | expect(hc.class).to eq JSONAPI::HeaderCollection 57 | end 58 | 59 | it 'should have the right header names and values' do 60 | expect(hc.size).to eq 4 61 | expect(hc.include?(:host)).to be true 62 | expect(hc.include?(:accept)).to be true 63 | expect(hc.include?(:content_type)).to be true 64 | expect(hc.include?(:postman_token)).to be true 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/parser/json_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/exceptions' 4 | require 'easy/jsonapi/parser/json_parser' 5 | require 'oj' 6 | 7 | describe JSONAPI::Parser::JSONParser do 8 | let(:hash1) { { data: { type: 'person', id: '123' } } } 9 | 10 | describe '#parse' do 11 | it 'should parse valid json into a hash' do 12 | hash = {} 13 | expect(JSONAPI::Parser::JSONParser.parse('{}')).to eq hash 14 | expect(JSONAPI::Parser::JSONParser.parse('{ "data": { "type": "person", "id": "123" } }')).to eq hash1 15 | end 16 | 17 | it 'should raise JSONAPI::Exceptions::JSONParseError when invalid input' do 18 | err_class = JSONAPI::Exceptions::JSONParseError 19 | expect { JSONAPI::Parser::JSONParser.parse('{ase') }.to raise_error err_class 20 | end 21 | end 22 | 23 | describe '#dump' do 24 | it 'should return valid JSON from a given ruby hash' do 25 | expect(JSONAPI::Parser::JSONParser.dump(hash1)).to eq Oj.dump(hash1, mode: :compat) 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/parser/rack_req_params_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/parser/rack_req_params_parser' 4 | require 'easy/jsonapi/exceptions/query_params_exceptions' 5 | 6 | describe JSONAPI::Parser::RackReqParamsParser do 7 | 8 | before do 9 | rack_params = 10 | { 11 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 12 | 'include' => 'author,comments-likers,comments.users', 13 | 'josh_ua' => 'demoss,simpson', 14 | 'page' => { 'offset' => '5', 'limit' => '20' }, 15 | 'filter' => { 'comments' => '(author/age > 21)', 'users' => '(age < 15)' }, 16 | 'sort' => 'age,title' 17 | } 18 | 19 | @rack_params_w_bad_name = 20 | { 21 | 'fields' => { 'articles' => 'title,body,author', 'people' => 'name' }, 22 | 'include' => 'author,comments-likers,comments.users', 23 | 'joshua' => 'demoss,simpson', 24 | 'page' => { 'offset' => '5', 'limit' => '20' }, 25 | 'filter' => { 'comments' => '(author/age > 21)', 'users' => '(age < 15)' }, 26 | 'sort' => 'age,title' 27 | } 28 | 29 | @query_param_collection = JSONAPI::Parser::RackReqParamsParser.parse(rack_params) 30 | end 31 | 32 | # The query_param collection when the parser is passed params 33 | let(:pc) { @query_param_collection } 34 | 35 | # The query_param collection when the parser is passed an empty query_param object 36 | let(:epc) { JSONAPI::Parser::RackReqParamsParser.parse({}) } 37 | 38 | let(:e_class) { JSONAPI::Exceptions::QueryParamsExceptions::InvalidQueryParameter } 39 | 40 | describe '#parse' do 41 | it 'should return a QueryParamCollection object' do 42 | expect(pc.class).to eq JSONAPI::Request::QueryParamCollection 43 | end 44 | 45 | it 'should return an empty QueryParamCollection when no params given' do 46 | expect(epc.empty?).to be true 47 | end 48 | 49 | it 'should return a QueryParamCollection when params given' do 50 | expect(pc.empty?).to be false 51 | end 52 | 53 | it 'should include each added item' do 54 | expect(pc.include?(:fields)).to be true 55 | expect(pc.include?(:includes)).to be true 56 | expect(pc.include?(:josh_ua)).to be true 57 | expect(pc.include?(:page)).to be true 58 | expect(pc.include?(:filters)).to be true 59 | expect(pc.include?(:sorts)).to be true 60 | end 61 | 62 | it 'should contain proper classes for each item in the param collection' do 63 | expect(pc.get(:fields).class).to be JSONAPI::Request::QueryParamCollection::FieldsParam 64 | expect(pc.get(:includes).class).to be JSONAPI::Request::QueryParamCollection::IncludeParam 65 | expect(pc.get(:josh_ua).class).to be JSONAPI::Request::QueryParamCollection::QueryParam 66 | expect(pc.get(:page).class).to be JSONAPI::Request::QueryParamCollection::PageParam 67 | expect(pc.get(:filters).class).to be JSONAPI::Request::QueryParamCollection::FilterParam 68 | expect(pc.get(:sorts).class).to be JSONAPI::Request::QueryParamCollection::SortParam 69 | end 70 | 71 | it 'should raise InvalidQueryParameter if given a impl specific param that does not follow naming rules' do 72 | expect { JSONAPI::Parser::RackReqParamsParser.parse(@rack_params_w_bad_name) }.to raise_error e_class 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/parser' 4 | require 'easy/jsonapi/parser/json_parser' 5 | require 'easy/jsonapi/request' 6 | 7 | 8 | describe JSONAPI::Parser do 9 | 10 | describe '#parse_request!' do 11 | 12 | req_body_hash = { 13 | data: { 14 | type: "photos", 15 | id: "550e8400-e29b-41d4-a716-446655440000", 16 | attributes: { 17 | title: "Ember Hamster", 18 | src: "http://example.com/images/productivity.png" 19 | } 20 | } 21 | } 22 | 23 | req_body_str = JSONAPI::Parser::JSONParser.dump(req_body_hash) 24 | 25 | env = 26 | { 27 | 'SERVER_SOFTWARE' => 'thin 1.7.2 codename Bachmanity', 28 | 'SERVER_NAME' => 'localhost', 29 | "rack.input" => StringIO.new(req_body_str), 30 | 'rack.version' => [1, 0], 31 | 'rack.multithread' => false, 32 | 'rack.multiprocess' => false, 33 | 'rack.run_once' => false, 34 | 'REQUEST_METHOD' => 'POST', 35 | 'REQUEST_PATH' => '/articles', 36 | 'PATH_INFO' => '/articles', 37 | 'QUERY_STRING' => 'include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 38 | 'REQUEST_URI' => '/articles?include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 39 | 'HTTP_VERSION' => 'HTTP/1.1', 40 | 'HTTP_ACCEPT' => 'application/vnd.beta.curatess.v1.api+json ; q=0.5, text/*, image/* ; q=.3', 41 | 'HTTP_POSTMAN_TOKEN' => 'de878a8f-917e-4016-b9f7-f723a6483f03', 42 | 'HTTP_HOST' => 'localhost:9292', 43 | 'CONTENT_TYPE' => 'application/vnd.api+json', 44 | 'GATEWAY_INTERFACE' => 'CGI/1.2', 45 | 'SERVER_PORT' => '9292', 46 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 47 | 'rack.url_scheme' => 'http', 48 | 'SCRIPT_NAME' => '', 49 | 'REMOTE_ADDR' => '::1' 50 | } 51 | 52 | env_get_w_body = 53 | { 54 | "SERVER_SOFTWARE" => "thin 1.8.0 codename Possessed Pickle", 55 | "SERVER_NAME" => "localhost", 56 | "rack.input" => StringIO.new(req_body_str), 57 | "rack.version" => [1, 0], 58 | "rack.multithread" => true, 59 | "rack.multiprocess" => false, 60 | "rack.run_once" => false, 61 | "REQUEST_METHOD" => "GET", 62 | "REQUEST_PATH" => "/person/5632139873746944", 63 | "PATH_INFO" => "/person/5632139873746944", 64 | "REQUEST_URI" => "/person/5632139873746944", 65 | "HTTP_VERSION" => "HTTP/1.1", 66 | "HTTP_USER_AGENT" => "PostmanRuntime/7.26.8", 67 | "HTTP_ACCEPT" => "*/*", 68 | "HTTP_POSTMAN_TOKEN" => "ad8b44f0-8f24-43a3-a6d8-f4291929e00b", 69 | "HTTP_HOST" => "localhost:4567", 70 | "HTTP_ACCEPT_ENCODING" => "gzip, deflate, br", 71 | "HTTP_CONNECTION" => "keep-alive", 72 | "CONTENT_LENGTH" => "89", 73 | "CONTENT_TYPE" => "application/vnd.api+json", 74 | "GATEWAY_INTERFACE" => "CGI/1.2", 75 | "SERVER_PORT" => "4567", 76 | "QUERY_STRING" => "", 77 | "SERVER_PROTOCOL" => "HTTP/1.1", 78 | "rack.url_scheme" => "http", 79 | "SCRIPT_NAME" => "", 80 | "REMOTE_ADDR" => "::1", 81 | "sinatra.commonlogger" => true, 82 | "rack.request.query_string" => "", 83 | "rack.request.query_hash" => {}, 84 | "sinatra.route" => "GET /:type/:id/?" 85 | } 86 | 87 | env_no_body = 88 | { 89 | 'SERVER_SOFTWARE' => 'thin 1.7.2 codename Bachmanity', 90 | 'SERVER_NAME' => 'localhost', 91 | "rack.input" => StringIO.new, 92 | 'rack.version' => [1, 0], 93 | 'rack.multithread' => false, 94 | 'rack.multiprocess' => false, 95 | 'rack.run_once' => false, 96 | 'REQUEST_METHOD' => 'POST', 97 | 'REQUEST_PATH' => '/articles', 98 | 'PATH_INFO' => '/articles', 99 | 'QUERY_STRING' => 'include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 100 | 'REQUEST_URI' => '/articles?include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 101 | 'HTTP_VERSION' => 'HTTP/1.1', 102 | 'HTTP_ACCEPT' => 'application/vnd.beta.curatess.v1.api+json ; q=0.5, text/*, image/* ; q=.3', 103 | 'HTTP_POSTMAN_TOKEN' => 'de878a8f-917e-4016-b9f7-f723a6483f03', 104 | 'HTTP_HOST' => 'localhost:9292', 105 | 'GATEWAY_INTERFACE' => 'CGI/1.2', 106 | 'SERVER_PORT' => '9292', 107 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 108 | 'rack.url_scheme' => 'http', 109 | 'SCRIPT_NAME' => '', 110 | 'REMOTE_ADDR' => '::1' 111 | } 112 | 113 | req = JSONAPI::Parser.parse_request(env) 114 | req_get_w_body = JSONAPI::Parser.parse_request(env_get_w_body) 115 | req_no_body = JSONAPI::Parser.parse_request(env_no_body) 116 | 117 | let(:req) { req } 118 | let(:req_get_w_body) { req_get_w_body } 119 | 120 | let(:req_no_body) { req_no_body } 121 | 122 | 123 | 124 | it 'should return a Request object' do 125 | expect(req.class).to eq JSONAPI::Request 126 | expect(req_get_w_body.class).to eq JSONAPI::Request 127 | expect(req_no_body.class).to eq JSONAPI::Request 128 | end 129 | 130 | context 'when a jsonapi document is not included with the request' do 131 | it 'should return nil when accessing the request body' do 132 | expect(req_get_w_body.body).to eq nil 133 | expect(req_no_body.body).to eq nil 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/rack_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/parser' 4 | 5 | # Demo Rack App to test middleware class locally. 6 | class RackApp 7 | 8 | def call(env) 9 | status = 200 10 | headers = { "Content-Type" => "text/plain" } 11 | 12 | jsonapi_request = JSONAPI::Parser.parse_request(env) 13 | body = 14 | [ 15 | "Testing: #{jsonapi_request.class} | #{jsonapi_request.body.data.class}" 16 | ] 17 | [status, headers, body] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/fields_param/fieldset_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/fields_param/fieldset' 4 | require 'easy/jsonapi/field' 5 | 6 | describe JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset do 7 | 8 | let(:fieldset1) do 9 | JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new( 10 | 'articles', 11 | [ 12 | JSONAPI::Field.new('title'), 13 | JSONAPI::Field.new('body'), 14 | JSONAPI::Field.new('author') 15 | ] 16 | ) 17 | end 18 | 19 | let(:fieldset2) do 20 | JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new( 21 | 'people', 22 | [ 23 | JSONAPI::Field.new('name') 24 | ] 25 | ) 26 | end 27 | 28 | context 'when initializing' do 29 | it 'should have proper reader methods' do 30 | expect(fieldset1.resource_type).to eq 'articles' 31 | expect(fieldset2.resource_type).to eq 'people' 32 | 33 | expect(fieldset1.fields.size).to eq 3 34 | expect(fieldset2.fields.size).to eq 1 35 | end 36 | 37 | it 'should raise when trying to overwrite instance variables' do 38 | expect { fieldset1.resource_type = 'new_type' }.to raise_error NoMethodError 39 | expect { fieldset2.resource_type = 'new_type' }.to raise_error NoMethodError 40 | expect { fieldset1.fields = 'new_fields' }.to raise_error NoMethodError 41 | expect { fieldset2.fields = 'new_fields' }.to raise_error NoMethodError 42 | end 43 | end 44 | 45 | context 'when checking to_s' do 46 | it 'should represent an individual fields query string' do 47 | expect(fieldset1.to_s).to eq 'fields[articles]=title,body,author' 48 | expect(fieldset2.to_s).to eq 'fields[people]=name' 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/fields_param_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/fields_param' 4 | require 'easy/jsonapi/request/query_param_collection/fields_param/fieldset' 5 | require 'easy/jsonapi/field' 6 | 7 | require 'shared_examples/query_param_tests' 8 | 9 | describe JSONAPI::Request::QueryParamCollection::FieldsParam do 10 | 11 | let(:res_field_arr1) do 12 | [ 13 | JSONAPI::Field.new('title'), 14 | JSONAPI::Field.new('body') 15 | ] 16 | end 17 | 18 | let(:res_field_arr2) do 19 | [ 20 | JSONAPI::Field.new('name') 21 | ] 22 | end 23 | 24 | let(:fieldset1) { JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new('articles', res_field_arr1) } 25 | let(:fieldset2) { JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new('people', res_field_arr2) } 26 | 27 | # Used when checking #value= 28 | res_field = JSONAPI::Field.new('date') 29 | let(:fieldset3) { JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new('comments', res_field) } 30 | 31 | 32 | it_behaves_like 'query param tests' do 33 | let(:pair) { JSONAPI::Request::QueryParamCollection::FieldsParam.new([fieldset1, fieldset2]) } 34 | let(:name) { 'fields' } 35 | let(:value) { [fieldset1, fieldset2] } 36 | let(:new_value_input) { fieldset3 } 37 | let(:new_value) { [fieldset3] } 38 | let(:to_str_orig) { 'fields[articles]=title,body&fields[people]=name' } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/filter_param/fitler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/filter_param/filter' 4 | 5 | describe JSONAPI::Request::QueryParamCollection::FilterParam::Filter do 6 | 7 | let(:f) { JSONAPI::Request::QueryParamCollection::FilterParam::Filter.new('res_name', 'special') } 8 | 9 | context 'when initializing' do 10 | it 'should have proper reader methods' do 11 | expect(f.resource_type).to eq 'res_name' 12 | expect(f.filter).to eq 'special' 13 | end 14 | 15 | it 'should raise when trying to overwrite instance variables' do 16 | expect { f.resource_type = 'new_type' }.to raise_error NoMethodError 17 | expect { f.filter = 'new_string' }.to raise_error NoMethodError 18 | end 19 | end 20 | 21 | context 'when checking to_s' do 22 | it 'should represent an individual fields query string' do 23 | expect(f.to_s).to eq 'filter[res_name]=special' 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/filter_param_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/filter_param' 4 | require 'easy/jsonapi/request/query_param_collection/filter_param/filter' 5 | require 'shared_examples/query_param_tests' 6 | 7 | describe JSONAPI::Request::QueryParamCollection::FilterParam do 8 | 9 | it_behaves_like 'query param tests' do 10 | 11 | filter1 = JSONAPI::Request::QueryParamCollection::FilterParam::Filter.new('comments', '(date == today)') 12 | filter2 = JSONAPI::Request::QueryParamCollection::FilterParam::Filter.new('users', '(age < 15)') 13 | new_filter = JSONAPI::Request::QueryParamCollection::FilterParam::Filter.new('users', '(age > 15)') 14 | 15 | let(:pair) { JSONAPI::Request::QueryParamCollection::FilterParam.new([filter1, filter2]) } 16 | let(:name) { 'filters' } 17 | let(:value) { [filter1, filter2] } 18 | let(:new_value_input) { new_filter } 19 | let(:new_value) { [new_filter] } 20 | let(:to_str_orig) { 'filter[comments]=(date == today)&filter[users]=(age < 15)' } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/include_param_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/include_param' 4 | require 'shared_examples/query_param_tests' 5 | 6 | describe JSONAPI::Request::QueryParamCollection::IncludeParam do 7 | 8 | include_str_arr1 = [ 9 | 'author', 10 | 'comments-likers.children', 11 | 'comments-author-children', 12 | 'sources-publisher.ceo-children' 13 | ] 14 | let(:i1) { JSONAPI::Request::QueryParamCollection::IncludeParam.new(include_str_arr1) } 15 | 16 | let(:value1) do 17 | { 18 | author: { 19 | included: true, 20 | relationships: {} 21 | }, 22 | comments: { 23 | included: false, 24 | relationships: { 25 | likers: { 26 | included: true, 27 | relationships: { 28 | children: { 29 | included: true, 30 | relationships: {} 31 | } 32 | } 33 | }, 34 | author: { 35 | included: false, 36 | relationships: { 37 | children: { 38 | included: true, 39 | relationships: {} 40 | } 41 | } 42 | } 43 | } 44 | }, 45 | sources: { 46 | included: false, 47 | relationships: { 48 | publisher: { 49 | included: true, 50 | relationships: { 51 | ceo: { 52 | included: false, 53 | relationships: { 54 | children: { 55 | included: true, 56 | relationships: {} 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | end 66 | 67 | include_str_arr2 = ['author', 'comments-likers', 'comments.author'] 68 | let(:i2) { JSONAPI::Request::QueryParamCollection::IncludeParam.new(include_str_arr2) } 69 | 70 | let(:value2) do 71 | { 72 | author: { 73 | included: true, 74 | relationships: {} 75 | }, 76 | comments: { 77 | included: true, 78 | relationships: { 79 | likers: { 80 | included: true, 81 | relationships: {} 82 | }, 83 | author: { 84 | included: true, 85 | relationships: {} 86 | } 87 | } 88 | } 89 | } 90 | end 91 | 92 | 93 | it_behaves_like 'query param tests' do 94 | let(:pair) { i1 } 95 | let(:name) { 'includes' } 96 | let(:value) { value1 } 97 | let(:new_value_input) { value2 } 98 | let(:new_value) { value2 } 99 | to_string = 'include=author,comments-likers.children,comments-author-children,' \ 100 | 'sources-publisher.ceo-children' 101 | let(:to_str_orig) { to_string } 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/page_param_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/page_param' 4 | require 'shared_examples/name_value_pair_tests' 5 | 6 | describe JSONAPI::Request::QueryParamCollection::PageParam do 7 | 8 | let(:p) { JSONAPI::Request::QueryParamCollection::PageParam.new(offset: 2, limit: 3) } 9 | 10 | it 'should have proper accessor methods' do 11 | expect(p.name).to eq 'page' 12 | expect(p.offset).to eq 2 13 | expect(p.limit).to eq 3 14 | p.offset = 4 15 | p.limit = 6 16 | expect(p.offset).to eq 4 17 | expect(p.limit).to eq 6 18 | end 19 | 20 | it 'should raise when calling #name=, #value, or #value=' do 21 | error_msg = 'Cannot change the name of QueryParam Objects' 22 | expect { p.name = 'new_name' }.to raise_error error_msg 23 | 24 | error_msg = 'PageParam does not provide a #value method, try #offset or #limit instead' 25 | expect { p.value }.to raise_error error_msg 26 | 27 | error_msg = 'PageParam does not provide a #value= method, try #offset= or #limit= instead' 28 | expect { p.value = 'new_value' }.to raise_error error_msg 29 | end 30 | 31 | it 'should have a working #to_s method' do 32 | expect(p.to_s).to eq 'page[offset]=2&page[limit]=3' 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/query_param_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/item' 4 | require 'easy/jsonapi/request/query_param_collection/query_param' 5 | require 'shared_examples/query_param_tests' 6 | 7 | describe JSONAPI::Request::QueryParamCollection::QueryParam do 8 | 9 | let(:p1) { JSONAPI::Request::QueryParamCollection::QueryParam.new('te_st', 'ing') } 10 | 11 | it_behaves_like 'query param tests' do 12 | let(:pair) { p1 } 13 | let(:name) { 'te_st' } 14 | let(:value) { ['ing'] } 15 | let(:new_value_input) { 'new_value' } 16 | let(:new_value) { ['new_value'] } 17 | let(:to_str_orig) { 'te_st=ing' } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/request/query_param_collection/sort_param_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request/query_param_collection/sort_param' 4 | require 'easy/jsonapi/field' 5 | require 'shared_examples/query_param_tests' 6 | 7 | describe JSONAPI::Request::QueryParamCollection::SortParam do 8 | 9 | it_behaves_like 'query param tests' do 10 | res_field1 = JSONAPI::Field.new('age') 11 | res_field2 = JSONAPI::Field.new('title') 12 | 13 | let(:pair) { JSONAPI::Request::QueryParamCollection::SortParam.new([res_field1, res_field2]) } 14 | let(:name) { 'sorts' } 15 | let(:value) { [res_field1, res_field2] } 16 | 17 | res_field3 = JSONAPI::Field.new('name') 18 | let(:new_value_input) { [res_field3] } 19 | let(:new_value) { [res_field3] } 20 | let(:to_str_orig) { 'sort=age,title' } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/request/query_param_collection_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/collection' 4 | require 'easy/jsonapi/name_value_pair_collection' 5 | require 'easy/jsonapi/request/query_param_collection' 6 | 7 | require 'easy/jsonapi/request/query_param_collection/include_param' 8 | 9 | require 'easy/jsonapi/request/query_param_collection/filter_param' 10 | require 'easy/jsonapi/request/query_param_collection/filter_param/filter' 11 | 12 | require 'easy/jsonapi/request/query_param_collection/page_param' 13 | require 'easy/jsonapi/request/query_param_collection/sort_param' 14 | 15 | require 'easy/jsonapi/request/query_param_collection/fields_param' 16 | require 'easy/jsonapi/request/query_param_collection/fields_param/fieldset' 17 | 18 | require 'easy/jsonapi/field' 19 | 20 | require 'shared_examples/name_value_pair_collections' 21 | 22 | describe JSONAPI::Request::QueryParamCollection do 23 | 24 | item_arr = 25 | [ 26 | JSONAPI::Request::QueryParamCollection::IncludeParam.new(['author', 'comments.likes']), 27 | JSONAPI::Request::QueryParamCollection::FieldsParam.new( 28 | [ 29 | JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new( 30 | 'articles', 31 | [ 32 | JSONAPI::Field.new('title'), 33 | JSONAPI::Field.new('body'), 34 | JSONAPI::Field.new('author') 35 | ] 36 | ), 37 | JSONAPI::Request::QueryParamCollection::FieldsParam::Fieldset.new( 38 | 'people', 39 | [ 40 | JSONAPI::Field.new('name') 41 | ] 42 | ) 43 | ] 44 | ), 45 | JSONAPI::Request::QueryParamCollection::QueryParam.new('leBron', 'james'), 46 | JSONAPI::Request::QueryParamCollection::PageParam.new(offset: 3, limit: 25), 47 | JSONAPI::Request::QueryParamCollection::SortParam.new(['alpha']), 48 | JSONAPI::Request::QueryParamCollection::FilterParam.new( 49 | [JSONAPI::Request::QueryParamCollection::FilterParam::Filter.new('res_name', 'special')] 50 | ) 51 | ] 52 | 53 | # rack::request.params: 54 | # { 55 | # "include"=>"author, comments.author", 56 | # "fields"=>{"articles"=>"title,body,author", "people"=>"name"}, 57 | # "leBron"=>"james", 58 | # "page"=>{"offset"=>"3", "limit"=>"25"}, 59 | # "sort"=>"alpha", 60 | # "filter"=>"special", 61 | # } 62 | 63 | let(:item_class) { JSONAPI::Request::QueryParamCollection::QueryParam } 64 | let(:c_size) { 6 } 65 | let(:keys) { %i[includes fields leBron page sorts filters] } 66 | let(:ex_item_key) { :leBron } 67 | let(:ex_item) { JSONAPI::Request::QueryParamCollection::QueryParam.new('leBron', 'james') } 68 | 69 | let(:to_string) do 70 | 'include=author,comments.likes&fields[articles]=title,body,author&fields[people]=name&' \ 71 | 'leBron=james&page[offset]=3&page[limit]=25&sort=alpha&filter[res_name]=special' 72 | end 73 | 74 | let(:c) { JSONAPI::Request::QueryParamCollection.new(item_arr, &:name) } 75 | let(:ec) { JSONAPI::Request::QueryParamCollection.new } 76 | 77 | it_behaves_like 'name value pair collections' 78 | 79 | describe '#method_missing' do 80 | 81 | let(:pc) { JSONAPI::Request::QueryParamCollection.new(item_arr, &:name) } 82 | 83 | let(:epc) { JSONAPI::Request::QueryParamCollection.new } 84 | 85 | it 'should allow you to have dynamic methods for special params' do 86 | expect(pc.fields.class).to eq JSONAPI::Request::QueryParamCollection::FieldsParam 87 | expect(pc.filters.class).to eq JSONAPI::Request::QueryParamCollection::FilterParam 88 | expect(pc.includes.class).to eq JSONAPI::Request::QueryParamCollection::IncludeParam 89 | expect(pc.page.class).to eq JSONAPI::Request::QueryParamCollection::PageParam 90 | expect(pc.sorts.class).to eq JSONAPI::Request::QueryParamCollection::SortParam 91 | end 92 | 93 | it 'should return nil for special params that are not included instead of raising NoMethodError' do 94 | expect(epc.fields).to be nil 95 | expect(epc.filters).to be nil 96 | expect(epc.includes).to be nil 97 | expect(epc.page).to be nil 98 | expect(epc.sorts).to be nil 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/request_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/request' 4 | require 'easy/jsonapi/parser' 5 | 6 | describe JSONAPI::Request do 7 | 8 | 9 | body_hash = { 10 | data: { 11 | type: "articles", 12 | id: "1", 13 | attributes: { 14 | title: "JSON:API paints my bikeshed!" 15 | }, 16 | links: { 17 | self: "http://example.com/articles/1" 18 | }, 19 | relationships: { 20 | author: { 21 | links: { 22 | self: "http://example.com/articles/1/relationships/author", 23 | related: "http://example.com/articles/1/author" 24 | }, 25 | data: { type: "people", id: "9" } 26 | }, 27 | comments: { 28 | links: { 29 | self: "http://example.com/articles/1/relationships/comments", 30 | related: "http://example.com/articles/1/comments" 31 | }, 32 | data: [ 33 | { type: "comments", id: "5" }, 34 | { type: "comments", id: "12" } 35 | ] 36 | } 37 | } 38 | }, 39 | included: [{ 40 | type: "people", 41 | id: "9", 42 | attributes: { 43 | "first-name": "Dan", 44 | "last-name": "Gebhardt", 45 | twitter: "dgeb" 46 | }, 47 | links: { 48 | self: "http://example.com/people/9" 49 | } 50 | }, { 51 | type: "comments", 52 | id: "5", 53 | attributes: { 54 | body: "First!" 55 | }, 56 | relationships: { 57 | author: { 58 | data: { type: "people", id: "2" } 59 | } 60 | }, 61 | links: { 62 | self: "http://example.com/comments/5" 63 | } 64 | }, { 65 | type: "comments", 66 | id: "12", 67 | attributes: { 68 | body: "I like XML better" 69 | }, 70 | relationships: { 71 | author: { 72 | data: { type: "people", id: "9" } 73 | } 74 | }, 75 | links: { 76 | self: "http://example.com/comments/12" 77 | } 78 | }] 79 | } 80 | 81 | body_str = JSONAPI::Parser::JSONParser.dump(body_hash) 82 | 83 | let(:env) do 84 | { 85 | 'SERVER_SOFTWARE' => 'thin 1.7.2 codename Bachmanity', 86 | 'SERVER_NAME' => 'localhost', 87 | "rack.input" => StringIO.new(body_str), 88 | 'rack.version' => [1, 0], 89 | 'rack.multithread' => false, 90 | 'rack.multiprocess' => false, 91 | 'rack.run_once' => false, 92 | 'REQUEST_METHOD' => 'POST', 93 | 'REQUEST_PATH' => '/articles', 94 | 'PATH_INFO' => '/articles', 95 | 'QUERY_STRING' => 'include=author,comments&fields[articles]=title,body,author&fields[people]=name&josh_ua=demoss&page[offset]=1&page[limit]=1', 96 | 'REQUEST_URI' => '/articles?include=author,comments&fields[articles]=title,body,author&fields[people]=name&=demoss&page[offset]=1&page[limit]=1', 97 | 'HTTP_VERSION' => 'HTTP/1.1', 98 | 'HTTP_ACCEPT' => 'application/vnd.beta.curatess.v1.api+json ; q=0.5, text/*, image/* ; q=.3', 99 | 'HTTP_POSTMAN_TOKEN' => 'de878a8f-917e-4016-b9f7-f723a6483f03', 100 | 'HTTP_HOST' => 'localhost:9292', 101 | 'CONTENT_TYPE' => 'application/vnd.api+json', 102 | 'GATEWAY_INTERFACE' => 'CGI/1.2', 103 | 'SERVER_PORT' => '9292', 104 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 105 | 'rack.url_scheme' => 'http', 106 | 'SCRIPT_NAME' => '', 107 | 'REMOTE_ADDR' => '::1' 108 | } 109 | end 110 | 111 | let(:req) { JSONAPI::Parser.parse_request(env) } 112 | 113 | let(:rack_req) { Rack::Request.new(env) } 114 | 115 | context 'when checking accessor methods' do 116 | it 'should be able to read path, protocol, host, port, query_string' do 117 | expect(req.path).to eq rack_req.path 118 | expect(req.http_method).to eq rack_req.request_method 119 | expect(req.host).to eq rack_req.host 120 | expect(req.port).to eq rack_req.port 121 | expect(req.query_string).to eq rack_req.query_string 122 | end 123 | 124 | it 'should also be able to access params, headers, and body' do 125 | expect(req.params.class).to eq JSONAPI::Request::QueryParamCollection 126 | expect(req.headers.class).to eq JSONAPI::HeaderCollection 127 | expect(req.body.class).to eq JSONAPI::Document 128 | end 129 | 130 | it 'should raise if attempting to overwrite an instance variable' do 131 | expect { req.path = 'new_path' }.to raise_error NoMethodError 132 | expect { req.method = 'new_method' }.to raise_error NoMethodError 133 | expect { req.host = 'new_host' }.to raise_error NoMethodError 134 | expect { req.port = 'new_port' }.to raise_error NoMethodError 135 | expect { req.query_string = 'new_query_string' }.to raise_error NoMethodError 136 | expect { req.params = 'new_params' }.to raise_error NoMethodError 137 | expect { req.headers = 'new_headers' }.to raise_error NoMethodError 138 | expect { req.body = 'new_body' }.to raise_error NoMethodError 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/response' 4 | require 'shared_contexts/document_exceptions_shared_context' 5 | require 'shared_contexts/headers_exceptions_shared_context' 6 | 7 | describe JSONAPI::Response do 8 | include_context 'document exceptions' 9 | include_context 'headers exceptions' 10 | 11 | describe '#validate' do 12 | it 'should return nil if given a valid body and headers' do 13 | expect(JSONAPI::Response.validate(env1, response_doc)).to be nil 14 | end 15 | 16 | it 'should raise InvalidHeader if a header is found to be non compliant' do 17 | expect { JSONAPI::Response.validate(env4, response_doc) }.to raise_error hec 18 | end 19 | 20 | it 'should raise InvalidDocument if the document is found to be non compliant' do 21 | expect { JSONAPI::Response.validate(env1, {}) }.to raise_error dec 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/shared_contexts/document_exceptions_shared_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context 'document exceptions' do 4 | let(:dec) { JSONAPI::Exceptions::DocumentExceptions::InvalidDocument } 5 | 6 | # The example response document given by JSON:API spec examples 7 | let(:response_doc) do 8 | { 9 | links: { 10 | self: "http://example.com/articles", 11 | next: "http://example.com/articles?page[offset]=2", 12 | last: "http://example.com/articles?page[offset]=10" 13 | }, 14 | data: [{ 15 | type: "articles", 16 | id: "1", 17 | attributes: { 18 | title: "JSON:API paints my bikeshed!" 19 | }, 20 | relationships: { 21 | author: { 22 | links: { 23 | self: "http://example.com/articles/1/relationships/author", 24 | related: "http://example.com/articles/1/author" 25 | }, 26 | data: { type: "people", id: "9" } 27 | }, 28 | comments: { 29 | links: { 30 | self: "http://example.com/articles/1/relationships/comments", 31 | related: "http://example.com/articles/1/comments" 32 | }, 33 | data: [ 34 | { type: "comments", id: "5" }, 35 | { type: "comments", id: "12" } 36 | ] 37 | } 38 | }, 39 | links: { 40 | self: "http://example.com/articles/1" 41 | } 42 | }], 43 | included: [{ 44 | type: "people", 45 | id: "9", 46 | attributes: { 47 | firstName: "Dan", 48 | lastName: "Gebhardt", 49 | twitter: "dgeb" 50 | }, 51 | links: { 52 | self: "http://example.com/people/9" 53 | } 54 | }, { 55 | type: "comments", 56 | id: "5", 57 | attributes: { 58 | body: "First!" 59 | }, 60 | relationships: { 61 | author: { 62 | data: { type: "people", id: "2" } 63 | } 64 | }, 65 | links: { 66 | self: "http://example.com/comments/5" 67 | } 68 | }, { 69 | type: "comments", 70 | id: "12", 71 | attributes: { 72 | body: "I like XML better" 73 | }, 74 | relationships: { 75 | author: { 76 | data: { type: "people", id: "9" } 77 | } 78 | }, 79 | links: { 80 | self: "http://example.com/comments/12" 81 | } 82 | }] 83 | } 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/shared_examples/document_collections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shared_examples/collection_like_classes' 4 | 5 | shared_examples 'document collections' do 6 | it_behaves_like 'collection-like classes' 7 | 8 | describe '#to_h' do 9 | it 'should mimic a JSONAPI document' do 10 | expect(c.to_h).to eq to_hash 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/shared_examples/item_shared_tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples 'item shared tests' do 4 | describe '#initialize' do 5 | it 'should inherit methods and variables of the super class' do 6 | expect(item.is_a?(JSONAPI::Item)).to be true 7 | end 8 | 9 | it 'should raise if a hash is passed with strings as keys' do 10 | tmp = { 'name' => 'test', 'value' => 'ing' } 11 | expect { JSONAPI::Item.new(tmp) }.to raise_error "All keys must be Symbols. 'name' was String" 12 | tmp = { name: 'test', 'value' => 'ing' } 13 | expect { JSONAPI::Item.new(tmp) }.to raise_error "All keys must be Symbols. 'value' was String" 14 | end 15 | end 16 | 17 | context 'testing accessor methods' do 18 | it 'should be able to retrieve name and value' do 19 | expect(item.name).to eq 'name' 20 | expect(item.value).to eq 'value' 21 | end 22 | 23 | it 'should be able to update name and value' do 24 | item.name = 'new_name' 25 | item.value = 'new_value' 26 | expect(item.name).to eq 'new_name' 27 | expect(item.value).to eq 'new_value' 28 | end 29 | end 30 | 31 | context 'checking scope' do 32 | it 'should not be able to access parent private methods' do 33 | expect { item.ensure_keys_ar_sym!({ name: 'name' }) }.to raise_error NoMethodError 34 | expect { item.should_update_var?(:name) }.to raise_error NoMethodError 35 | expect { item.should_get_var?(:name) }.to raise_error NoMethodError 36 | end 37 | end 38 | 39 | describe '#to_s' do 40 | it 'should work' do 41 | str = "{ \"name\": \"name\", \"value\": \"value\" }" 42 | expect(item.to_s).to eq str 43 | end 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/shared_examples/name_value_and_query_shared_tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_examples 'name value and query shared tests' do 4 | context '#initialize' do 5 | it 'should inherit methods and variables of the super class' do 6 | expect(pair.is_a?(JSONAPI::Item)).to be true 7 | end 8 | end 9 | 10 | context 'testing accessor methods' do 11 | it 'should be able to retrieve name and value' do 12 | expect(pair.name).to eq name 13 | expect(pair.value).to eq value 14 | end 15 | 16 | it 'should be able to update name and value' do 17 | pair.value = new_value 18 | expect { pair.name = 'new_name' }.to raise_error name_error_msg 19 | expect(pair.value).to eq new_value 20 | end 21 | end 22 | 23 | context 'checking scope' do 24 | it 'should not be able to access parent private methods' do 25 | expect { pair.ensure_keys_are_sym!({ name: name }) }.to raise_error NoMethodError 26 | expect { pair.should_update_var?(:name) }.to raise_error NoMethodError 27 | expect { pair.should_get_var?(:name) }.to raise_error NoMethodError 28 | end 29 | 30 | it 'should return NoMethodError when calling private methods' do 31 | expect { pair.method_missing(:no_method) }.to raise_error NoMethodError 32 | expect { pair.pair }.to raise_error NoMethodError 33 | expect { pair.pair = 5 }.to raise_error NoMethodError 34 | end 35 | end 36 | 37 | describe '#to_s' do 38 | it 'should work' do 39 | expect(pair.to_s).to eq to_str_orig 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/shared_examples/name_value_pair_collections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shared_examples/collection_like_classes' 4 | 5 | shared_examples 'name value pair collections' do 6 | it_behaves_like 'collection-like classes' 7 | end 8 | -------------------------------------------------------------------------------- /spec/shared_examples/name_value_pair_tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shared_examples/name_value_and_query_shared_tests' 4 | 5 | shared_examples 'name value pair tests' do 6 | let(:name) { 'name' } 7 | let(:value) { 'value' } 8 | let(:new_value) { 'new_value' } 9 | let(:to_str_orig) { "\"name\": \"value\"" } 10 | let(:name_error_msg) { 'Cannot change the name of NameValuePair Objects' } 11 | 12 | it_behaves_like 'name value and query shared tests' 13 | 14 | it 'should have a to_h method that mimics JSON' do 15 | expect(pair.to_h).to eq({ name.to_sym => value }) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/shared_examples/query_param_tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shared_examples/name_value_and_query_shared_tests' 4 | 5 | shared_examples 'query param tests' do 6 | it_behaves_like 'name value and query shared tests' do 7 | let(:name_error_msg) { 'Cannot change the name of QueryParam Objects' } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'codecov' 5 | 6 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 7 | SimpleCov.start 8 | 9 | RSpec.configure do |config| 10 | # Enable flags like --only-failures and --next-failure 11 | config.example_status_persistence_file_path = '.rspec_status' 12 | 13 | # Disable RSpec exposing methods globally on `Module` and `main` 14 | config.disable_monkey_patching! 15 | 16 | config.expect_with :rspec do |c| 17 | c.syntax = :expect 18 | end 19 | 20 | # Allows you to call describe without calling RSpec.describe 21 | config.expose_dsl_globally = true 22 | end 23 | -------------------------------------------------------------------------------- /spec/utility_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'easy/jsonapi/utility' 4 | 5 | describe JSONAPI::Utility do 6 | 7 | let(:u) { JSONAPI::Utility } 8 | 9 | describe '#to_h_collection' do 10 | # checked in specific classes they are used in 11 | end 12 | 13 | describe '#to_h_value' do 14 | # checked in specific classes they are used in 15 | end 16 | 17 | describe '#to_h_member' do 18 | # checked in specific classes they are used in 19 | end 20 | 21 | describe '#to_string_collection' do 22 | # checked in specific classes they are used in 23 | end 24 | 25 | describe '#member_to_s' do 26 | # checked in specific classes they are used in 27 | end 28 | 29 | describe '#array_to_s' do 30 | # checked in specific classes they are used in 31 | end 32 | 33 | describe '#path_to_res_type' do 34 | it 'should return the resource type given the path' do 35 | expect(u.path_to_res_type('/test/ing/person/123')).to eq 'person' 36 | expect(u.path_to_res_type('person')).to eq 'person' 37 | expect(u.path_to_res_type('person/6aa7fad8-6d1c-46d9-93fe-83d361155a80')).to eq 'person' 38 | end 39 | # assume valid path input bc invalid paths will not be routed to the server endpoints 40 | end 41 | 42 | describe '#integer?' do 43 | it 'should return true for valid integers' do 44 | expect(u.integer?(123)).to be true 45 | expect(u.integer?('123')).to be true 46 | end 47 | 48 | it 'should return false for invalid integers' do 49 | expect(u.integer?('123a')).to be false 50 | expect(u.integer?('b')).to be false 51 | end 52 | end 53 | 54 | describe '#valid_uuid?' do 55 | it 'should return false for a invalid uuid' do 56 | expect(u.uuid?(123)).to be false 57 | expect(u.uuid?('123-131a-1fs-1')).to be false 58 | expect(u.uuid?('aaaaaaaa-aaaa-aaaa-aaa-aaaaaaaa')).to be false 59 | expect(u.uuid?('666666666666666666666666666666666666')).to be false 60 | expect(u.uuid?('6Aa7fad8-6d1c-46d9-93fe-83d361155a80')).to be true 61 | end 62 | 63 | it 'should return true if a valid uuid' do 64 | expect(u.uuid?('6aa7fad8-6d1c-46d9-93fe-83d361155a80')).to be true 65 | expect(u.uuid?('6AA7FAD8-6D1C-46D9-93FE-83D361155A80')).to be true 66 | end 67 | end 68 | 69 | describe '#all_hash_path?' do 70 | it 'should return true for valid hash paths' do 71 | h = { test: { ing: 'ok' } } 72 | args = %i[test ing] 73 | expect(u.all_hash_path?(h, args)).to be true 74 | end 75 | 76 | it 'should return false for invalid hash paths' do 77 | h = { test: [:ing, 'ok'] } 78 | args = %i[test ing] 79 | expect(u.all_hash_path?(h, args)).to be false 80 | 81 | h = {} 82 | args = %i[test ing] 83 | expect(u.all_hash_path?(h, args)).to be false 84 | 85 | h = { test: 'ing' } 86 | args = %i[test ing] 87 | expect(u.all_hash_path?(h, args)).to be false 88 | 89 | h = { data: { type: "type" } } 90 | args = %i[data id] 91 | expect(u.all_hash_path?(h, args)).to be false 92 | end 93 | end 94 | end 95 | --------------------------------------------------------------------------------