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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -1
- data/Gemfile +2 -1
- data/README.md +21 -27
- data/lib/api_browser/app/index.html +3 -3
- data/lib/api_browser/app/js/app.js +23 -3
- data/lib/api_browser/app/js/controllers/action.js +33 -21
- data/lib/api_browser/app/js/controllers/controller.js +3 -25
- data/lib/api_browser/app/js/controllers/menu.js +61 -51
- data/lib/api_browser/app/js/controllers/trait.js +10 -0
- data/lib/api_browser/app/js/controllers/type.js +8 -5
- data/lib/api_browser/app/js/directives/fixed_if_fits.js +9 -2
- data/lib/api_browser/app/js/directives/menu_item.js +59 -0
- data/lib/api_browser/app/js/directives/readable_list.js +87 -0
- data/lib/api_browser/app/js/directives/url.js +16 -0
- data/lib/api_browser/app/js/factories/Configuration.js +1 -2
- data/lib/api_browser/app/js/factories/Documentation.js +49 -7
- data/lib/api_browser/app/js/factories/PageInfo.js +9 -0
- data/lib/api_browser/app/js/factories/normalize_attributes.js +1 -2
- data/lib/api_browser/app/js/factories/template_for.js +9 -4
- data/lib/api_browser/app/sass/modules/_sidebar.scss +54 -15
- data/lib/api_browser/app/sass/praxis.scss +4 -0
- data/lib/api_browser/app/views/action.html +72 -41
- data/lib/api_browser/app/views/builtin/field-selector.html +24 -0
- data/lib/api_browser/app/views/controller.html +9 -10
- data/lib/api_browser/app/views/directives/menu_item.html +8 -0
- data/lib/api_browser/app/views/directives/url.html +3 -0
- data/lib/api_browser/app/views/layout.html +2 -2
- data/lib/api_browser/app/views/menu.html +8 -14
- data/lib/api_browser/app/views/navbar.html +1 -1
- data/lib/api_browser/app/views/trait.html +13 -0
- data/lib/api_browser/app/views/type/details.html +1 -1
- data/lib/api_browser/app/views/type.html +1 -1
- data/lib/api_browser/app/views/types/embedded/field-selector.html +13 -0
- data/lib/api_browser/app/views/types/label/primitive.html +1 -1
- data/lib/api_browser/app/views/types/standalone/array.html +3 -0
- data/lib/praxis/action_definition.rb +15 -2
- data/lib/praxis/collection.rb +17 -5
- data/lib/praxis/controller.rb +12 -3
- data/lib/praxis/docs/generator.rb +11 -7
- data/lib/praxis/extensions/field_expansion.rb +59 -0
- data/lib/praxis/extensions/field_selection/field_selector.rb +125 -0
- data/lib/praxis/extensions/field_selection.rb +10 -0
- data/lib/praxis/extensions/mapper_selectors.rb +16 -0
- data/lib/praxis/extensions/rendering.rb +43 -0
- data/lib/praxis/links.rb +1 -0
- data/lib/praxis/media_type.rb +87 -3
- data/lib/praxis/media_type_collection.rb +1 -1
- data/lib/praxis/media_type_identifier.rb +6 -1
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +29 -10
- data/lib/praxis/restful_doc_generator.rb +11 -8
- data/lib/praxis/tasks/api_docs.rb +6 -5
- data/lib/praxis/types/multipart_array.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +5 -0
- data/praxis.gemspec +4 -3
- data/spec/api_browser/factories/configuration_spec.js +32 -0
- data/spec/api_browser/factories/documentation_spec.js +75 -25
- data/spec/api_browser/factories/normalize_attributes_spec.js +0 -5
- data/spec/praxis/{types/collection_spec.rb → collection_spec.rb} +36 -23
- data/spec/praxis/extensions/field_expansion_spec.rb +96 -0
- data/spec/praxis/extensions/field_selection/field_selector_spec.rb +92 -0
- data/spec/praxis/extensions/rendering_spec.rb +63 -0
- data/spec/praxis/links_spec.rb +6 -0
- data/spec/praxis/media_type_collection_spec.rb +0 -1
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/media_type_spec.rb +101 -3
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +33 -24
- data/spec/praxis/request_stages/request_stage_spec.rb +1 -1
- data/spec/praxis/types/multipart_array_spec.rb +14 -4
- data/spec/spec_app/app/controllers/instances.rb +6 -1
- data/spec/spec_app/config/environment.rb +2 -1
- data/spec/spec_app/design/resources/instances.rb +1 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/support/spec_media_types.rb +224 -1
- 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">
|
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.
|
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>
|
@@ -95,7 +95,7 @@ module Praxis
|
|
95
95
|
end
|
96
96
|
|
97
97
|
def create_attribute(type=Attributor::Struct, **opts, &block)
|
98
|
-
unless opts
|
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] =
|
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
|
|
data/lib/praxis/collection.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Praxis
|
2
|
-
|
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
|
-
|
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
|
data/lib/praxis/controller.rb
CHANGED
@@ -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
|
-
|
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:
|
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,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
data/lib/praxis/media_type.rb
CHANGED
@@ -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
|
-
|
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
|