praxis 0.18.1 → 0.19.0

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/Gemfile +2 -1
  4. data/README.md +21 -27
  5. data/lib/api_browser/app/index.html +3 -3
  6. data/lib/api_browser/app/js/app.js +23 -3
  7. data/lib/api_browser/app/js/controllers/action.js +33 -21
  8. data/lib/api_browser/app/js/controllers/controller.js +3 -25
  9. data/lib/api_browser/app/js/controllers/menu.js +61 -51
  10. data/lib/api_browser/app/js/controllers/trait.js +10 -0
  11. data/lib/api_browser/app/js/controllers/type.js +8 -5
  12. data/lib/api_browser/app/js/directives/fixed_if_fits.js +9 -2
  13. data/lib/api_browser/app/js/directives/menu_item.js +59 -0
  14. data/lib/api_browser/app/js/directives/readable_list.js +87 -0
  15. data/lib/api_browser/app/js/directives/url.js +16 -0
  16. data/lib/api_browser/app/js/factories/Configuration.js +1 -2
  17. data/lib/api_browser/app/js/factories/Documentation.js +49 -7
  18. data/lib/api_browser/app/js/factories/PageInfo.js +9 -0
  19. data/lib/api_browser/app/js/factories/normalize_attributes.js +1 -2
  20. data/lib/api_browser/app/js/factories/template_for.js +9 -4
  21. data/lib/api_browser/app/sass/modules/_sidebar.scss +54 -15
  22. data/lib/api_browser/app/sass/praxis.scss +4 -0
  23. data/lib/api_browser/app/views/action.html +72 -41
  24. data/lib/api_browser/app/views/builtin/field-selector.html +24 -0
  25. data/lib/api_browser/app/views/controller.html +9 -10
  26. data/lib/api_browser/app/views/directives/menu_item.html +8 -0
  27. data/lib/api_browser/app/views/directives/url.html +3 -0
  28. data/lib/api_browser/app/views/layout.html +2 -2
  29. data/lib/api_browser/app/views/menu.html +8 -14
  30. data/lib/api_browser/app/views/navbar.html +1 -1
  31. data/lib/api_browser/app/views/trait.html +13 -0
  32. data/lib/api_browser/app/views/type/details.html +1 -1
  33. data/lib/api_browser/app/views/type.html +1 -1
  34. data/lib/api_browser/app/views/types/embedded/field-selector.html +13 -0
  35. data/lib/api_browser/app/views/types/label/primitive.html +1 -1
  36. data/lib/api_browser/app/views/types/standalone/array.html +3 -0
  37. data/lib/praxis/action_definition.rb +15 -2
  38. data/lib/praxis/collection.rb +17 -5
  39. data/lib/praxis/controller.rb +12 -3
  40. data/lib/praxis/docs/generator.rb +11 -7
  41. data/lib/praxis/extensions/field_expansion.rb +59 -0
  42. data/lib/praxis/extensions/field_selection/field_selector.rb +125 -0
  43. data/lib/praxis/extensions/field_selection.rb +10 -0
  44. data/lib/praxis/extensions/mapper_selectors.rb +16 -0
  45. data/lib/praxis/extensions/rendering.rb +43 -0
  46. data/lib/praxis/links.rb +1 -0
  47. data/lib/praxis/media_type.rb +87 -3
  48. data/lib/praxis/media_type_collection.rb +1 -1
  49. data/lib/praxis/media_type_identifier.rb +6 -1
  50. data/lib/praxis/plugins/praxis_mapper_plugin.rb +29 -10
  51. data/lib/praxis/restful_doc_generator.rb +11 -8
  52. data/lib/praxis/tasks/api_docs.rb +6 -5
  53. data/lib/praxis/types/multipart_array.rb +1 -1
  54. data/lib/praxis/version.rb +1 -1
  55. data/lib/praxis.rb +5 -0
  56. data/praxis.gemspec +4 -3
  57. data/spec/api_browser/factories/configuration_spec.js +32 -0
  58. data/spec/api_browser/factories/documentation_spec.js +75 -25
  59. data/spec/api_browser/factories/normalize_attributes_spec.js +0 -5
  60. data/spec/praxis/{types/collection_spec.rb → collection_spec.rb} +36 -23
  61. data/spec/praxis/extensions/field_expansion_spec.rb +96 -0
  62. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +92 -0
  63. data/spec/praxis/extensions/rendering_spec.rb +63 -0
  64. data/spec/praxis/links_spec.rb +6 -0
  65. data/spec/praxis/media_type_collection_spec.rb +0 -1
  66. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  67. data/spec/praxis/media_type_spec.rb +101 -3
  68. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +33 -24
  69. data/spec/praxis/request_stages/request_stage_spec.rb +1 -1
  70. data/spec/praxis/types/multipart_array_spec.rb +14 -4
  71. data/spec/spec_app/app/controllers/instances.rb +6 -1
  72. data/spec/spec_app/config/environment.rb +2 -1
  73. data/spec/spec_app/design/resources/instances.rb +1 -0
  74. data/spec/spec_helper.rb +3 -1
  75. data/spec/support/spec_media_types.rb +224 -1
  76. metadata +50 -16
@@ -16,7 +16,7 @@
16
16
  <div class="col-md-4">
17
17
  <h4>Attributes</h4>
18
18
  <ul>
19
- <li ng-repeat="(name,value) in view.attributes">{{name}} <span class="label label-default" ng-if="value.view">rendered with {{value.view}} view</span></li>
19
+ <li ng-repeat="(name,value) in view.attributes">{{name}} <span class="label label-default" ng-if="value.view">{{value.view}} view</span></li>
20
20
  </ul>
21
21
  </div>
22
22
  <div class="col-md-8">
@@ -16,7 +16,7 @@
16
16
  <h3>Served by</h3>
17
17
  <ul>
18
18
  <li ng-repeat="controller in controllers">
19
- <a ui-sref="root.controller({version: apiVersion, controller: controller.controller})">{{ controller.name | resourceName }}</a>
19
+ <a ui-sref="root.controller({version: apiVersion, controller: controller.id})">{{ controller.name | resourceName }}</a>
20
20
  </li>
21
21
  </ul>
22
22
  </div>
@@ -0,0 +1,13 @@
1
+ <tr>
2
+ <td ng-bind-html="name | attributeName">
3
+ </td>
4
+ <td>
5
+ <a ui-sref="root.builtin.field-selector">FieldSelector</a>
6
+ </td>
7
+ <td>
8
+ <p>This attribute allows you to specify what response you would like from
9
+ the API. You can select individual fields from the schema using a comma
10
+ separated list. <a ui-sref="root.builtin.field-selector">Find out more</a>.</p>
11
+ <attribute-description attribute="details"></attribute-description>
12
+ </td>
13
+ </tr>
@@ -1 +1 @@
1
- <span>{{type.name}}</span>
1
+ <span>{{type.name | resourceName}}</span>
@@ -0,0 +1,3 @@
1
+ <p>A Collection of:</p>
2
+
3
+ <type-placeholder type="type.member_attribute.type" template="standalone" details="type.member_attribute.type.attributes"></type-placeholder>
@@ -95,7 +95,7 @@ module Praxis
95
95
  end
96
96
 
97
97
  def create_attribute(type=Attributor::Struct, **opts, &block)
98
- unless opts[:reference]
98
+ unless opts.key?(:reference)
99
99
  opts[:reference] = @reference_media_type if @reference_media_type && block
100
100
  end
101
101
 
@@ -222,7 +222,7 @@ module Praxis
222
222
  hash[:metadata] = metadata
223
223
  if headers
224
224
  headers_example = headers.example(context)
225
- hash[:headers] = headers.describe(example: headers_example)
225
+ hash[:headers] = headers_description(example: headers_example)
226
226
  end
227
227
  if params
228
228
  params_example = params.example(context)
@@ -249,6 +249,14 @@ module Praxis
249
249
  end
250
250
  end
251
251
 
252
+ def headers_description(example: )
253
+ output = headers.describe(example: example)
254
+ required_headers = self.headers.attributes.select{|k,attr| attr.options && attr.options[:required] == true }
255
+ output[:example] = required_headers.each_with_object({}) do | (name, attr), hash |
256
+ hash[name] = example[name].to_s # Some simple types (like Boolean) can be used as header values, but must convert back to s
257
+ end
258
+ output
259
+ end
252
260
 
253
261
  def params_description(example:)
254
262
  route_params = []
@@ -270,6 +278,11 @@ module Praxis
270
278
  end
271
279
  desc[:type][:attributes][k][:source] = source
272
280
  end
281
+ required_params = desc[:type][:attributes].select{|k,v| v[:source] == 'query' && v[:required] == true }.keys
282
+ phash = required_params.each_with_object({}) do | name, hash |
283
+ hash[name] = example[name]
284
+ end
285
+ desc[:example] = URI.encode_www_form(phash)
273
286
  desc
274
287
  end
275
288
 
@@ -1,5 +1,5 @@
1
1
  module Praxis
2
- class Collection < Attributor::Collection
2
+ class Collection < Attributor::Collection
3
3
  include Types::MediaTypeCommon
4
4
 
5
5
  def self.of(type)
@@ -8,12 +8,10 @@ module Praxis
8
8
  end
9
9
 
10
10
  klass = super
11
+ klass.anonymous_type
11
12
 
12
13
  if type < Praxis::Types::MediaTypeCommon
13
- unless type.identifier.nil?
14
- klass.identifier(type.identifier + ';type=collection')
15
- end
16
-
14
+ klass.member_type type
17
15
  type.const_set :Collection, klass
18
16
  else
19
17
  warn "DEPRECATION: Praxis::Collection.of() for non-MediaTypes will be unsupported in 1.0. Use Attributor::Collection.of() instead."
@@ -25,10 +23,24 @@ module Praxis
25
23
  def self.member_type(type=nil)
26
24
  unless type.nil?
27
25
  @member_type = type
26
+ @views = nil
27
+ self.identifier(type.identifier + ';type=collection') unless type.identifier.nil?
28
28
  end
29
29
 
30
30
  @member_type
31
31
  end
32
32
 
33
+ def self.views
34
+ @views ||= begin
35
+ @member_type.views.each_with_object(Hash.new) do |(name, view), hash|
36
+ hash[name] = Praxis::CollectionView.new(name, @member_type, view)
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.domain_model
42
+ @member_type.domain_model
43
+ end
44
+
33
45
  end
34
46
  end
@@ -4,10 +4,10 @@ require 'active_support/all'
4
4
  module Praxis
5
5
  module Controller
6
6
  extend ::ActiveSupport::Concern
7
-
7
+
8
8
  # A Controller always requires the callbacks
9
9
  include Praxis::Callbacks
10
-
10
+
11
11
  included do
12
12
  attr_reader :request
13
13
  attr_accessor :response
@@ -22,7 +22,7 @@ module Praxis
22
22
  definition.controller = self
23
23
  Application.instance.controllers << self
24
24
  end
25
-
25
+
26
26
  def id
27
27
  self.name.gsub('::'.freeze,'-'.freeze)
28
28
  end
@@ -36,5 +36,14 @@ module Praxis
36
36
  def definition
37
37
  self.class.definition
38
38
  end
39
+
40
+ def media_type
41
+ if (response_definition = self.request.action.responses[self.response.name])
42
+ return response_definition.media_type
43
+ else
44
+ self.definition.media_type
45
+ end
46
+ end
47
+
39
48
  end
40
49
  end
@@ -35,7 +35,8 @@ module Praxis
35
35
  end
36
36
 
37
37
  def save!
38
- write_index_file
38
+ # Restrict the versions listed in the index file to the ones for which we have at least 1 resource
39
+ write_index_file( for_versions: resources_by_version.keys )
39
40
  resources_by_version.keys.each do |version|
40
41
  write_version_file(version)
41
42
  end
@@ -97,9 +98,9 @@ module Praxis
97
98
  reachable_types
98
99
  end
99
100
 
100
- def write_index_file
101
+ def write_index_file( for_versions: )
101
102
  # Gather the versions
102
- versions = infos_by_version.keys.reject{|v| v == :global || v == :traits }.map do |version|
103
+ versions = infos_by_version.keys.reject{|v| v == :global || v == :traits || !for_versions.include?(v) }.map do |version|
103
104
  version == "n/a" ? "unversioned" : version
104
105
  end
105
106
  data = {
@@ -118,18 +119,17 @@ module Praxis
118
119
  # Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
119
120
  version_info[:traits] = infos_by_version[:traits]
120
121
  dumped_resources = dump_resources( resources_by_version[version] )
121
- found_media_types = resources_by_version[version].collect {|r| r.media_type.describe }
122
+ found_media_types = resources_by_version[version].select{|r| r.media_type}.collect {|r| r.media_type.describe }
122
123
 
123
124
  collected_types = Set.new
124
125
  collect_reachable_types( dumped_resources, collected_types );
125
126
  collect_reachable_types( found_media_types , collected_types );
126
127
 
127
128
  dumped_schemas = dump_schemas( collected_types )
128
-
129
129
  full_data = {
130
130
  info: version_info[:info],
131
131
  resources: dumped_resources,
132
- schemas: nil,#dumped_schemas,
132
+ schemas: dumped_schemas,
133
133
  traits: version_info[:traits] || []
134
134
  }
135
135
  # Write the file
@@ -169,9 +169,13 @@ module Praxis
169
169
  def dump_schemas(types)
170
170
  reportable_types = types - EXCLUDED_TYPES_FROM_OUTPUT
171
171
  reportable_types.each_with_object({}) do |type, array|
172
+ next if ( type.respond_to?(:anonymous?) && type.anonymous? )
173
+
172
174
  context = [type.id]
173
175
  example_data = type.example(context)
174
176
  type_output = type.describe(false, example: example_data)
177
+
178
+ type_output[:display_name] = type.display_name if type.respond_to?(:display_name)
175
179
  unless type_output[:display_name]
176
180
  # For non MediaTypes or pure types or anonymous types fallback to their name, and worst case to their id
177
181
  type_output[:display_name] = type_output[:name] || type_output[:id]
@@ -205,4 +209,4 @@ module Praxis
205
209
 
206
210
  end
207
211
  end
208
- end
212
+ end
@@ -0,0 +1,59 @@
1
+ module Praxis
2
+ module Extensions
3
+ module FieldExpansion
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ Praxis::ActionDefinition.send(:include, ActionDefinitionExtension)
8
+ end
9
+
10
+
11
+ def expanded_fields
12
+ @expansion ||= request.action.expanded_fields(self.request, self.media_type)
13
+ end
14
+
15
+
16
+ module ActionDefinitionExtension
17
+ extend ActiveSupport::Concern
18
+
19
+ def expanded_fields(request, media_type)
20
+ use_fields = self.params.attributes.key?(:fields)
21
+ use_view = self.params.attributes.key?(:view)
22
+
23
+ # Determine what, if any, fields to display.
24
+ fields = if use_fields
25
+ request.params.fields.fields
26
+ else
27
+ true
28
+ end
29
+
30
+ # Determine the view that COULD be applicable.
31
+ view = if use_view && (view_name = request.params.view)
32
+ media_type.views[view_name]
33
+ else
34
+ media_type.views[:default]
35
+ end
36
+
37
+ expandable = if fields == true
38
+ # We want to show ALL of the available fields.
39
+ # This can never be applied to the type (it's likely infinitely recursive).
40
+ # So use view_name determimed above.
41
+ view
42
+ else
43
+ # We want to show SOME of fields available on a view or type.
44
+ if use_view && request.params.view
45
+ # Use the requested view.
46
+ view
47
+ else
48
+ # Use the type.
49
+ media_type
50
+ end
51
+ end
52
+
53
+ Praxis::FieldExpander.expand(expandable,fields)
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,125 @@
1
+
2
+ module Praxis
3
+ module Extensions
4
+ module FieldSelection
5
+
6
+ class FieldSelector
7
+ include Attributor::Type
8
+
9
+ def self.native_type
10
+ self
11
+ end
12
+
13
+ def self.display_name
14
+ 'FieldSelector'
15
+ end
16
+
17
+ def self.family
18
+ 'string'
19
+ end
20
+
21
+ def self.for(media_type)
22
+ unless media_type < Praxis::MediaType
23
+ raise ArgumentError, "Invalid type: #{media_type.name} for FieldSelector. " +
24
+ "Must be a subclass of MediaType"
25
+ end
26
+
27
+ ::Class.new(self) do
28
+ @media_type = media_type
29
+ end
30
+ end
31
+
32
+ def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
33
+ return value if value.kind_of?(self.native_type)
34
+
35
+ if value.nil? || value.blank?
36
+ self.new(true)
37
+ else
38
+ parsed = Attributor::FieldSelector.load(value)
39
+ self.new(parsed)
40
+ end
41
+ end
42
+
43
+ def self.example(context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
44
+ fields = if media_type
45
+ media_type.attributes.keys.sample(3).join(',')
46
+ else
47
+ Attributor::FieldSelector.example(context,**options)
48
+ end
49
+ self.load(fields)
50
+ end
51
+
52
+ def self.validate(value, context=Attributor::DEFAULT_ROOT_CONTEXT, _attribute=nil)
53
+ return [] unless media_type
54
+ instance = self.load(value, context)
55
+ instance.validate(context)
56
+ end
57
+
58
+ def self.dump(value,**opts)
59
+ self.load(value).dump
60
+ end
61
+
62
+ class << self
63
+ attr_reader :media_type
64
+ end
65
+
66
+ attr_reader :fields
67
+
68
+ def initialize(fields)
69
+ @fields = fields
70
+ end
71
+
72
+ def dump(*args)
73
+ return '' if self.fields == true
74
+ _dump(self.fields)
75
+ end
76
+
77
+ def _dump(fields)
78
+ fields.each_with_object([]) do |(field, spec), array|
79
+ if spec == true
80
+ array << field
81
+ else
82
+ array << "#{field}{#{_dump(spec)}}"
83
+ end
84
+ end.join(',')
85
+ end
86
+
87
+ def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
88
+ errors = []
89
+ return errors if self.fields == true
90
+ _validate(self.class.media_type, fields)
91
+ end
92
+
93
+ def _validate(type, fields, context=Attributor::DEFAULT_ROOT_CONTEXT)
94
+ errors = []
95
+ fields.each do |name, field_spec|
96
+ unless type.attributes.key?(name)
97
+ errors << "Attribute with name #{name} not found in #{Attributor.type_name(type)}"
98
+ next
99
+ end
100
+
101
+ if field_spec.kind_of?(Hash)
102
+ sub_context = context + [name]
103
+ sub_attribute = type.attributes[name]
104
+ sub_type = sub_attribute.type
105
+ if sub_attribute.type.respond_to?(:member_attribute)
106
+ sub_type = sub_type.member_type
107
+ end
108
+ errors.push(*_validate(sub_type,field_spec, sub_context))
109
+ end
110
+ end
111
+ errors
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+ end
118
+ end
119
+
120
+ # Alias it to a much shorter and sweeter name in the Types namespace.
121
+ module Praxis
122
+ module Types
123
+ FieldSelector = Praxis::Extensions::FieldSelection::FieldSelector
124
+ end
125
+ end
@@ -0,0 +1,10 @@
1
+ require 'attributor/extras/field_selector'
2
+
3
+ require 'praxis/extensions/field_selection/field_selector'
4
+
5
+ module Praxis
6
+ module Extensions
7
+ module FieldSelection
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ module Praxis
2
+ module Extensions
3
+ module MapperSelectors
4
+ extend ActiveSupport::Concern
5
+ include FieldExpansion
6
+
7
+ def set_selectors
8
+ return unless self.media_type.respond_to?(:domain_model) &&
9
+ self.media_type.domain_model < Praxis::Mapper::Resource
10
+
11
+ resolved = Praxis::MediaType::FieldResolver.resolve(self.media_type, self.expanded_fields)
12
+ identity_map.add_selectors(self.media_type.domain_model, resolved)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ module Praxis
2
+ module Extensions
3
+
4
+ module Rendering
5
+ extend ActiveSupport::Concern
6
+ include FieldExpansion
7
+
8
+ def render(object, include_nil: false)
9
+ loaded = self.media_type.load(object)
10
+ renderer = Praxis::Renderer.new(include_nil: include_nil)
11
+ renderer.render(loaded, self.expanded_fields)
12
+ rescue Attributor::DumpError
13
+ if self.media_type.domain_model == Object
14
+ warn "Detected the rendering of an object of type #{self.media_type} without having a domain object model set.\n" +
15
+ "Did you forget to define it?"
16
+ end
17
+ raise
18
+ end
19
+
20
+ def display(object, include_nil: false, encoder: self.default_encoder )
21
+ identifier = Praxis::MediaTypeIdentifier.load(self.media_type.identifier)
22
+ identifier += encoder unless encoder.blank?
23
+ response.headers['Content-Type'] = identifier.to_s
24
+ response.body = render(object, include_nil: include_nil)
25
+ response
26
+ rescue Praxis::Renderer::CircularRenderingError => e
27
+ Praxis::Application.instance.validation_handler.handle!(
28
+ summary: "Circular Rendering Error when rendering response. " +
29
+ "Please especify a view to narrow the dependent fields, or narrow your field set.",
30
+ exception: e,
31
+ request: request,
32
+ stage: :action,
33
+ errors: nil
34
+ )
35
+ end
36
+
37
+ def default_encoder
38
+ ''
39
+ end
40
+
41
+ end
42
+ end
43
+ end
data/lib/praxis/links.rb CHANGED
@@ -32,6 +32,7 @@ module Praxis
32
32
  klass = Class.new(self) do
33
33
  @reference = reference
34
34
  @links = Hash.new
35
+ anonymous_type
35
36
  end
36
37
 
37
38
  reference.const_set :Links, klass
@@ -55,6 +55,7 @@ module Praxis
55
55
  # end
56
56
  # end
57
57
  class MediaType < Praxis::Blueprint
58
+
58
59
  include Types::MediaTypeCommon
59
60
 
60
61
  class DSLCompiler < Attributor::DSLCompiler
@@ -69,10 +70,11 @@ module Praxis
69
70
 
70
71
  def self._finalize!
71
72
  super
73
+
74
+ # Only define our special links accessor if it was setup using the special DSL
75
+ # (we might have an app defining an attribute called `links` on its own, in which
76
+ # case we leave it be)
72
77
  if @attribute && self.attributes.key?(:links) && self.attributes[:links].type < Praxis::Links
73
- # Only define out special links accessor if it was setup using the special DSL
74
- # (we might have an app defining an attribute called `links` on its own, in which
75
- # case we leave it be)
76
78
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
77
79
  def links
78
80
  self.class::Links.new(@object)
@@ -81,6 +83,88 @@ module Praxis
81
83
  end
82
84
  end
83
85
 
86
+
87
+ class FieldResolver
88
+ def self.resolve(type,fields)
89
+ self.new.resolve(type,fields)
90
+ end
91
+
92
+ attr_reader :history
93
+
94
+ def initialize
95
+ @history = Hash.new do |hash,key|
96
+ hash[key] = Hash.new
97
+ end
98
+ end
99
+
100
+ def resolve(type,fields)
101
+ history_key = fields
102
+ history_type = type
103
+ if fields.kind_of?(Array)
104
+ loop do
105
+ type = type.member_attribute.type
106
+ fields = fields.first
107
+ break unless fields.kind_of?(Array)
108
+ end
109
+ end
110
+
111
+ return true if fields == true
112
+
113
+ if history[history_type].include? history_key
114
+ return history[history_type][history_key]
115
+ end
116
+
117
+ result = history[history_type][history_key] = {}
118
+
119
+
120
+ fields.each do |name, sub_fields|
121
+ # skip links and do them below
122
+ next if name == :links && defined?(type::Links)
123
+
124
+ new_type = type.attributes[name].type
125
+ result[name] = resolve(new_type, sub_fields)
126
+ end
127
+
128
+ # now to tackle whatever links there may be
129
+ if (links_fields = fields[:links])
130
+ resolved_links = resolve_links(type::Links, links_fields)
131
+ self.deep_merge(result, resolved_links)
132
+ end
133
+
134
+ result
135
+ end
136
+
137
+ def resolve_links(links_type, links)
138
+ links.each_with_object({}) do |(name, link_fields), hash|
139
+ using = links_type.links[name]
140
+ new_type = links_type.attributes[name].type
141
+ hash[using] = resolve(new_type, link_fields)
142
+ end
143
+ end
144
+
145
+ # perform a deep recursive *in place* merge
146
+ # form all values in +source+ onto +target+
147
+ #
148
+ # note: can not use ActiveSupport's Hash#deep_merge! because it does not
149
+ # properly do a recursive `deep_merge!`, but instead does `deep_merge`,
150
+ # which destroys the self-referential behavior of field hashes.
151
+ #
152
+ # note: unlike Hash#merge, doesn't take a block.
153
+ def deep_merge(target, source)
154
+ source.each do |current_key, source_value|
155
+ target_value = target[current_key]
156
+
157
+ target[current_key] = if target_value.is_a?(Hash) && source_value.is_a?(Hash)
158
+ deep_merge(target_value, source_value)
159
+ else
160
+ source_value
161
+ end
162
+ end
163
+ target
164
+ end
165
+
166
+ end
167
+
84
168
  end
85
169
 
86
170
  end
@@ -104,7 +104,7 @@ module Praxis
104
104
  def self.member_view(name, using: nil)
105
105
  if using
106
106
  member_view = self.member_type.view(using)
107
- return self.views[name] = CollectionView.new(name, self, member_view)
107
+ return self.views[name] = CollectionView.new(name, self.member_type, member_view)
108
108
  end
109
109
 
110
110
  self.views[name]
@@ -1,4 +1,5 @@
1
1
  require 'set'
2
+ require 'active_support/core_ext/object/blank'
2
3
 
3
4
  module Praxis
4
5
  # Ruby object representation of an Internet Media Type Identifier as defined by
@@ -41,7 +42,9 @@ module Praxis
41
42
  # @return [MediaTypeIdentifier]
42
43
  # @see Attributor::Model#load
43
44
  def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
44
- if value.is_a?(String)
45
+ case value
46
+ when String
47
+ return nil if value.blank?
45
48
  base, *parameters = value.split(PARAMETER_SEPARATOR)
46
49
  match = VALID_TYPE.match(base)
47
50
 
@@ -62,6 +65,8 @@ module Praxis
62
65
  obj.parameters = {}
63
66
  end
64
67
  obj
68
+ when nil
69
+ return nil
65
70
  else
66
71
  super
67
72
  end