praxis 0.21 → 2.0.pre.3
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/.travis.yml +8 -15
- data/CHANGELOG.md +328 -299
- data/CONTRIBUTING.md +4 -4
- data/README.md +11 -9
- data/lib/api_browser/app/js/directives/attribute_table.js +2 -1
- data/lib/api_browser/app/js/directives/conditional_requirements.js +13 -0
- data/lib/api_browser/app/js/directives/type_placeholder.js +10 -1
- data/lib/api_browser/app/js/factories/normalize_attributes.js +4 -2
- data/lib/api_browser/app/js/factories/template_for.js +5 -2
- data/lib/api_browser/app/js/filters/has_requirement.js +14 -0
- data/lib/api_browser/app/js/filters/tag_requirement.js +13 -0
- data/lib/api_browser/app/sass/praxis.scss +11 -0
- data/lib/api_browser/app/views/action.html +2 -2
- data/lib/api_browser/app/views/directives/attribute_description/member_options.html +2 -2
- data/lib/api_browser/app/views/directives/attribute_table.html +1 -1
- data/lib/api_browser/app/views/type.html +1 -1
- data/lib/api_browser/app/views/type/details.html +2 -2
- data/lib/api_browser/app/views/types/embedded/array.html +2 -0
- data/lib/api_browser/app/views/types/embedded/default.html +3 -1
- data/lib/api_browser/app/views/types/embedded/requirements.html +6 -0
- data/lib/api_browser/app/views/types/embedded/single_req.html +9 -0
- data/lib/api_browser/app/views/types/embedded/struct.html +14 -2
- data/lib/api_browser/app/views/types/standalone/array.html +1 -1
- data/lib/api_browser/app/views/types/standalone/struct.html +2 -1
- data/lib/api_browser/package.json +1 -1
- data/lib/praxis.rb +9 -3
- data/lib/praxis/action_definition.rb +1 -1
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
- data/lib/praxis/application.rb +1 -9
- data/lib/praxis/bootloader.rb +1 -4
- data/lib/praxis/config.rb +1 -1
- data/lib/praxis/dispatcher.rb +10 -6
- data/lib/praxis/docs/generator.rb +2 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
- data/lib/praxis/extensions/field_selection.rb +1 -9
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +51 -0
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +61 -0
- data/lib/praxis/extensions/rails_compat.rb +2 -0
- data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
- data/lib/praxis/handlers/xml.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +98 -0
- data/lib/praxis/mapper/resource.rb +242 -0
- data/lib/praxis/mapper/selector_generator.rb +149 -0
- data/lib/praxis/mapper/sequel_compat.rb +76 -0
- data/lib/praxis/media_type_identifier.rb +2 -1
- data/lib/praxis/middleware_app.rb +20 -2
- data/lib/praxis/multipart/parser.rb +14 -2
- data/lib/praxis/notifications.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +64 -0
- data/lib/praxis/plugins/rails_plugin.rb +104 -0
- data/lib/praxis/request.rb +7 -1
- data/lib/praxis/request_superclassing.rb +11 -0
- data/lib/praxis/resource_definition.rb +5 -5
- data/lib/praxis/response.rb +1 -1
- data/lib/praxis/route.rb +1 -1
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/trait.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +2 -2
- data/lib/praxis/types/multipart.rb +1 -1
- data/lib/praxis/types/multipart_array.rb +2 -2
- data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +14 -13
- data/spec/functional_spec.rb +4 -7
- data/spec/praxis/action_definition_spec.rb +1 -1
- data/spec/praxis/application_spec.rb +1 -1
- data/spec/praxis/collection_spec.rb +3 -2
- data/spec/praxis/config_spec.rb +2 -2
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
- data/spec/praxis/handlers/xml_spec.rb +2 -2
- data/spec/praxis/mapper/resource_spec.rb +169 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +293 -0
- data/spec/praxis/media_type_spec.rb +0 -10
- data/spec/praxis/middleware_app_spec.rb +29 -9
- data/spec/praxis/request_stages/action_spec.rb +8 -1
- data/spec/praxis/response_definition_spec.rb +7 -4
- data/spec/praxis/response_spec.rb +1 -1
- data/spec/praxis/responses/internal_server_error_spec.rb +2 -2
- data/spec/praxis/responses/validation_error_spec.rb +2 -2
- data/spec/praxis/router_spec.rb +1 -1
- data/spec/spec_app/app/controllers/instances.rb +1 -1
- data/spec/spec_app/config/environment.rb +3 -21
- data/spec/spec_helper.rb +11 -15
- data/spec/support/be_deep_equal_matcher.rb +39 -0
- data/spec/support/spec_resources.rb +124 -0
- data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
- metadata +102 -77
- data/.ruby-version +0 -1
- data/lib/praxis/extensions/mapper_selectors.rb +0 -16
- data/lib/praxis/media_type_collection.rb +0 -127
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
- data/lib/praxis/stats.rb +0 -113
- data/spec/praxis/media_type_collection_spec.rb +0 -157
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
- data/spec/praxis/stats_spec.rb +0 -9
- data/spec/spec_app/app/models/person.rb +0 -3
data/lib/praxis/request.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Praxis
|
|
2
2
|
|
|
3
|
-
class Request
|
|
3
|
+
class Request < Praxis.request_superclass
|
|
4
4
|
attr_reader :env, :query
|
|
5
5
|
attr_accessor :route_params, :action
|
|
6
6
|
|
|
@@ -162,6 +162,12 @@ module Praxis
|
|
|
162
162
|
@unmatched_versions ||= Set.new
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
+
# Override the inspect instance method of a request, as, by default, the kernel inspect will go nuts
|
|
166
|
+
# traversing the action and app_instance and therefore all associated instance variables reachable through that
|
|
167
|
+
def inspect
|
|
168
|
+
"'@env' => #{@env.inspect},\n'@headers' => #{@headers.inspect},\n'@params' => #{@params.inspect},\n'@query' => #{@query.inspect}"
|
|
169
|
+
end
|
|
170
|
+
|
|
165
171
|
end
|
|
166
172
|
|
|
167
173
|
end
|
|
@@ -49,7 +49,7 @@ module Praxis
|
|
|
49
49
|
unless base_attributes.empty?
|
|
50
50
|
params do
|
|
51
51
|
base_attributes.each do |base_name, base_attribute|
|
|
52
|
-
attribute base_name, base_attribute.type, base_attribute.options
|
|
52
|
+
attribute base_name, base_attribute.type, **base_attribute.options
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -87,9 +87,9 @@ module Praxis
|
|
|
87
87
|
@display_name = string
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
def on_finalize
|
|
90
|
+
def on_finalize(&block)
|
|
91
91
|
if block_given?
|
|
92
|
-
@on_finalize <<
|
|
92
|
+
@on_finalize << proc(&block)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
@on_finalize
|
|
@@ -168,7 +168,7 @@ module Praxis
|
|
|
168
168
|
|
|
169
169
|
parent_attribute = parent_action.params.attributes[parent_name]
|
|
170
170
|
|
|
171
|
-
attribute name, parent_attribute.type, parent_attribute.options
|
|
171
|
+
attribute name, parent_attribute.type, **parent_attribute.options
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
end
|
|
@@ -221,7 +221,7 @@ module Praxis
|
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
def to_href( params )
|
|
224
|
-
canonical_path.primary_route.path.expand(params)
|
|
224
|
+
canonical_path.primary_route.path.expand(params.transform_values(&:to_s))
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
def parse_href(path)
|
data/lib/praxis/response.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Praxis
|
|
|
18
18
|
klass.status = self.status if self.status
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def initialize(status:self.class.status, headers:{}, body:
|
|
21
|
+
def initialize(status:self.class.status, headers:{}, body: nil, location: nil)
|
|
22
22
|
@name = response_name
|
|
23
23
|
@status = status
|
|
24
24
|
@headers = headers
|
data/lib/praxis/route.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Praxis
|
|
|
24
24
|
path_params = example_hash.select{|k,v| path_param_keys.include? k }
|
|
25
25
|
# Let's generate the example only using required params, to avoid mixing incompatible parameters
|
|
26
26
|
query_params = example_hash.select{|k,v| required_query_param_keys.include? k }
|
|
27
|
-
example = { verb: self.verb, url: self.path.expand(path_params), query_params: query_params }
|
|
27
|
+
example = { verb: self.verb, url: self.path.expand(path_params.transform_values(&:to_s)), query_params: query_params }
|
|
28
28
|
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -55,7 +55,7 @@ module Praxis
|
|
|
55
55
|
path = (base + path).gsub('//','/')
|
|
56
56
|
# Reject our own options
|
|
57
57
|
route_name = options.delete(:name);
|
|
58
|
-
pattern = Mustermann.new(path, {ignore_unknown_options: true}.merge( options ))
|
|
58
|
+
pattern = Mustermann.new(path, **{ignore_unknown_options: true}.merge( options ))
|
|
59
59
|
route = Route.new(verb, pattern, version, name: route_name, prefixed_path: prefixed_path, **options)
|
|
60
60
|
@routes << route
|
|
61
61
|
route
|
data/lib/praxis/trait.rb
CHANGED
|
@@ -76,7 +76,7 @@ module Praxis
|
|
|
76
76
|
dsl_compiler: ActionDefinition::HeadersDSLCompiler,
|
|
77
77
|
case_insensitive_load: true
|
|
78
78
|
}
|
|
79
|
-
Attributor::Hash.of(key: String).construct(Proc.new {}, hash_opts)
|
|
79
|
+
Attributor::Hash.of(key: String).construct(Proc.new {}, **hash_opts)
|
|
80
80
|
else
|
|
81
81
|
Attributor::Hash.construct(Proc.new {})
|
|
82
82
|
end
|
|
@@ -32,8 +32,8 @@ module Praxis
|
|
|
32
32
|
#
|
|
33
33
|
# @return [String] the string-representation of this type's identifier
|
|
34
34
|
def identifier(identifier=nil)
|
|
35
|
-
return @identifier
|
|
36
|
-
|
|
35
|
+
return @identifier unless identifier
|
|
36
|
+
@identifier = MediaTypeIdentifier.load(identifier)
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -12,7 +12,7 @@ module Praxis
|
|
|
12
12
|
return value if value.kind_of?(self) || value.nil?
|
|
13
13
|
|
|
14
14
|
unless (value.kind_of?(::String) && ! content_type.nil?)
|
|
15
|
-
raise Attributor::CoercionError
|
|
15
|
+
raise Attributor::CoercionError.new(context: context, from: value.class, to: self.name, value: value)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
headers = {'Content-Type' => content_type}
|
|
@@ -83,7 +83,7 @@ module Praxis
|
|
|
83
83
|
|
|
84
84
|
self.multiple << name if multiple
|
|
85
85
|
|
|
86
|
-
compiler = Attributor::DSLCompiler.new(self, opts)
|
|
86
|
+
compiler = Attributor::DSLCompiler.new(self, **opts)
|
|
87
87
|
|
|
88
88
|
if filename
|
|
89
89
|
filename_attribute = compiler.define('filename', String, required: true)
|
|
@@ -218,7 +218,7 @@ module Praxis
|
|
|
218
218
|
attr_accessor :preamble
|
|
219
219
|
attr_reader :content_type
|
|
220
220
|
|
|
221
|
-
def initialize(content_type: self.class.identifier)
|
|
221
|
+
def initialize(content_type: self.class.identifier.to_s)
|
|
222
222
|
self.content_type = content_type
|
|
223
223
|
end
|
|
224
224
|
|
|
@@ -14,7 +14,7 @@ module Praxis
|
|
|
14
14
|
|
|
15
15
|
def update_attribute(attribute, options, block)
|
|
16
16
|
attribute.options.merge!(options)
|
|
17
|
-
attribute.type.attributes(options, &block)
|
|
17
|
+
attribute.type.attributes(**options, &block)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def create_attribute(type=Attributor::Struct, **opts, &block)
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
|
|
12
12
|
spec.email = ["blanquer@gmail.com","dane.jensen@gmail.com"]
|
|
13
13
|
|
|
14
|
-
spec.homepage = "https://github.com/
|
|
14
|
+
spec.homepage = "https://github.com/praxis/praxis"
|
|
15
15
|
spec.license = "MIT"
|
|
16
16
|
spec.required_ruby_version = ">=2.1"
|
|
17
17
|
|
|
@@ -20,24 +20,22 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.bindir = 'bin'
|
|
21
21
|
spec.executables << 'praxis'
|
|
22
22
|
|
|
23
|
-
spec.add_dependency 'rack', '
|
|
24
|
-
spec.add_dependency 'mustermann', '
|
|
23
|
+
spec.add_dependency 'rack', '>= 1'
|
|
24
|
+
spec.add_dependency 'mustermann', '>=1.1', '<=2'
|
|
25
25
|
spec.add_dependency 'activesupport', '>= 3'
|
|
26
26
|
spec.add_dependency 'mime', '~> 0'
|
|
27
|
-
spec.add_dependency 'praxis-
|
|
28
|
-
spec.add_dependency '
|
|
29
|
-
spec.add_dependency '
|
|
30
|
-
spec.add_dependency 'thor', '~> 0.18'
|
|
27
|
+
spec.add_dependency 'praxis-blueprints', '>= 3.4'
|
|
28
|
+
spec.add_dependency 'attributor', '>= 5.4'
|
|
29
|
+
spec.add_dependency 'thor'
|
|
31
30
|
spec.add_dependency 'terminal-table', '~> 1.4'
|
|
32
|
-
spec.add_dependency 'harness', '~> 2'
|
|
33
31
|
|
|
34
|
-
spec.add_development_dependency 'bundler'
|
|
32
|
+
spec.add_development_dependency 'bundler'
|
|
35
33
|
spec.add_development_dependency 'rake', '~> 0.9'
|
|
36
34
|
spec.add_development_dependency 'rake-notes', '~> 0'
|
|
37
35
|
if RUBY_PLATFORM !~ /java/
|
|
38
|
-
spec.add_development_dependency 'pry'
|
|
39
|
-
spec.add_development_dependency 'pry-byebug'
|
|
40
|
-
spec.add_development_dependency 'pry-stack_explorer'
|
|
36
|
+
spec.add_development_dependency 'pry'
|
|
37
|
+
spec.add_development_dependency 'pry-byebug'
|
|
38
|
+
spec.add_development_dependency 'pry-stack_explorer'
|
|
41
39
|
spec.add_development_dependency 'sqlite3', '~> 1'
|
|
42
40
|
else
|
|
43
41
|
spec.add_development_dependency 'jdbc-sqlite3'
|
|
@@ -51,6 +49,9 @@ Gem::Specification.new do |spec|
|
|
|
51
49
|
spec.add_development_dependency 'rack-test', '~> 0'
|
|
52
50
|
spec.add_development_dependency 'simplecov', '~> 0'
|
|
53
51
|
spec.add_development_dependency 'fuubar', '~> 2'
|
|
54
|
-
spec.add_development_dependency 'yard',
|
|
52
|
+
spec.add_development_dependency 'yard', ">= 0.9.20"
|
|
55
53
|
spec.add_development_dependency 'coveralls'
|
|
54
|
+
# Just for the query selector extensions etc...
|
|
55
|
+
spec.add_development_dependency 'sequel', '~> 5'
|
|
56
|
+
spec.add_development_dependency 'activerecord', '> 4'
|
|
56
57
|
end
|
data/spec/functional_spec.rb
CHANGED
|
@@ -136,7 +136,6 @@ describe 'Functional specs' do
|
|
|
136
136
|
headers = last_response.headers
|
|
137
137
|
expect(headers['Content-Type']).to eq('application/json')
|
|
138
138
|
expect(headers['Spec-Middleware']).to eq('used')
|
|
139
|
-
expect(headers['Content-Length']).to eq(last_response.body.size.to_s)
|
|
140
139
|
end
|
|
141
140
|
|
|
142
141
|
it 'returns early when making the before filter break' do
|
|
@@ -333,11 +332,11 @@ describe 'Functional specs' do
|
|
|
333
332
|
|
|
334
333
|
context 'wildcard verb routing' do
|
|
335
334
|
it 'can terminate instances with POST' do
|
|
336
|
-
post '/api/clouds/23/instances/1/terminate?api_version=1.0',
|
|
335
|
+
post '/api/clouds/23/instances/1/terminate?api_version=1.0', '', 'global_session' => session
|
|
337
336
|
expect(last_response.status).to eq(200)
|
|
338
337
|
end
|
|
339
338
|
it 'can terminate instances with DELETE' do
|
|
340
|
-
post '/api/clouds/23/instances/1/terminate?api_version=1.0',
|
|
339
|
+
post '/api/clouds/23/instances/1/terminate?api_version=1.0', '', 'global_session' => session
|
|
341
340
|
expect(last_response.status).to eq(200)
|
|
342
341
|
end
|
|
343
342
|
|
|
@@ -352,18 +351,16 @@ describe 'Functional specs' do
|
|
|
352
351
|
get '/api/clouds/23/otherinstances/_action/exceptional?api_version=1.0', nil, 'global_session' => session
|
|
353
352
|
expect(last_response.status).to eq(404)
|
|
354
353
|
end
|
|
355
|
-
|
|
356
|
-
|
|
357
354
|
end
|
|
358
355
|
|
|
359
356
|
context 'auth_plugin' do
|
|
360
357
|
it 'can terminate' do
|
|
361
|
-
post '/api/clouds/23/instances/1/terminate?api_version=1.0',
|
|
358
|
+
post '/api/clouds/23/instances/1/terminate?api_version=1.0', '', 'global_session' => session
|
|
362
359
|
expect(last_response.status).to eq(200)
|
|
363
360
|
end
|
|
364
361
|
|
|
365
362
|
it 'can not stop' do
|
|
366
|
-
post '/api/clouds/23/instances/1/stop?api_version=1.0',
|
|
363
|
+
post '/api/clouds/23/instances/1/stop?api_version=1.0', '', 'global_session' => session
|
|
367
364
|
expect(last_response.status).to eq(403)
|
|
368
365
|
end
|
|
369
366
|
end
|
|
@@ -281,7 +281,7 @@ describe Praxis::ActionDefinition do
|
|
|
281
281
|
subject(:action) { resource_definition.actions[:show] }
|
|
282
282
|
|
|
283
283
|
it 'works' do
|
|
284
|
-
expansion = action.primary_route.path.expand(cloud_id:232, id: 2)
|
|
284
|
+
expansion = action.primary_route.path.expand(cloud_id:'232', id: '2')
|
|
285
285
|
expect(expansion).to eq "/api/clouds/232/instances/2"
|
|
286
286
|
end
|
|
287
287
|
|
|
@@ -27,7 +27,7 @@ describe Praxis::Application do
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
it 'passes the params and block to config' do
|
|
30
|
-
ret = app.config(:key, Attributor::Hash, {option: :one}, &myblock)
|
|
30
|
+
ret = app.config(:key, Attributor::Hash, **{option: :one}, &myblock)
|
|
31
31
|
expect(ret).to eq([:key, Attributor::Hash, {option: :one}, myblock])
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -8,6 +8,7 @@ describe Praxis::Collection do
|
|
|
8
8
|
subject!(:collection) do
|
|
9
9
|
Praxis::Collection.of(member_type)
|
|
10
10
|
end
|
|
11
|
+
let(:identifier_string) { subject.identifier.to_s }
|
|
11
12
|
|
|
12
13
|
context '.of' do
|
|
13
14
|
let(:member_type) do
|
|
@@ -16,7 +17,7 @@ describe Praxis::Collection do
|
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
it { expect(identifier_string).to eq('application/an-awesome-type; type=collection') }
|
|
20
21
|
|
|
21
22
|
it 'sets the collection on the media type' do
|
|
22
23
|
expect(member_type::Collection).to be(collection)
|
|
@@ -34,7 +35,7 @@ describe Praxis::Collection do
|
|
|
34
35
|
context 'defined explicitly' do
|
|
35
36
|
subject(:type) { Volume::Collection }
|
|
36
37
|
its(:member_type) { should be Volume }
|
|
37
|
-
|
|
38
|
+
it { expect(identifier_string).to eq('application/vnd.acme.volumes') }
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
context '.member_type' do
|
data/spec/praxis/config_spec.rb
CHANGED
|
@@ -72,12 +72,12 @@ describe Praxis::Config do
|
|
|
72
72
|
|
|
73
73
|
it 'it is not allowed if its for the top key' do
|
|
74
74
|
expect{
|
|
75
|
-
config.define nil, config_type, config_opts
|
|
75
|
+
config.define nil, config_type, **config_opts
|
|
76
76
|
}.to raise_error(/You cannot define the top level configuration with a non-Struct type/)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
before do
|
|
80
|
-
config.define config_key, config_type, config_opts
|
|
80
|
+
config.define config_key, config_type, **config_opts
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
it 'sets the attribute to the defined type' do
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
require_relative 'support/spec_resources_active_model.rb'
|
|
4
|
+
|
|
5
|
+
describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
|
|
6
|
+
let(:selector_fields) do
|
|
7
|
+
{
|
|
8
|
+
name: true,
|
|
9
|
+
author: {
|
|
10
|
+
id: true,
|
|
11
|
+
books: true
|
|
12
|
+
},
|
|
13
|
+
category: {
|
|
14
|
+
name: true,
|
|
15
|
+
books: true
|
|
16
|
+
},
|
|
17
|
+
tags: {
|
|
18
|
+
name: true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
let(:expected_select_from_to_query) do
|
|
23
|
+
# The columns to select from the top Simple model
|
|
24
|
+
[
|
|
25
|
+
:simple_name, # from the :name alias
|
|
26
|
+
:author_id, # the FK needed for the author association
|
|
27
|
+
:added_column, # from the extra column defined in the parent property
|
|
28
|
+
:category_uuid, # the FK needed for the cateory association
|
|
29
|
+
:id # We always load the primary keys
|
|
30
|
+
]
|
|
31
|
+
end
|
|
32
|
+
let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields) }
|
|
33
|
+
|
|
34
|
+
subject(:selector) {described_class.new(query: query, selectors: selector_node) }
|
|
35
|
+
context '#generate with a mocked' do
|
|
36
|
+
let(:query) { double("Query") }
|
|
37
|
+
it 'calls the select columns for the top level, and includes the right association hashes' do
|
|
38
|
+
expect(query).to receive(:select).with(*expected_select_from_to_query).and_return(query)
|
|
39
|
+
expected_includes = {
|
|
40
|
+
author: {
|
|
41
|
+
books: {}
|
|
42
|
+
},
|
|
43
|
+
category: {
|
|
44
|
+
books: {}
|
|
45
|
+
},
|
|
46
|
+
tags: {}
|
|
47
|
+
}
|
|
48
|
+
expect(query).to receive(:includes).with(expected_includes).and_return(query)
|
|
49
|
+
expect(subject).to_not receive(:explain_query)
|
|
50
|
+
subject.generate
|
|
51
|
+
end
|
|
52
|
+
it 'calls the explain debug method if enabled' do
|
|
53
|
+
expect(query).to receive(:select).and_return(query)
|
|
54
|
+
expect(query).to receive(:includes).and_return(query)
|
|
55
|
+
expect(subject).to receive(:explain_query)
|
|
56
|
+
subject.generate(debug: true)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context '#generate with a real AR model' do
|
|
61
|
+
let(:query) { ActiveBook }
|
|
62
|
+
|
|
63
|
+
it 'calls the select columns for the top level, and includes the right association hashes' do
|
|
64
|
+
expected_includes = {
|
|
65
|
+
author: {
|
|
66
|
+
books: {}
|
|
67
|
+
},
|
|
68
|
+
category: {
|
|
69
|
+
books: {}
|
|
70
|
+
},
|
|
71
|
+
tags: {}
|
|
72
|
+
}
|
|
73
|
+
#expect(query).to receive(:includes).with(expected_includes).and_return(query)
|
|
74
|
+
expect(subject).to_not receive(:explain_query)
|
|
75
|
+
final_query = subject.generate
|
|
76
|
+
expect(final_query.select_values).to match_array(expected_select_from_to_query)
|
|
77
|
+
# Our query selector always uses a single hash tree from the top, not an array of things
|
|
78
|
+
includes_hash = final_query.includes_values.first
|
|
79
|
+
expect(includes_hash).to match(expected_includes)
|
|
80
|
+
# Also, make AR do the actual query to make sure everything is wired up correctly
|
|
81
|
+
result = final_query.to_a
|
|
82
|
+
expect(result.size).to be 2
|
|
83
|
+
book1 = result[0]
|
|
84
|
+
book2 = result[1]
|
|
85
|
+
expect(book1.author.id).to eq 11
|
|
86
|
+
expect(book1.author.books.size).to eq 1
|
|
87
|
+
expect(book1.author.books.map(&:simple_name)).to eq(['Book1'])
|
|
88
|
+
expect(book1.category.name).to eq 'cat1'
|
|
89
|
+
expect(book1.tags.map(&:name)).to match_array(['blue','red'])
|
|
90
|
+
|
|
91
|
+
expect(book2.author.id).to eq 22
|
|
92
|
+
expect(book2.author.books.size).to eq 1
|
|
93
|
+
expect(book2.author.books.map(&:simple_name)).to eq(['Book2'])
|
|
94
|
+
expect(book2.category.name).to eq 'cat2'
|
|
95
|
+
expect(book2.tags.map(&:name)).to match_array(['red'])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'calls the explain debug method if enabled' do
|
|
99
|
+
suppress_output do
|
|
100
|
+
# Actually make it run all the way...but suppressing the output
|
|
101
|
+
subject.generate(debug: true)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'sequel'
|
|
3
|
+
|
|
4
|
+
require 'praxis/extensions/field_selection/sequel_query_selector'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe Praxis::Extensions::FieldSelection::SequelQuerySelector do
|
|
8
|
+
class Q
|
|
9
|
+
attr_reader :object, :cols
|
|
10
|
+
def initialize
|
|
11
|
+
@object = {}
|
|
12
|
+
@cols = []
|
|
13
|
+
end
|
|
14
|
+
def eager(hash)
|
|
15
|
+
raise "we are only calling eager one at a time!" if hash.keys.size > 1
|
|
16
|
+
name = hash.keys.first
|
|
17
|
+
# Actually call the incoming proc with an instance of Q, to collect the further select/eager calls
|
|
18
|
+
@object[name] = hash[name].call(Q.new)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
def select(*names)
|
|
22
|
+
@cols += names.map(&:column)
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
def dump
|
|
26
|
+
eagers = @object.each_with_object({}) do |(name, val), hash|
|
|
27
|
+
hash[name] = val.dump
|
|
28
|
+
end
|
|
29
|
+
{
|
|
30
|
+
columns: @cols,
|
|
31
|
+
eagers: eagers
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Pay the price for creating and connecting only in this spec instead in spec helper
|
|
38
|
+
# this way all other specs do not need to be slower and it's a better TDD experience
|
|
39
|
+
|
|
40
|
+
require_relative 'support/spec_resources_sequel.rb'
|
|
41
|
+
|
|
42
|
+
let(:selector_fields) do
|
|
43
|
+
{
|
|
44
|
+
name: true,
|
|
45
|
+
other_model: {
|
|
46
|
+
id: true
|
|
47
|
+
},
|
|
48
|
+
parent: {
|
|
49
|
+
children: true
|
|
50
|
+
},
|
|
51
|
+
tags: {
|
|
52
|
+
tag_name: true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
let(:expected_select_from_to_query) do
|
|
57
|
+
# The columns to select from the top Simple model
|
|
58
|
+
[
|
|
59
|
+
:simple_name, # from the :name alias
|
|
60
|
+
:added_column, # from the extra column defined in the parent property
|
|
61
|
+
:id, # We always load the primary keys
|
|
62
|
+
:other_model_id, # the FK needed for the other_model association
|
|
63
|
+
:parent_id # the FK needed for the parent association
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields) }
|
|
68
|
+
subject {described_class.new(query: query, selectors: selector_node) }
|
|
69
|
+
|
|
70
|
+
context 'generate' do
|
|
71
|
+
context 'using the real models and DB' do
|
|
72
|
+
let(:query) { SequelSimpleModel }
|
|
73
|
+
|
|
74
|
+
it 'calls the select columns for the top level, and includes the right association hashes' do
|
|
75
|
+
ds = subject.generate
|
|
76
|
+
opts = ds.opts
|
|
77
|
+
# Top model is our simplemodel
|
|
78
|
+
expect(opts[:model]).to be(SequelSimpleModel)
|
|
79
|
+
selected_column_names = opts[:select].map(&:column)
|
|
80
|
+
expect(selected_column_names).to match_array(expected_select_from_to_query)
|
|
81
|
+
# 2 Eager loaded associations as well
|
|
82
|
+
expect(opts[:eager].keys).to match_array([:other_model, :parent, :tags])
|
|
83
|
+
# We can not introspect those eagers, as they are procs...but at least validate they are
|
|
84
|
+
expect(opts[:eager][:other_model]).to be_a Proc
|
|
85
|
+
expect(opts[:eager][:parent]).to be_a Proc
|
|
86
|
+
|
|
87
|
+
# Also, let's make sure the query actually works by making Sequel attempt to retrieve it and finding the right things.
|
|
88
|
+
result = ds.all
|
|
89
|
+
# 2 simple models
|
|
90
|
+
expect(result.size).to be 2
|
|
91
|
+
# First simple model points to other_model 11 and parent 1
|
|
92
|
+
simple_one = result.find{|i| i.id == 1}
|
|
93
|
+
expect(simple_one.other_model.id).to be 11
|
|
94
|
+
expect(simple_one.parent.id).to be 1
|
|
95
|
+
# also, its' parent in turn has 2 children (1 and 2) linked by its parent_uuid
|
|
96
|
+
expect(simple_one.parent.children.map(&:id)).to match_array([1,2])
|
|
97
|
+
# Has the blue and red tags
|
|
98
|
+
expect(simple_one.tags.map(&:tag_name)).to match_array(['blue','red'])
|
|
99
|
+
|
|
100
|
+
# second simple model points to other_model 22 and parent 2
|
|
101
|
+
simple_two = result.find{|i| i.id == 2}
|
|
102
|
+
expect(simple_two.other_model.id).to be 22
|
|
103
|
+
expect(simple_two.parent.id).to be 2
|
|
104
|
+
# also, its' parent in turn has no children (as no simple models point to it by uuid)
|
|
105
|
+
expect(simple_two.parent.children.map(&:id)).to be_empty
|
|
106
|
+
# Also has the red tag
|
|
107
|
+
expect(simple_two.tags.map(&:tag_name)).to match_array(['red'])
|
|
108
|
+
end
|
|
109
|
+
it 'calls the explain debug method if enabled' do
|
|
110
|
+
suppress_output do
|
|
111
|
+
# Actually make it run all the way...but suppressing the output
|
|
112
|
+
subject.generate(debug: true)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
context 'just mocking the query' do
|
|
117
|
+
let(:query) { Q.new }
|
|
118
|
+
|
|
119
|
+
it 'creates the right recursive lambdas for the eager loading' do
|
|
120
|
+
|
|
121
|
+
ds = subject.generate
|
|
122
|
+
result = ds.dump
|
|
123
|
+
expect(result[:columns]).to match_array(expected_select_from_to_query)
|
|
124
|
+
# 2 eager loads
|
|
125
|
+
expect(result[:eagers].keys).to match_array([:other_model, :parent, :tags])
|
|
126
|
+
# 1 - other model
|
|
127
|
+
other_model_eagers = result[:eagers][:other_model]
|
|
128
|
+
expect(other_model_eagers[:columns]).to match_array([:id])
|
|
129
|
+
|
|
130
|
+
# 2 - parent association
|
|
131
|
+
parent_eagers = result[:eagers][:parent]
|
|
132
|
+
expect(parent_eagers[:columns]).to match_array([:id,:uuid]) # uuid is necessary for the "children" assoc
|
|
133
|
+
expect(parent_eagers[:eagers].keys).to match_array([:children])
|
|
134
|
+
# 2.1 - children association off of the parent
|
|
135
|
+
parent_children_eagers = parent_eagers[:eagers][:children]
|
|
136
|
+
expect(parent_children_eagers[:columns]).to match_array([:id,:parent_uuid]) # parent_uuid is required for the assoc
|
|
137
|
+
expect(parent_children_eagers[:eagers]).to be_empty
|
|
138
|
+
|
|
139
|
+
# 3 - tags association
|
|
140
|
+
tags_eagers = result[:eagers][:tags]
|
|
141
|
+
expect(tags_eagers[:columns]).to match_array([:id, :tag_name]) # uuid is necessary for the "children" assoc
|
|
142
|
+
expect(tags_eagers[:eagers].keys).to be_empty
|
|
143
|
+
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|