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