New Upstream Snapshot - ruby-jsonapi-renderer
Ready changes
Summary
Merged new upstream version: 0.2.2+git20190915.1.92e2d1f (was: 0.1.3).
Resulting package
Built on 2022-12-20T08:36 (took 6m23s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots ruby-jsonapi-renderer
Lintian Result
Diff
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2e7d8de
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,28 @@
+language: ruby
+sudo: false
+before_install:
+ - bundle update
+env:
+ global:
+ - CC_TEST_REPORTER_ID=63659c56322a7a1262f6375083f44c8789ee405a6bcf9027189d67c90d08887c
+ - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi)
+rvm:
+ - 2.1
+ - 2.2.2
+ - 2.3.3
+ - ruby-head
+matrix:
+ allow_failures:
+ - rvm: ruby-head
+before_script:
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
+ - chmod +x ./cc-test-reporter
+ - ./cc-test-reporter before-build
+after_script:
+ # Preferably you will run test-reporter on branch update events. But
+ # if you setup travis to build PR updates only, you don't need to run
+ # the line below
+ - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
+ # In the case where travis is setup to build PR updates only,
+ # uncomment the line below
+ # - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..fa75df1
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+
+gemspec
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0c6e615
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Lucas Hosseini
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 7d8bf4d..2b6d443 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ class ResourceInterface
# @return [String]
def jsonapi_id; end
- # Returns a hash containing, for each included relationship, an array of the
+ # Returns a hash containing, for each included relationship, an array of the
# resources to be included from that one.
# @param included_relationships [Array<Symbol>] The keys of the relationships
# to be included.
@@ -57,8 +57,8 @@ class ResourceInterface
# Returns a JSON API-compliant representation of the resource as a hash.
# @param options [Hash]
- # @option fields [Array<Symbol>, Nil] The requested fields, or nil.
- # @option include [Array<Symbol>] The requested relationships to
+ # @option fields [Set<Symbol>, Nil] The requested fields, or nil.
+ # @option include [Set<Symbol>] The requested relationships to
# include (defaults to []).
# @return [Hash]
def as_jsonapi(options = {}); end
@@ -87,6 +87,18 @@ JSONAPI.render(data: resources,
This returns a JSON API compliant hash representing the described document.
+#### Rendering a relationship
+```ruby
+JSONAPI.render(data: resource,
+ relationship: :posts,
+ include: include_string,
+ fields: fields_hash,
+ meta: meta_hash,
+ links: links_hash)
+```
+
+This returns a JSON API compliant hash representing the described document.
+
### Rendering errors
```ruby
@@ -100,6 +112,32 @@ returns a JSON API-compliant representation of the error.
This returns a JSON API compliant hash representing the described document.
+### Caching
+
+The generated JSON fragments can be cached in any cache implementation
+supporting the `fetch_multi` method.
+
+When using caching, the serializable resources must implement an
+additional `jsonapi_cache_key` method:
+```ruby
+ # Returns a cache key for the resource, parameterized by the `include` and
+ # `fields` options.
+ # @param options [Hash]
+ # @option fields [Set<Symbol>, Nil] The requested fields, or nil.
+ # @option include [Set<Symbol>] The requested relationships to
+ # include (defaults to []).
+ # @return [String]
+ def jsonapi_cache_key(options = {}); end
+```
+
+The cache instance must be passed to the renderer as follows:
+```ruby
+JSONAPI.render(data: resources,
+ include: include_string,
+ fields: fields_hash,
+ cache: cache_instance)
+```
+
## License
jsonapi-renderer is released under the [MIT License](http://www.opensource.org/licenses/MIT).
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..7a4e66c
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,9 @@
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec) do |t|
+ t.pattern = Dir.glob('spec/**/*_spec.rb')
+end
+
+task default: :test
+task test: :spec
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..ee1372d
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.2.2
diff --git a/debian/changelog b/debian/changelog
index 9005761..f3306bc 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-ruby-jsonapi-renderer (0.1.3-2) UNRELEASED; urgency=medium
+ruby-jsonapi-renderer (0.2.2+git20190915.1.92e2d1f-1) UNRELEASED; urgency=medium
[ Utkarsh Gupta ]
* Add salsa-ci.yml
@@ -17,8 +17,9 @@ ruby-jsonapi-renderer (0.1.3-2) UNRELEASED; urgency=medium
+ ruby-jsonapi-renderer: Add :all qualifier for ruby dependency.
* Update watch file format version to 4.
* Bump debhelper from old 12 to 13.
+ * New upstream snapshot.
- -- Utkarsh Gupta <guptautkarsh2102@gmail.com> Tue, 13 Aug 2019 05:50:36 +0530
+ -- Utkarsh Gupta <guptautkarsh2102@gmail.com> Tue, 20 Dec 2022 08:32:50 -0000
ruby-jsonapi-renderer (0.1.3-1) unstable; urgency=medium
diff --git a/jsonapi-renderer.gemspec b/jsonapi-renderer.gemspec
index fa1c608..d357345 100644
--- a/jsonapi-renderer.gemspec
+++ b/jsonapi-renderer.gemspec
@@ -1,40 +1,19 @@
-#########################################################
-# This file has been automatically generated by gem2tgz #
-#########################################################
-# -*- encoding: utf-8 -*-
-# stub: jsonapi-renderer 0.1.3 ruby lib
+version = File.read(File.expand_path('../VERSION', __FILE__)).strip
-Gem::Specification.new do |s|
- s.name = "jsonapi-renderer".freeze
- s.version = "0.1.3"
+Gem::Specification.new do |spec|
+ spec.name = 'jsonapi-renderer'
+ spec.version = version
+ spec.author = 'Lucas Hosseini'
+ spec.email = 'lucas.hosseini@gmail.com'
+ spec.summary = 'Render JSONAPI documents.'
+ spec.description = 'Efficiently render JSON API documents.'
+ spec.homepage = 'https://github.com/jsonapi-rb/jsonapi-renderer'
+ spec.license = 'MIT'
- s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
- s.require_paths = ["lib".freeze]
- s.authors = ["Lucas Hosseini".freeze]
- s.date = "2017-07-12"
- s.description = "Efficiently render JSON API documents.".freeze
- s.email = "lucas.hosseini@gmail.com".freeze
- s.files = ["README.md".freeze, "lib/jsonapi/include_directive.rb".freeze, "lib/jsonapi/include_directive/parser.rb".freeze, "lib/jsonapi/renderer.rb".freeze, "lib/jsonapi/renderer/document.rb".freeze, "lib/jsonapi/renderer/resources_processor.rb".freeze]
- s.homepage = "https://github.com/jsonapi-rb/jsonapi-renderer".freeze
- s.licenses = ["MIT".freeze]
- s.rubygems_version = "2.5.2".freeze
- s.summary = "Render JSONAPI documents.".freeze
+ spec.files = Dir['README.md', 'lib/**/*']
+ spec.require_path = 'lib'
- if s.respond_to? :specification_version then
- s.specification_version = 4
-
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
- s.add_development_dependency(%q<codecov>.freeze, ["~> 0.1"])
- s.add_development_dependency(%q<rake>.freeze, ["~> 11.3"])
- s.add_development_dependency(%q<rspec>.freeze, ["~> 3.5"])
- else
- s.add_dependency(%q<codecov>.freeze, ["~> 0.1"])
- s.add_dependency(%q<rake>.freeze, ["~> 11.3"])
- s.add_dependency(%q<rspec>.freeze, ["~> 3.5"])
- end
- else
- s.add_dependency(%q<codecov>.freeze, ["~> 0.1"])
- s.add_dependency(%q<rake>.freeze, ["~> 11.3"])
- s.add_dependency(%q<rspec>.freeze, ["~> 3.5"])
- end
+ spec.add_development_dependency 'rake', '~> 11.3'
+ spec.add_development_dependency 'rspec', '~> 3.5'
+ spec.add_development_dependency 'simplecov'
end
diff --git a/lib/jsonapi/include_directive.rb b/lib/jsonapi/include_directive.rb
index 32ec4f7..142633d 100644
--- a/lib/jsonapi/include_directive.rb
+++ b/lib/jsonapi/include_directive.rb
@@ -16,6 +16,8 @@ module JSONAPI
def initialize(include_args, options = {})
include_hash = Parser.parse_include_args(include_args)
@hash = include_hash.each_with_object({}) do |(key, value), hash|
+ raise InvalidKey, key unless valid?(key)
+
hash[key] = self.class.new(value, options)
end
@options = options
@@ -68,5 +70,18 @@ module JSONAPI
string_array.join(',')
end
+
+ class InvalidKey < StandardError; end
+
+ private
+
+ def valid?(key)
+ key.match(valid_json_key_name_regex)
+ end
+
+ def valid_json_key_name_regex
+ # https://jsonapi.org/format/#document-member-names
+ /^(?![\s\-_])[\u0080-\u10FFA-Za-z0-9* _-]+(?<![\s\-_])$/
+ end
end
end
diff --git a/lib/jsonapi/renderer/cached_resources_processor.rb b/lib/jsonapi/renderer/cached_resources_processor.rb
new file mode 100644
index 0000000..3f77228
--- /dev/null
+++ b/lib/jsonapi/renderer/cached_resources_processor.rb
@@ -0,0 +1,44 @@
+require 'jsonapi/renderer/resources_processor'
+
+module JSONAPI
+ class Renderer
+ # @private
+ class CachedResourcesProcessor < ResourcesProcessor
+ class JSONString < String
+ def to_json(*)
+ self
+ end
+ end
+
+ def initialize(cache)
+ @cache = cache
+ end
+
+ def process_resources
+ [@primary, @included].each do |resources|
+ cache_hash = cache_key_map(resources)
+ processed_resources = @cache.fetch_multi(*cache_hash.keys) do |key|
+ res, include, fields = cache_hash[key]
+ json = res.as_jsonapi(include: include, fields: fields).to_json
+
+ JSONString.new(json)
+ end
+
+ resources.replace(processed_resources.values)
+ end
+ end
+
+ def cache_key_map(resources)
+ resources.each_with_object({}) do |res, h|
+ ri = [res.jsonapi_type, res.jsonapi_id]
+ include_rels = @include_rels[ri]
+ # Sort for cache key consistency
+ include_dir = include_rels.keys.sort unless include_rels.nil?
+ fields = @fields[ri.first.to_sym]
+ h[res.jsonapi_cache_key(include: include_dir, fields: fields)] =
+ [res, include_dir, fields]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/jsonapi/renderer/document.rb b/lib/jsonapi/renderer/document.rb
index 6b9d4ad..21a5ae7 100644
--- a/lib/jsonapi/renderer/document.rb
+++ b/lib/jsonapi/renderer/document.rb
@@ -1,17 +1,21 @@
require 'jsonapi/include_directive'
-require 'jsonapi/renderer/resources_processor'
+require 'jsonapi/renderer/simple_resources_processor'
+require 'jsonapi/renderer/cached_resources_processor'
module JSONAPI
class Renderer
+ # @private
class Document
def initialize(params = {})
@data = params.fetch(:data, :no_data)
@errors = params.fetch(:errors, [])
@meta = params[:meta]
@links = params[:links] || {}
- @fields = _symbolize_fields(params[:fields] || {})
+ @fields = _canonize_fields(params[:fields] || {})
@jsonapi = params[:jsonapi]
@include = JSONAPI::IncludeDirective.new(params[:include] || {})
+ @relationship = params[:relationship]
+ @cache = params[:cache]
end
def to_hash
@@ -21,37 +25,75 @@ module JSONAPI
private
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength
+ # rubocop:disable Metrics/CyclomaticComplexity
def document_hash
{}.tap do |hash|
- if @data != :no_data
+ if @relationship
+ hash.merge!(relationship_hash)
+ elsif @data != :no_data
hash.merge!(data_hash)
elsif @errors.any?
hash.merge!(errors_hash)
end
- hash[:links] = @links if @links.any?
- hash[:meta] = @meta unless @meta.nil?
- hash[:jsonapi] = @jsonapi unless @jsonapi.nil?
+ hash[:links] = @links if @links.any?
+ hash[:meta] = @meta unless @meta.nil?
+ hash[:jsonapi] = @jsonapi unless @jsonapi.nil?
end
end
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength
+ # rubocop:enable Metrics/CyclomaticComplexity
def data_hash
primary, included =
- ResourcesProcessor.new(Array(@data), @include, @fields).process
+ resources_processor.process(Array(@data), @include, @fields)
{}.tap do |hash|
hash[:data] = @data.respond_to?(:to_ary) ? primary : primary[0]
hash[:included] = included if included.any?
end
end
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
+ def relationship_hash
+ rel_name = @relationship.to_sym
+ data = @data.jsonapi_related([rel_name])[rel_name]
+ included =
+ if @include.key?(rel_name)
+ resources_processor.process(data, @include[rel_name], @fields)
+ .flatten!
+ else
+ []
+ end
+
+ res = @data.as_jsonapi(fields: [rel_name], include: [rel_name])
+ rel = res[:relationships][rel_name]
+ @links = rel[:links].merge!(@links)
+ @meta ||= rel[:meta]
+
+ {}.tap do |hash|
+ hash[:data] = rel[:data]
+ hash[:included] = included if included.any?
+ end
+ end
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
+
def errors_hash
{}.tap do |hash|
- hash[:errors] = @errors.map(&:as_jsonapi)
+ hash[:errors] = @errors.flat_map(&:as_jsonapi)
+ end
+ end
+
+ def resources_processor
+ if @cache
+ CachedResourcesProcessor.new(@cache)
+ else
+ SimpleResourcesProcessor.new
end
end
- def _symbolize_fields(fields)
+ def _canonize_fields(fields)
fields.each_with_object({}) do |(k, v), h|
- h[k.to_sym] = v.map(&:to_sym)
+ h[k.to_sym] = v.map(&:to_sym).sort!
end
end
end
diff --git a/lib/jsonapi/renderer/resources_processor.rb b/lib/jsonapi/renderer/resources_processor.rb
index 9e6be30..dfd5264 100644
--- a/lib/jsonapi/renderer/resources_processor.rb
+++ b/lib/jsonapi/renderer/resources_processor.rb
@@ -1,15 +1,12 @@
-require 'set'
-
module JSONAPI
class Renderer
+ # @private
class ResourcesProcessor
- def initialize(resources, include, fields)
+ def process(resources, include, fields)
@resources = resources
@include = include
@fields = fields
- end
- def process
traverse_resources
process_resources
@@ -19,8 +16,9 @@ module JSONAPI
private
def traverse_resources
- @traversed = Set.new # [type, id, prefix]
- @include_rels = {} # [type, id => Set]
+ # Use hash instead of set for better performances
+ @traversed = {} # Hash[type, id, prefix] => true
+ @include_rels = {} # Hash[type, id] => Hash[include_key => true]
@queue = []
@primary = []
@included = []
@@ -31,7 +29,7 @@ module JSONAPI
def initialize_queue
@resources.each do |res|
- @traversed.add([res.jsonapi_type, res.jsonapi_id, ''])
+ @traversed[[res.jsonapi_type, res.jsonapi_id, '']] = true
traverse_resource(res, @include.keys, true)
enqueue_related_resources(res, '', @include)
end
@@ -47,40 +45,37 @@ module JSONAPI
def traverse_resource(res, include_keys, primary)
ri = [res.jsonapi_type, res.jsonapi_id]
+ keys_hash = {}
+ include_keys.each { |k| keys_hash[k] = true }
+
if @include_rels.include?(ri)
- @include_rels[ri].merge(include_keys)
+ @include_rels[ri].merge!(keys_hash)
else
- @include_rels[ri] = Set.new(include_keys)
+ @include_rels[ri] = keys_hash
(primary ? @primary : @included) << res
end
end
def enqueue_related_resources(res, prefix, include_dir)
res.jsonapi_related(include_dir.keys).each do |key, data|
+ child_prefix = "#{prefix}.#{key}".freeze
data.each do |child_res|
next if child_res.nil?
- child_prefix = "#{prefix}.#{key}"
enqueue_resource(child_res, child_prefix, include_dir[key])
end
end
end
def enqueue_resource(res, prefix, include_dir)
- return unless @traversed.add?([res.jsonapi_type,
- res.jsonapi_id,
- prefix])
+ key = [res.jsonapi_type, res.jsonapi_id, prefix]
+ return if @traversed[key]
+
+ @traversed[key] = true
@queue << [res, prefix, include_dir]
end
def process_resources
- [@primary, @included].each do |resources|
- resources.map! do |res|
- ri = [res.jsonapi_type, res.jsonapi_id]
- include_dir = @include_rels[ri]
- fields = @fields[res.jsonapi_type.to_sym]
- res.as_jsonapi(include: include_dir, fields: fields)
- end
- end
+ raise 'Not implemented'
end
end
end
diff --git a/lib/jsonapi/renderer/simple_resources_processor.rb b/lib/jsonapi/renderer/simple_resources_processor.rb
new file mode 100644
index 0000000..1249e6d
--- /dev/null
+++ b/lib/jsonapi/renderer/simple_resources_processor.rb
@@ -0,0 +1,19 @@
+require 'jsonapi/renderer/resources_processor'
+
+module JSONAPI
+ class Renderer
+ # @api private
+ class SimpleResourcesProcessor < ResourcesProcessor
+ def process_resources
+ [@primary, @included].each do |resources|
+ resources.map! do |res|
+ ri = [res.jsonapi_type, res.jsonapi_id]
+ include_dir = @include_rels[ri].keys
+ fields = @fields[res.jsonapi_type.to_sym]
+ res.as_jsonapi(include: include_dir, fields: fields)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/caching_spec.rb b/spec/caching_spec.rb
new file mode 100644
index 0000000..35c605c
--- /dev/null
+++ b/spec/caching_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+class Cache
+ def initialize
+ @cache = {}
+ end
+
+ def fetch_multi(*keys)
+ keys.each_with_object({}) do |k, h|
+ @cache[k] = yield(k) unless @cache.key?(k)
+ h[k] = @cache[k]
+ end
+ end
+end
+
+describe JSONAPI::Renderer, '#render' do
+ before(:all) do
+ @users = [
+ UserResource.new(1, 'User 1', '123 Example st.', []),
+ UserResource.new(2, 'User 2', '234 Example st.', []),
+ UserResource.new(3, 'User 3', '345 Example st.', []),
+ UserResource.new(4, 'User 4', '456 Example st.', [])
+ ]
+ @posts = [
+ PostResource.new(1, 'Post 1', 'yesterday', @users[1]),
+ PostResource.new(2, 'Post 2', 'today', @users[0]),
+ PostResource.new(3, 'Post 3', 'tomorrow', @users[1])
+ ]
+ @users[0].posts = [@posts[1]]
+ @users[1].posts = [@posts[0], @posts[2]]
+ end
+
+ it 'renders included relationships' do
+ cache = Cache.new
+ # Warm up the cache.
+ subject.render(data: @users[0],
+ include: 'posts',
+ cache: cache)
+ # Actual call on warm cache.
+ actual = subject.render(data: @users[0],
+ include: 'posts',
+ cache: cache)
+ expected = {
+ data: {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1',
+ address: '123 Example st.'
+ },
+ relationships: {
+ posts: {
+ data: [{ type: 'posts', id: '2' }],
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ },
+ included: [
+ {
+ type: 'posts',
+ id: '2',
+ attributes: {
+ title: 'Post 2',
+ date: 'today'
+ },
+ relationships: {
+ author: {
+ links: {
+ self: 'http://api.example.com/posts/2/relationships/author',
+ related: 'http://api.example.com/posts/2/author'
+ },
+ meta: {
+ author_active: true
+ }
+ }
+ }
+ }
+ ]
+ }
+
+ expect(JSON.parse(actual.to_json)).to eq(JSON.parse(expected.to_json))
+ expect(actual[:data]).to be_a(JSONAPI::Renderer::CachedResourcesProcessor::JSONString)
+ end
+end
diff --git a/spec/include_directive/parser_spec.rb b/spec/include_directive/parser_spec.rb
new file mode 100644
index 0000000..6179da0
--- /dev/null
+++ b/spec/include_directive/parser_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+require 'jsonapi/include_directive'
+
+describe JSONAPI::IncludeDirective::Parser, '.parse_include_args' do
+ it 'handles arrays of symbols and hashes' do
+ args = [:friends,
+ comments: [:author],
+ posts: [:author,
+ comments: [:author]]]
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args)
+ expected = {
+ friends: {},
+ comments: { author: {} },
+ posts: { author: {}, comments: { author: {} } }
+ }
+
+ expect(hash).to eq expected
+ end
+
+ it 'handles strings' do
+ str = 'friends,comments.author,posts.author,posts.comments.author'
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(str)
+ expected = {
+ friends: {},
+ comments: { author: {} },
+ posts: { author: {}, comments: { author: {} } }
+ }
+
+ expect(hash).to eq expected
+ end
+
+ it 'treats spaces as part of the resource name' do
+ str = 'friends, comments.author , posts.author,posts. comments.author'
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(str)
+ expected = {
+ friends: {},
+ :' comments' => { :'author ' => {} },
+ :' posts' => { author: {} },
+ :'posts' => { :' comments' => { author: {} } }
+ }
+
+ expect(hash).to eq expected
+ end
+
+ it 'handles common prefixes in strings' do
+ args = ['friends', 'comments.author', 'posts.author',
+ 'posts.comments.author']
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args)
+ expected = {
+ friends: {},
+ comments: { author: {} },
+ posts: { author: {}, comments: { author: {} } }
+ }
+
+ expect(hash).to eq expected
+ end
+
+ it 'handles an empty string' do
+ args = ''
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args)
+ expected = {}
+
+ expect(hash).to eq expected
+ end
+
+ it 'handles an empty array' do
+ args = []
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args)
+ expected = {}
+
+ expect(hash).to eq expected
+ end
+
+ it 'handles invalid input' do
+ args = Object.new
+ hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args)
+ expected = {}
+
+ expect(hash).to eq expected
+ end
+end
diff --git a/spec/include_directive_spec.rb b/spec/include_directive_spec.rb
new file mode 100644
index 0000000..3223d1b
--- /dev/null
+++ b/spec/include_directive_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+require 'jsonapi/include_directive'
+
+describe JSONAPI::IncludeDirective, '.initialize' do
+ context 'raises InvalidKey when' do
+ (
+ ["\u002B", "\u002C", "\u002E", "\u005B", "\u005D", "\u002A", "\u002F",
+ "\u0040", "\u005C", "\u005E", "\u0060"] + ("\u0021".."\u0029").to_a \
+ + ("\u003A".."\u003F").to_a \
+ + ("\u007B".."\u007F").to_a \
+ + ("\u0000".."\u001F").to_a \
+ - ['*', '.', ',']
+ ).each do |invalid_character|
+ it "invalid character provided: '#{invalid_character}'" do
+ expect { JSONAPI::IncludeDirective.new(invalid_character) }
+ .to raise_error(JSONAPI::IncludeDirective::InvalidKey)
+ end
+ end
+
+ [' ', '_', '-'].each do |char|
+ it "starts with following character: '#{char}'" do
+ expect { JSONAPI::IncludeDirective.new("#{char}_with_valid") }
+ .to raise_error(JSONAPI::IncludeDirective::InvalidKey, "#{char}_with_valid")
+ end
+
+ it "ends with following character: '#{char}'" do
+ expect { JSONAPI::IncludeDirective.new("valid_with_#{char}") }
+ .to raise_error(JSONAPI::IncludeDirective::InvalidKey, "valid_with_#{char}")
+ end
+ end
+ end
+
+ context 'does not raise InvalidKey' do
+ ["\u0080", "B", "t", "5", "\u0100", "\u10FFFAA"].each do |char|
+ it "when provided character '#{char}'" do
+ expect(JSONAPI::IncludeDirective.new(char).key?(char)).to be true
+ end
+ end
+
+ [' ', '_', '-'].each do |char|
+ it "when '#{char}' is not at beginning or end" do
+ key = "valid#{char}valid"
+ expect(JSONAPI::IncludeDirective.new(key).key?(key)).to be true
+ end
+ end
+ end
+end
+
+describe JSONAPI::IncludeDirective, '.key?' do
+ it 'handles existing keys' do
+ str = 'posts.comments'
+ include_directive = JSONAPI::IncludeDirective.new(str)
+
+ expect(include_directive.key?(:posts)).to be_truthy
+ end
+
+ it 'handles absent keys' do
+ str = 'posts.comments'
+ include_directive = JSONAPI::IncludeDirective.new(str)
+
+ expect(include_directive.key?(:author)).to be_falsy
+ end
+
+ it 'handles wildcards' do
+ str = 'posts.*'
+ include_directive = JSONAPI::IncludeDirective.new(
+ str, allow_wildcard: true)
+
+ expect(include_directive[:posts].key?(:author)).to be_truthy
+ expect(include_directive[:posts][:author].key?(:comments)).to be_falsy
+ end
+
+ it 'handles wildcards' do
+ str = 'posts.**'
+ include_directive = JSONAPI::IncludeDirective.new(
+ str, allow_wildcard: true)
+
+ expect(include_directive[:posts].key?(:author)).to be_truthy
+ expect(include_directive[:posts][:author].key?(:comments)).to be_truthy
+ end
+end
+
+describe JSONAPI::IncludeDirective, '.to_string' do
+ it 'works' do
+ str = 'friends,comments.author,posts.author,posts.comments.author'
+ include_directive = JSONAPI::IncludeDirective.new(str)
+ expected = include_directive.to_hash
+ actual = JSONAPI::IncludeDirective.new(include_directive.to_string)
+ .to_hash
+
+ expect(actual).to eq expected
+ end
+end
diff --git a/spec/renderer_spec.rb b/spec/renderer_spec.rb
new file mode 100644
index 0000000..71cf473
--- /dev/null
+++ b/spec/renderer_spec.rb
@@ -0,0 +1,439 @@
+require 'spec_helper'
+
+describe JSONAPI::Renderer, '#render' do
+ before(:all) do
+ @users = [
+ UserResource.new(1, 'User 1', '123 Example st.', []),
+ UserResource.new(2, 'User 2', '234 Example st.', []),
+ UserResource.new(3, 'User 3', '345 Example st.', []),
+ UserResource.new(4, 'User 4', '456 Example st.', [])
+ ]
+ @posts = [
+ PostResource.new(1, 'Post 1', 'yesterday', @users[1]),
+ PostResource.new(2, 'Post 2', 'today', @users[0]),
+ PostResource.new(3, 'Post 3', 'tomorrow', @users[1])
+ ]
+ @users[0].posts = [@posts[1]]
+ @users[1].posts = [@posts[0], @posts[2]]
+ end
+
+ it 'renders nil' do
+ actual = subject.render(data: nil)
+ expected = {
+ data: nil
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders an empty array' do
+ actual = subject.render(data: [])
+ expected = {
+ data: []
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders a single resource' do
+ actual = subject.render(data: @users[0])
+ expected = {
+ data: {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1',
+ address: '123 Example st.'
+ },
+ relationships: {
+ posts: {
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ }
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders a collection of resources' do
+ actual = subject.render(data: [@users[0],
+ @users[1]])
+ expected = {
+ data: [
+ {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1',
+ address: '123 Example st.'
+ },
+ relationships: {
+ posts: {
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ },
+ {
+ type: 'users',
+ id: '2',
+ attributes: {
+ name: 'User 2',
+ address: '234 Example st.'
+ },
+ relationships: {
+ posts: {
+ links: {
+ self: 'http://api.example.com/users/2/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/2/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/2'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ }
+ ]
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders included relationships' do
+ actual = subject.render(data: @users[0],
+ include: 'posts')
+ expected = {
+ data: {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1',
+ address: '123 Example st.'
+ },
+ relationships: {
+ posts: {
+ data: [{ type: 'posts', id: '2' }],
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ },
+ included: [
+ {
+ type: 'posts',
+ id: '2',
+ attributes: {
+ title: 'Post 2',
+ date: 'today'
+ },
+ relationships: {
+ author: {
+ links: {
+ self: 'http://api.example.com/posts/2/relationships/author',
+ related: 'http://api.example.com/posts/2/author'
+ },
+ meta: {
+ author_active: true
+ }
+ }
+ }
+ }
+ ]
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'filters out fields' do
+ actual = subject.render(data: @users[0],
+ fields: { users: [:name] })
+ expected = {
+ data: {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1'
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ }
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ context 'when fields option is nil' do
+ it 'does not filter out fields' do
+ actual = subject.render(data: @users[0], fields: nil)
+
+ expected = {
+ data: {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1',
+ address: '123 Example st.'
+ },
+ relationships: {
+ posts: {
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ }
+ }
+
+ expect(actual).to eq(expected)
+ end
+ end
+
+ it 'renders a toplevel meta' do
+ actual = subject.render(data: nil,
+ meta: { this: 'is_meta' })
+ expected = {
+ data: nil,
+ meta: { this: 'is_meta' }
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders toplevel links' do
+ actual = subject.render(data: nil,
+ links: { self: 'http://api.example.com/users' })
+ expected = {
+ data: nil,
+ links: { self: 'http://api.example.com/users' }
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders a toplevel jsonapi object' do
+ actual = subject.render(data: nil,
+ jsonapi: {
+ version: '1.0',
+ meta: 'For real'
+ })
+ expected = {
+ data: nil,
+ jsonapi: {
+ version: '1.0',
+ meta: 'For real'
+ }
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders an empty hash if neither errors nor data provided' do
+ actual = subject.render({})
+ expected = {}
+
+ expect(actual).to eq(expected)
+ end
+
+ class ErrorResource
+ def initialize(id, title)
+ @id = id
+ @title = title
+ end
+
+ def as_jsonapi
+ { id: @id, title: @title }
+ end
+ end
+
+ it 'renders errors' do
+ errors = [ErrorResource.new('1', 'Not working'),
+ ErrorResource.new('2', 'Works poorly')]
+ actual = subject.render(errors: errors)
+ expected = {
+ errors: [{ id: '1', title: 'Not working' },
+ { id: '2', title: 'Works poorly' }]
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ context 'when rendering a relationship' do
+ it 'renders the linkage data only' do
+ actual = subject.render(data: @users[0], relationship: :posts)
+ expected = {
+ data: [{ type: 'posts', id: '2' }],
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+
+ expect(actual).to eq(expected)
+ end
+
+ it 'renders supports include parameter' do
+ actual = subject.render(data: @users[0], relationship: :posts,
+ include: 'posts.author')
+ actual_included = actual.delete(:included)
+
+ expected = {
+ data: [{ type: 'posts', id: '2' }],
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ expected_included = [
+ {
+ type: 'users',
+ id: '1',
+ attributes: {
+ name: 'User 1',
+ address: '123 Example st.'
+ },
+ relationships: {
+ posts: {
+ links: {
+ self: 'http://api.example.com/users/1/relationships/posts',
+ related: {
+ href: 'http://api.example.com/users/1/posts',
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ },
+ links: {
+ self: 'http://api.example.com/users/1'
+ },
+ meta: {
+ user_meta: 'is_meta'
+ }
+ },
+ {
+ type: 'posts',
+ id: '2',
+ attributes: {
+ title: 'Post 2',
+ date: 'today'
+ },
+ relationships: {
+ author: {
+ data: { type: 'users', id: '1' },
+ links: {
+ self: 'http://api.example.com/posts/2/relationships/author',
+ related: 'http://api.example.com/posts/2/author'
+ },
+ meta: {
+ author_active: true
+ }
+ }
+ }
+ }
+ ]
+
+ expect(actual).to eq(expected)
+ expect(actual_included).to match_array(expected_included)
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..547994d
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,133 @@
+require 'simplecov'
+SimpleCov.start do
+ add_filter '/spec/'
+end
+
+require 'jsonapi/renderer'
+
+class UserResource
+ attr_accessor :id, :name, :address, :posts
+
+ def initialize(id, name, address, posts)
+ @id = id
+ @name = name
+ @address = address
+ @posts = posts
+ end
+
+ def jsonapi_type
+ 'users'
+ end
+
+ def jsonapi_id
+ @id.to_s
+ end
+
+ def jsonapi_related(included)
+ if included.include?(:posts)
+ { posts: @posts.map { |p| p } }
+ else
+ {}
+ end
+ end
+
+ def jsonapi_cache_key(options = {})
+ "#{jsonapi_type} - #{jsonapi_id} - #{options[:include].to_a.sort} - #{(options[:fields] || Set.new).to_a.sort}"
+ end
+
+ def as_jsonapi(options = {})
+ fields = options[:fields] || [:name, :address, :posts]
+ included = options[:include] || []
+
+ hash = { id: jsonapi_id, type: jsonapi_type }
+ hash[:attributes] = { name: @name, address: @address }
+ .select { |k, _| fields.include?(k) }
+ if fields.include?(:posts)
+ hash[:relationships] = { posts: {} }
+ hash[:relationships][:posts] = {
+ links: {
+ self: "http://api.example.com/users/#{@id}/relationships/posts",
+ related: {
+ href: "http://api.example.com/users/#{@id}/posts",
+ meta: {
+ do_not_use: true
+ }
+ }
+ },
+ meta: {
+ deleted_posts: 5
+ }
+ }
+ if included.include?(:posts)
+ hash[:relationships][:posts][:data] = @posts.map do |p|
+ { type: 'posts', id: p.id.to_s }
+ end
+ end
+ end
+
+ hash[:links] = {
+ self: "http://api.example.com/users/#{@id}"
+ }
+ hash[:meta] = { user_meta: 'is_meta' }
+
+ hash
+ end
+end
+
+class PostResource
+ attr_accessor :id, :title, :date, :author
+
+ def initialize(id, title, date, author)
+ @id = id
+ @title = title
+ @date = date
+ @author = author
+ end
+
+ def jsonapi_type
+ 'posts'
+ end
+
+ def jsonapi_id
+ @id.to_s
+ end
+
+ def jsonapi_related(included)
+ included.include?(:author) ? { author: [@author] } : {}
+ end
+
+ def jsonapi_cache_key(options = {})
+ "#{jsonapi_type} - #{jsonapi_id} - #{options[:include].to_a.sort} - #{(options[:fields] || Set.new).to_a.sort}"
+ end
+
+ def as_jsonapi(options = {})
+ fields = options[:fields] || [:title, :date, :author]
+ included = options[:include] || []
+ hash = { id: jsonapi_id, type: jsonapi_type }
+
+ hash[:attributes] = { title: @title, date: @date }
+ .select { |k, _| fields.include?(k) }
+ if fields.include?(:author)
+ hash[:relationships] = { author: {} }
+ hash[:relationships][:author] = {
+ links: {
+ self: "http://api.example.com/posts/#{@id}/relationships/author",
+ related: "http://api.example.com/posts/#{@id}/author"
+ },
+ meta: {
+ author_active: true
+ }
+ }
+ if included.include?(:author)
+ hash[:relationships][:author][:data] =
+ if @author.nil?
+ nil
+ else
+ { type: 'users', id: @author.id.to_s }
+ end
+ end
+ end
+
+ hash
+ end
+end
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/lib/ruby/vendor_ruby/jsonapi/renderer/cached_resources_processor.rb -rw-r--r-- root/root /usr/lib/ruby/vendor_ruby/jsonapi/renderer/simple_resources_processor.rb -rw-r--r-- root/root /usr/share/doc/ruby-jsonapi-renderer/README.md.gz -rw-r--r-- root/root /usr/share/rubygems-integration/all/specifications/jsonapi-renderer-0.2.2.gemspec
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/share/doc/ruby-jsonapi-renderer/README.md -rw-r--r-- root/root /usr/share/rubygems-integration/all/specifications/jsonapi-renderer-0.1.3.gemspec
Control files: lines which differ (wdiff format)
Ruby-Versions: all