praxis 0.18.1 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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