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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -15
  3. data/CHANGELOG.md +328 -299
  4. data/CONTRIBUTING.md +4 -4
  5. data/README.md +11 -9
  6. data/lib/api_browser/app/js/directives/attribute_table.js +2 -1
  7. data/lib/api_browser/app/js/directives/conditional_requirements.js +13 -0
  8. data/lib/api_browser/app/js/directives/type_placeholder.js +10 -1
  9. data/lib/api_browser/app/js/factories/normalize_attributes.js +4 -2
  10. data/lib/api_browser/app/js/factories/template_for.js +5 -2
  11. data/lib/api_browser/app/js/filters/has_requirement.js +14 -0
  12. data/lib/api_browser/app/js/filters/tag_requirement.js +13 -0
  13. data/lib/api_browser/app/sass/praxis.scss +11 -0
  14. data/lib/api_browser/app/views/action.html +2 -2
  15. data/lib/api_browser/app/views/directives/attribute_description/member_options.html +2 -2
  16. data/lib/api_browser/app/views/directives/attribute_table.html +1 -1
  17. data/lib/api_browser/app/views/type.html +1 -1
  18. data/lib/api_browser/app/views/type/details.html +2 -2
  19. data/lib/api_browser/app/views/types/embedded/array.html +2 -0
  20. data/lib/api_browser/app/views/types/embedded/default.html +3 -1
  21. data/lib/api_browser/app/views/types/embedded/requirements.html +6 -0
  22. data/lib/api_browser/app/views/types/embedded/single_req.html +9 -0
  23. data/lib/api_browser/app/views/types/embedded/struct.html +14 -2
  24. data/lib/api_browser/app/views/types/standalone/array.html +1 -1
  25. data/lib/api_browser/app/views/types/standalone/struct.html +2 -1
  26. data/lib/api_browser/package.json +1 -1
  27. data/lib/praxis.rb +9 -3
  28. data/lib/praxis/action_definition.rb +1 -1
  29. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  30. data/lib/praxis/application.rb +1 -9
  31. data/lib/praxis/bootloader.rb +1 -4
  32. data/lib/praxis/config.rb +1 -1
  33. data/lib/praxis/dispatcher.rb +10 -6
  34. data/lib/praxis/docs/generator.rb +2 -1
  35. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
  36. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
  37. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  38. data/lib/praxis/extensions/field_selection.rb +1 -9
  39. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +51 -0
  40. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +61 -0
  41. data/lib/praxis/extensions/rails_compat.rb +2 -0
  42. data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
  43. data/lib/praxis/handlers/xml.rb +1 -1
  44. data/lib/praxis/mapper/active_model_compat.rb +98 -0
  45. data/lib/praxis/mapper/resource.rb +242 -0
  46. data/lib/praxis/mapper/selector_generator.rb +149 -0
  47. data/lib/praxis/mapper/sequel_compat.rb +76 -0
  48. data/lib/praxis/media_type_identifier.rb +2 -1
  49. data/lib/praxis/middleware_app.rb +20 -2
  50. data/lib/praxis/multipart/parser.rb +14 -2
  51. data/lib/praxis/notifications.rb +1 -1
  52. data/lib/praxis/plugins/mapper_plugin.rb +64 -0
  53. data/lib/praxis/plugins/rails_plugin.rb +104 -0
  54. data/lib/praxis/request.rb +7 -1
  55. data/lib/praxis/request_superclassing.rb +11 -0
  56. data/lib/praxis/resource_definition.rb +5 -5
  57. data/lib/praxis/response.rb +1 -1
  58. data/lib/praxis/route.rb +1 -1
  59. data/lib/praxis/routing_config.rb +1 -1
  60. data/lib/praxis/trait.rb +1 -1
  61. data/lib/praxis/types/media_type_common.rb +2 -2
  62. data/lib/praxis/types/multipart.rb +1 -1
  63. data/lib/praxis/types/multipart_array.rb +2 -2
  64. data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
  65. data/lib/praxis/version.rb +1 -1
  66. data/praxis.gemspec +14 -13
  67. data/spec/functional_spec.rb +4 -7
  68. data/spec/praxis/action_definition_spec.rb +1 -1
  69. data/spec/praxis/application_spec.rb +1 -1
  70. data/spec/praxis/collection_spec.rb +3 -2
  71. data/spec/praxis/config_spec.rb +2 -2
  72. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
  73. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
  74. data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
  75. data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
  76. data/spec/praxis/handlers/xml_spec.rb +2 -2
  77. data/spec/praxis/mapper/resource_spec.rb +169 -0
  78. data/spec/praxis/mapper/selector_generator_spec.rb +293 -0
  79. data/spec/praxis/media_type_spec.rb +0 -10
  80. data/spec/praxis/middleware_app_spec.rb +29 -9
  81. data/spec/praxis/request_stages/action_spec.rb +8 -1
  82. data/spec/praxis/response_definition_spec.rb +7 -4
  83. data/spec/praxis/response_spec.rb +1 -1
  84. data/spec/praxis/responses/internal_server_error_spec.rb +2 -2
  85. data/spec/praxis/responses/validation_error_spec.rb +2 -2
  86. data/spec/praxis/router_spec.rb +1 -1
  87. data/spec/spec_app/app/controllers/instances.rb +1 -1
  88. data/spec/spec_app/config/environment.rb +3 -21
  89. data/spec/spec_helper.rb +11 -15
  90. data/spec/support/be_deep_equal_matcher.rb +39 -0
  91. data/spec/support/spec_resources.rb +124 -0
  92. data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
  93. metadata +102 -77
  94. data/.ruby-version +0 -1
  95. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  96. data/lib/praxis/media_type_collection.rb +0 -127
  97. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  98. data/lib/praxis/stats.rb +0 -113
  99. data/spec/praxis/media_type_collection_spec.rb +0 -157
  100. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
  101. data/spec/praxis/stats_spec.rb +0 -9
  102. data/spec/spec_app/app/models/person.rb +0 -3
@@ -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
@@ -0,0 +1,11 @@
1
+ module Praxis
2
+ class << self
3
+
4
+ attr_writer :request_superclass
5
+
6
+ def request_superclass
7
+ @request_superclass || Object
8
+ end
9
+
10
+ end
11
+ 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 << Proc.new
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)
@@ -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:'', location: nil)
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
@@ -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
@@ -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.to_s unless identifier
36
- (@identifier = MediaTypeIdentifier.load(identifier)).to_s
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, context: context, from: value.class, to: self.name, value: value
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)
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.21'
2
+ VERSION = '2.0.pre.3'
3
3
  end
@@ -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/rightscale/praxis"
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', '~> 1'
24
- spec.add_dependency 'mustermann', '~> 0'
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-mapper', '~> 4.3'
28
- spec.add_dependency 'praxis-blueprints', '~> 3.3'
29
- spec.add_dependency 'attributor', '~> 5.1'
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', '~> 1.6'
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', '~> 0'
39
- spec.add_development_dependency 'pry-byebug', '~> 1'
40
- spec.add_development_dependency 'pry-stack_explorer', '~> 0'
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', '~> 0'
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
@@ -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', nil, 'global_session' => session
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', nil, 'global_session' => session
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', nil, 'global_session' => session
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', nil, 'global_session' => session
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
- its(:identifier) { should eq 'application/an-awesome-type; type=collection' }
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
- its(:identifier) { should eq 'application/vnd.acme.volumes' }
38
+ it { expect(identifier_string).to eq('application/vnd.acme.volumes') }
38
39
  end
39
40
 
40
41
  context '.member_type' do
@@ -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