jsonapi-renderer 0.1.1.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3f02425bfc2d75e4ea21cd4ecb42b6378b4c6939
4
+ data.tar.gz: 14400fddf957704ea29b97b422a0f814bad5f3a2
5
+ SHA512:
6
+ metadata.gz: 57a9d3c9d252fa70c29ee3734b322c8254b527b13661c226184cd3d4304eaaf9ae12b88a9c105a21f826b61f07fc71f6cd02e31f19c3a634001a4f178e204a26
7
+ data.tar.gz: c620d458eb042cc288c5b461af20181acc0e75dd9d0c0509c9f3bc6b9cb7edb1711db86bdbc1aa5c5805f882c148a1c4d57e708bcccfcb6fa4962a2b628c0555
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # jsonapi-renderer
2
+ Ruby gem for rendering [JSON API](http://jsonapi.org) documents.
3
+
4
+ ## Installation
5
+ ```ruby
6
+ # In Gemfile
7
+ gem 'jsonapi-renderer'
8
+ ```
9
+ then
10
+ ```
11
+ $ bundle
12
+ ```
13
+ or manually via
14
+ ```
15
+ $ gem install jsonapi-renderer
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ First, require the gem:
21
+ ```ruby
22
+ require 'jsonapi/renderer'
23
+ ```
24
+
25
+ ### Rendering resources
26
+
27
+ A resource here is any class that implements the following interface:
28
+ ```ruby
29
+ class ResourceInterface
30
+ # Returns the type of the resource.
31
+ # @return [String]
32
+ def jsonapi_type; end
33
+
34
+ # Returns the id of the resource.
35
+ # @return [String]
36
+ def jsonapi_id; end
37
+
38
+ # Returns a hash containing, for each included relationship, the resource(s)
39
+ # to be included from that one.
40
+ # @param [Array<Symbol>] included_relationships The keys of the relationships
41
+ # to be included.
42
+ # @return [Hash{Symbol => #ResourceInterface, Array<#ResourceInterface>}]
43
+ def jsonapi_related(included_relationships); end
44
+
45
+ # Returns a JSON API-compliant representation of the resource as a hash.
46
+ # @param [Hash] options
47
+ # @option [Array<Symbol>, Nil] fields The requested fields, or nil.
48
+ # @option [Array<Symbol>, Nil] included The requested included
49
+ # relationships, or nil.
50
+ # @return [Hash]
51
+ def as_jsonapi(options = {}); end
52
+ ```
53
+
54
+ #### Rendering a single resource
55
+ ```ruby
56
+ JSONAPI.render(resource,
57
+ include: include_string,
58
+ fields: fields_hash,
59
+ meta: meta_hash,
60
+ links: links_hash)
61
+ ```
62
+
63
+ This returns a JSON API compliant hash representing the described document.
64
+
65
+ #### Rendering a collection of resources
66
+ ```ruby
67
+ JSONAPI.render(resources,
68
+ include: include_string,
69
+ fields: fields_hash,
70
+ meta: meta_hash,
71
+ links: links_hash)
72
+ ```
73
+
74
+ This returns a JSON API compliant hash representing the described document.
75
+
76
+ ### Rendering errors
77
+
78
+ ```ruby
79
+ JSONAPI.render_errors(errors,
80
+ meta: meta_hash,
81
+ links: links_hash)
82
+ ```
83
+
84
+ where `errors` is an array of objects implementing the `as_jsonapi` method, that
85
+ returns a JSON API-compliant representation of the error.
86
+
87
+ This returns a JSON API compliant hash representing the described document.
88
+
89
+ ## License
90
+
91
+ jsonapi-renderer is released under the [MIT License](http://www.opensource.org/licenses/MIT).
@@ -0,0 +1,65 @@
1
+ module JSONAPI
2
+ class IncludeDirective
3
+ # Utilities to create an IncludeDirective hash from various types of
4
+ # inputs.
5
+ module Parser
6
+ module_function
7
+
8
+ # @api private
9
+ def parse_include_args(include_args)
10
+ case include_args
11
+ when Symbol
12
+ { include_args => {} }
13
+ when Hash
14
+ parse_hash(include_args)
15
+ when Array
16
+ parse_array(include_args)
17
+ when String
18
+ parse_string(include_args)
19
+ else
20
+ {}
21
+ end
22
+ end
23
+
24
+ # @api private
25
+ def parse_string(include_string)
26
+ include_string.split(',')
27
+ .each_with_object({}) do |path, hash|
28
+ deep_merge!(hash, parse_path_string(path))
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ def parse_path_string(include_path)
34
+ include_path.split('.')
35
+ .reverse
36
+ .reduce({}) { |a, e| { e.to_sym => a } }
37
+ end
38
+
39
+ # @api private
40
+ def parse_hash(include_hash)
41
+ include_hash.each_with_object({}) do |(key, value), hash|
42
+ hash[key.to_sym] = parse_include_args(value)
43
+ end
44
+ end
45
+
46
+ # @api private
47
+ def parse_array(include_array)
48
+ include_array.each_with_object({}) do |x, hash|
49
+ deep_merge!(hash, parse_include_args(x))
50
+ end
51
+ end
52
+
53
+ # @api private
54
+ def deep_merge!(src, ext)
55
+ ext.each do |k, v|
56
+ if src[k].is_a?(Hash) && v.is_a?(Hash)
57
+ deep_merge!(src[k], v)
58
+ else
59
+ src[k] = v
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,72 @@
1
+ require 'jsonapi/include_directive/parser'
2
+
3
+ module JSONAPI
4
+ # Represent a recursive set of include directives
5
+ # (c.f. http://jsonapi.org/format/#fetching-includes)
6
+ #
7
+ # Addition to the spec: two wildcards, namely '*' and '**'.
8
+ # The former stands for any one level of relationship, and the latter stands
9
+ # for any number of levels of relationships.
10
+ # @example 'posts.*' # => Include related posts, and all the included posts'
11
+ # related resources.
12
+ # @example 'posts.**' # => Include related posts, and all the included
13
+ # posts' related resources, and their related resources, recursively.
14
+ class IncludeDirective
15
+ # @param include_args (see Parser.parse_include_args)
16
+ def initialize(include_args, options = {})
17
+ include_hash = Parser.parse_include_args(include_args)
18
+ @hash = include_hash.each_with_object({}) do |(key, value), hash|
19
+ hash[key] = self.class.new(value, options)
20
+ end
21
+ @options = options
22
+ end
23
+
24
+ # @param key [Symbol, String]
25
+ def key?(key)
26
+ @hash.key?(key.to_sym) ||
27
+ (@options[:allow_wildcard] && (@hash.key?(:*) || @hash.key?(:**)))
28
+ end
29
+
30
+ # @return [Array<Symbol>]
31
+ def keys
32
+ @hash.keys
33
+ end
34
+
35
+ # @param key [Symbol, String]
36
+ # @return [IncludeDirective, nil]
37
+ def [](key)
38
+ case
39
+ when @hash.key?(key.to_sym)
40
+ @hash[key.to_sym]
41
+ when @options[:allow_wildcard] && @hash.key?(:**)
42
+ self.class.new({ :** => {} }, @options)
43
+ when @options[:allow_wildcard] && @hash.key?(:*)
44
+ @hash[:*]
45
+ end
46
+ end
47
+
48
+ # @return [Hash{Symbol => Hash}]
49
+ def to_hash
50
+ @hash.each_with_object({}) do |(key, value), hash|
51
+ hash[key] = value.to_hash
52
+ end
53
+ end
54
+
55
+ # @return [String]
56
+ def to_string
57
+ string_array = @hash.map do |(key, value)|
58
+ string_value = value.to_string
59
+ if string_value == ''
60
+ key.to_s
61
+ else
62
+ string_value
63
+ .split(',')
64
+ .map { |x| key.to_s + '.' + x }
65
+ .join(',')
66
+ end
67
+ end
68
+
69
+ string_array.join(',')
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,118 @@
1
+ require 'jsonapi/include_directive'
2
+
3
+ module JSONAPI
4
+ class Renderer
5
+ def initialize(resources, options = {})
6
+ @resources = resources
7
+ @errors = options[:errors] || false
8
+ @meta = options[:meta] || nil
9
+ @links = options[:links] || {}
10
+ @fields = options[:fields] || {}
11
+ # NOTE(beauby): Room for some nifty defaults on those.
12
+ @jsonapi = options[:jsonapi_object] || nil
13
+ @include = JSONAPI::IncludeDirective.new(options[:include] || {})
14
+ end
15
+
16
+ def as_json
17
+ return @json unless @json.nil?
18
+
19
+ process_resources
20
+ @json = {}
21
+
22
+ if @errors
23
+ @json[:errors] = @resources.map(&:as_jsonapi)
24
+ else
25
+ @json[:data] = @resources.respond_to?(:each) ? @primary : @primary[0]
26
+ @json[:included] = @included if @included.any?
27
+ end
28
+ @json[:links] = @links if @links.any?
29
+ @json[:meta] = @meta unless @meta.nil?
30
+ @json[:jsonapi] = @jsonapi unless @jsonapi.nil?
31
+
32
+ @json
33
+ end
34
+
35
+ private
36
+
37
+ def process_resources
38
+ @primary = []
39
+ @included = []
40
+ @hashes = {}
41
+ @processed = Set.new # NOTE(beauby): Set of [type, id, prefix].
42
+ @queue = []
43
+
44
+ Array(@resources).each do |res|
45
+ process_resource(res, '', @include, true)
46
+ @processed.add([res.jsonapi_type, res.jsonapi_id, ''])
47
+ end
48
+ until @queue.empty?
49
+ res, prefix, include_dir = @queue.pop
50
+ process_resource(res, prefix, include_dir, false)
51
+ end
52
+ end
53
+
54
+ def merge_resources!(a, b)
55
+ b[:relationships].each do |name, rel|
56
+ a[:relationships][name][:data] ||= rel[:data] if rel.key?(:data)
57
+ (a[:relationships][name][:links] ||= {})
58
+ .merge!(rel[:links]) if rel.key?(:links)
59
+ end
60
+ end
61
+
62
+ def process_resource(res, prefix, include_dir, is_primary)
63
+ ri = [res.jsonapi_type, res.jsonapi_id]
64
+ hash = res.as_jsonapi(fields: @fields[res.jsonapi_type.to_sym],
65
+ include: include_dir.keys)
66
+ if @hashes.key?(ri)
67
+ merge_resources!(@hashes[ri], hash)
68
+ else
69
+ (is_primary ? @primary : @included) << (@hashes[ri] = hash)
70
+ end
71
+ process_relationships(res, prefix, include_dir)
72
+ end
73
+
74
+ def process_relationships(res, prefix, include_dir)
75
+ res.jsonapi_related(include_dir.keys).each do |key, data|
76
+ Array(data).each do |child_res|
77
+ child_prefix = "#{prefix}.#{key}"
78
+ next unless @processed.add?([child_res.jsonapi_type,
79
+ child_res.jsonapi_id,
80
+ child_prefix])
81
+ @queue << [child_res, child_prefix, include_dir[key]]
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ module_function
88
+
89
+ # Render a success JSON API document.
90
+ #
91
+ # @param [(#jsonapi_id, #jsonapi_type, #jsonapi_related, #as_jsonapi),
92
+ # Array<(#jsonapi_id, #jsonapi_type, #jsonapi_related, #as_jsonapi)>,
93
+ # nil] resources The primary resource(s) to be rendered.
94
+ # @param [Hash] options All optional.
95
+ # @option [String, Hash{Symbol => Hash}] include Relationships to be
96
+ # included.
97
+ # @option [Hash{Symbol, Array<Symbol>}] fields List of requested fields
98
+ # for some or all of the resource types.
99
+ # @option [Hash] meta Non-standard top-level meta information to be
100
+ # included.
101
+ # @option [Hash] links Top-level links to be included.
102
+ # @option [Hash] jsonapi_object JSON API object.
103
+ def render(resources, options = {})
104
+ Renderer.new(resources, options).as_json
105
+ end
106
+
107
+ # Render an error JSON API document.
108
+ #
109
+ # @param [Array<#jsonapi_id>] errors Errors to be rendered.
110
+ # @param [Hash] options All optional.
111
+ # @option [Hash] meta Non-standard top-level meta information to be
112
+ # included.
113
+ # @option [Hash] links Top-level links to be included.
114
+ # @option [Hash] jsonapi_object JSON API object.
115
+ def render_errors(errors)
116
+ Renderer.new(errors, errors: true).as_json
117
+ end
118
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsonapi-renderer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1.beta1
5
+ platform: ruby
6
+ authors:
7
+ - Lucas Hosseini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Low-level renderer for JSONAPI documents.
14
+ email: lucas.hosseini@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/jsonapi/include_directive.rb
21
+ - lib/jsonapi/include_directive/parser.rb
22
+ - lib/jsonapi/renderer.rb
23
+ homepage: https://github.com/beauby/jsonapi
24
+ licenses:
25
+ - MIT
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.3.1
41
+ requirements: []
42
+ rubyforge_project:
43
+ rubygems_version: 2.5.1
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Render JSONAPI documents.
47
+ test_files: []