From 117e8feb4cd8a64573e9201facbcbc7b54ec2beb Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 12 Sep 2017 00:36:11 +0200 Subject: [PATCH 1/5] Refactor IncludeDirective. --- lib/jsonapi/include_directive.rb | 59 ++++++++++++++++++++----- lib/jsonapi/include_directive/parser.rb | 48 ++++++++------------ lib/jsonapi/renderer/document.rb | 2 +- spec/caching_spec.rb | 4 +- spec/include_directive/parser_spec.rb | 25 +++-------- spec/include_directive_spec.rb | 20 +++++---- spec/renderer_spec.rb | 8 ++-- 7 files changed, 91 insertions(+), 75 deletions(-) diff --git a/lib/jsonapi/include_directive.rb b/lib/jsonapi/include_directive.rb index 32ec4f7..9a3984d 100644 --- a/lib/jsonapi/include_directive.rb +++ b/lib/jsonapi/include_directive.rb @@ -12,9 +12,47 @@ module JSONAPI # @example 'posts.**' # => Include related posts, and all the included # posts' related resources, and their related resources, recursively. class IncludeDirective - # @param include_args (see Parser.parse_include_args) - def initialize(include_args, options = {}) - include_hash = Parser.parse_include_args(include_args) + # Convenience method to build an IncludeDirective from a string, an array, + # or a hash. + # @param obj [String,Array,Hash] + # @param options [Hash] + # @return [IncludeDirective] + def self.create(obj, options = {}) + if obj.is_a?(String) + from_string(obj, options) + elsif obj.is_a?(Array) + from_array(obj, options) + elsif obj.is_a?(Hash) + from_hash(obj, options) + end + end + + # Build IncludeDirective from a string. + # @param string [String] + # @param options [Hash] + # @return [IncludeDirective] + def self.from_string(string, options = {}) + new(Parser.parse_string(string), options) + end + + # Build IncludeDirective from an array. + # @param array [Array] + # @param options [Hash] + # @return [IncludeDirective] + def self.from_array(array, options = {}) + new(Parser.parse_array(array), options) + end + + # Build IncludeDirective from a hash. + # @param hash [Hash] + # @param options [Hash] + # @return [IncludeDirective] + def self.from_hash(hash, options = {}) + new(Parser.parse_hash(hash), options) + end + + # @param include_hash [Hash{Symbol=>Hash}] + def initialize(include_hash, options = {}) @hash = include_hash.each_with_object({}) do |(key, value), hash| hash[key] = self.class.new(value, options) end @@ -22,6 +60,7 @@ def initialize(include_args, options = {}) end # @param key [Symbol, String] + # @return [Boolean] def key?(key) @hash.key?(key.to_sym) || (@options[:allow_wildcard] && (@hash.key?(:*) || @hash.key?(:**))) @@ -35,12 +74,11 @@ def keys # @param key [Symbol, String] # @return [IncludeDirective, nil] def [](key) - case - when @hash.key?(key.to_sym) + if @hash.key?(key.to_sym) @hash[key.to_sym] - when @options[:allow_wildcard] && @hash.key?(:**) + elsif @options[:allow_wildcard] && @hash.key?(:**) self.class.new({ :** => {} }, @options) - when @options[:allow_wildcard] && @hash.key?(:*) + elsif @options[:allow_wildcard] && @hash.key?(:*) @hash[:*] end end @@ -51,6 +89,7 @@ def to_hash hash[key] = value.to_hash end end + alias to_h to_hash # @return [String] def to_string @@ -59,14 +98,12 @@ def to_string if string_value == '' key.to_s else - string_value - .split(',') - .map { |x| key.to_s + '.' + x } - .join(',') + string_value.split(',').map { |x| key.to_s + '.' + x }.join(',') end end string_array.join(',') end + alias to_s to_string end end diff --git a/lib/jsonapi/include_directive/parser.rb b/lib/jsonapi/include_directive/parser.rb index 384d4da..b325e0c 100644 --- a/lib/jsonapi/include_directive/parser.rb +++ b/lib/jsonapi/include_directive/parser.rb @@ -1,56 +1,44 @@ module JSONAPI class IncludeDirective - # Utilities to create an IncludeDirective hash from various types of - # inputs. + # @private module Parser module_function - # @api private - def parse_include_args(include_args) - case include_args - when Symbol - { include_args => {} } - when Hash - parse_hash(include_args) - when Array - parse_array(include_args) - when String - parse_string(include_args) - else - {} - end - end - - # @api private def parse_string(include_string) - include_string.split(',') - .each_with_object({}) do |path, hash| - deep_merge!(hash, parse_path_string(path)) + include_string.split(',').each_with_object({}) do |path, hash| + deep_merge!(hash, parse_path_string(path)) end end - # @api private def parse_path_string(include_path) - include_path.split('.') - .reverse - .reduce({}) { |a, e| { e.to_sym => a } } + include_path.split('.').reverse.reduce({}) { |a, e| { e.to_sym => a } } end - # @api private def parse_hash(include_hash) include_hash.each_with_object({}) do |(key, value), hash| hash[key.to_sym] = parse_include_args(value) end end - # @api private def parse_array(include_array) include_array.each_with_object({}) do |x, hash| - deep_merge!(hash, parse_include_args(x)) + hash.merge!(parse_include_args(x)) + end + end + + def parse_include_args(include_args) + case include_args + when Symbol + { include_args => {} } + when Hash + parse_hash(include_args) + when Array + parse_array(include_args) + else + {} end end - # @api private def deep_merge!(src, ext) ext.each do |k, v| if src[k].is_a?(Hash) && v.is_a?(Hash) diff --git a/lib/jsonapi/renderer/document.rb b/lib/jsonapi/renderer/document.rb index fd1de9c..84b97a4 100644 --- a/lib/jsonapi/renderer/document.rb +++ b/lib/jsonapi/renderer/document.rb @@ -13,7 +13,7 @@ def initialize(params = {}) @links = params[:links] || {} @fields = _symbolize_fields(params[:fields] || {}) @jsonapi = params[:jsonapi] - @include = JSONAPI::IncludeDirective.new(params[:include] || {}) + @include = params[:include] || {} @relationship = params[:relationship] @cache = params[:cache] end diff --git a/spec/caching_spec.rb b/spec/caching_spec.rb index f153ba7..c03d577 100644 --- a/spec/caching_spec.rb +++ b/spec/caching_spec.rb @@ -34,11 +34,11 @@ def fetch_multi(keys) cache = Cache.new # Warm up the cache. subject.render(data: @users[0], - include: 'posts', + include: JSONAPI::IncludeDirective.from_string('posts'), cache: cache) # Actual call on warm cache. actual = subject.render(data: @users[0], - include: 'posts', + include: JSONAPI::IncludeDirective.from_string('posts'), cache: cache) expected = { data: { diff --git a/spec/include_directive/parser_spec.rb b/spec/include_directive/parser_spec.rb index 6179da0..9420a5b 100644 --- a/spec/include_directive/parser_spec.rb +++ b/spec/include_directive/parser_spec.rb @@ -8,7 +8,7 @@ comments: [:author], posts: [:author, comments: [:author]]] - hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args) + hash = subject.parse_include_args(args) expected = { friends: {}, comments: { author: {} }, @@ -20,7 +20,7 @@ it 'handles strings' do str = 'friends,comments.author,posts.author,posts.comments.author' - hash = JSONAPI::IncludeDirective::Parser.parse_include_args(str) + hash = subject.parse_string(str) expected = { friends: {}, comments: { author: {} }, @@ -32,7 +32,7 @@ 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) + hash = subject.parse_string(str) expected = { friends: {}, :' comments' => { :'author ' => {} }, @@ -43,22 +43,9 @@ 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) + hash = subject.parse_include_args(args) expected = {} expect(hash).to eq expected @@ -66,7 +53,7 @@ it 'handles an empty array' do args = [] - hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args) + hash = subject.parse_include_args(args) expected = {} expect(hash).to eq expected @@ -74,7 +61,7 @@ it 'handles invalid input' do args = Object.new - hash = JSONAPI::IncludeDirective::Parser.parse_include_args(args) + hash = subject.parse_include_args(args) expected = {} expect(hash).to eq expected diff --git a/spec/include_directive_spec.rb b/spec/include_directive_spec.rb index 8e18eca..1a917bb 100644 --- a/spec/include_directive_spec.rb +++ b/spec/include_directive_spec.rb @@ -5,22 +5,23 @@ describe JSONAPI::IncludeDirective, '.key?' do it 'handles existing keys' do str = 'posts.comments' - include_directive = JSONAPI::IncludeDirective.new(str) + include_directive = JSONAPI::IncludeDirective.from_string(str) expect(include_directive.key?(:posts)).to be_truthy end it 'handles absent keys' do str = 'posts.comments' - include_directive = JSONAPI::IncludeDirective.new(str) + include_directive = JSONAPI::IncludeDirective.from_string(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) + include_directive = JSONAPI::IncludeDirective.from_string( + str, allow_wildcard: true + ) expect(include_directive[:posts].key?(:author)).to be_truthy expect(include_directive[:posts][:author].key?(:comments)).to be_falsy @@ -28,8 +29,9 @@ it 'handles wildcards' do str = 'posts.**' - include_directive = JSONAPI::IncludeDirective.new( - str, allow_wildcard: true) + include_directive = JSONAPI::IncludeDirective.from_string( + str, allow_wildcard: true + ) expect(include_directive[:posts].key?(:author)).to be_truthy expect(include_directive[:posts][:author].key?(:comments)).to be_truthy @@ -39,10 +41,10 @@ describe JSONAPI::IncludeDirective, '.to_string' do it 'works' do str = 'friends,comments.author,posts.author,posts.comments.author' - include_directive = JSONAPI::IncludeDirective.new(str) + include_directive = JSONAPI::IncludeDirective.from_string(str) expected = include_directive.to_hash - actual = JSONAPI::IncludeDirective.new(include_directive.to_string) - .to_hash + actual = JSONAPI::IncludeDirective.from_string(include_directive.to_string) + .to_hash expect(actual).to eq expected end diff --git a/spec/renderer_spec.rb b/spec/renderer_spec.rb index 71cf473..0998054 100644 --- a/spec/renderer_spec.rb +++ b/spec/renderer_spec.rb @@ -146,7 +146,7 @@ it 'renders included relationships' do actual = subject.render(data: @users[0], - include: 'posts') + include: JSONAPI::IncludeDirective.from_string('posts')) expected = { data: { type: 'users', @@ -360,8 +360,10 @@ def as_jsonapi end it 'renders supports include parameter' do - actual = subject.render(data: @users[0], relationship: :posts, - include: 'posts.author') + actual = subject.render( + data: @users[0], relationship: :posts, + include: JSONAPI::IncludeDirective.from_string('posts.author') + ) actual_included = actual.delete(:included) expected = { From 6ab06eeaa2eb7cb0370467df563e7486af6dd08d Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 12 Sep 2017 00:43:21 +0200 Subject: [PATCH 2/5] Add changelog. --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ec6ffd9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +### Changed + +* Introduced: + * `JSONAPI::IncludeDirective.create`. + * `JSONAPI::IncludeDirective.from_string` + * `JSONAPI::IncludeDirective.from_array` + * `JSONAPI::IncludeDirective.from_hash` +* `JSONAPI::IncludeDirective.new` now part of the private API. + +# v0.2.0 + +### Added + +* Support for relationship rendering. +* Support for fragment caching. +* Support for arrays of arrays when rendering errors. From cc03f64f52090422877efd2bc789f3de06fb2907 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 12 Sep 2017 00:45:53 +0200 Subject: [PATCH 3/5] Mark JSONAPI::IncludeDirective.new part of the private API. --- lib/jsonapi/include_directive.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/include_directive.rb b/lib/jsonapi/include_directive.rb index 9a3984d..8c04666 100644 --- a/lib/jsonapi/include_directive.rb +++ b/lib/jsonapi/include_directive.rb @@ -51,7 +51,7 @@ def self.from_hash(hash, options = {}) new(Parser.parse_hash(hash), options) end - # @param include_hash [Hash{Symbol=>Hash}] + # @api private def initialize(include_hash, options = {}) @hash = include_hash.each_with_object({}) do |(key, value), hash| hash[key] = self.class.new(value, options) From b46cd1e614935a3b4092f3c24de7a90cbce4a072 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 12 Sep 2017 00:47:37 +0200 Subject: [PATCH 4/5] Update yarddoc. --- lib/jsonapi/renderer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/renderer.rb b/lib/jsonapi/renderer.rb index bb6cbd7..006ada3 100644 --- a/lib/jsonapi/renderer.rb +++ b/lib/jsonapi/renderer.rb @@ -10,8 +10,8 @@ class Renderer # #as_jsonapi)>, # nil] Primary resource(s) to be rendered. # @option errors [Array<#jsonapi_id>] Errors to be rendered. - # @option include Relationships to be included. See - # JSONAPI::IncludeDirective. + # @option include [JSONAPI::IncludeDirective] Relationships to be + # included. # @option fields [Hash{Symbol, Array}, Hash{String, Array}] # List of requested fields for some or all of the resource types. # @option meta [Hash] Non-standard top-level meta information to be From 71cd920e20d234e048cc971ebfc3be80cc594955 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 12 Sep 2017 00:48:26 +0200 Subject: [PATCH 5/5] Update changelog. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec6ffd9..468b235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * `JSONAPI::IncludeDirective.from_array` * `JSONAPI::IncludeDirective.from_hash` * `JSONAPI::IncludeDirective.new` now part of the private API. +* `JSONAPI::Renderer#render`'s `include` option now requires an instance of + `JSONAPI::IncludeDirective`. # v0.2.0 @@ -14,3 +16,9 @@ * Support for relationship rendering. * Support for fragment caching. * Support for arrays of arrays when rendering errors. + +# v0.1.3 + +# v0.1.2 + +# v0.1.1