praxis 2.0.pre.31 → 2.0.pre.33

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -1
  4. data/Appraisals +11 -0
  5. data/CHANGELOG.md +144 -104
  6. data/Gemfile +6 -6
  7. data/bin/praxis +24 -1
  8. data/gemfiles/active_6.gemfile +16 -0
  9. data/gemfiles/active_6.gemfile.lock +199 -0
  10. data/gemfiles/active_7.gemfile +16 -0
  11. data/gemfiles/active_7.gemfile.lock +197 -0
  12. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  13. data/lib/praxis/application.rb +1 -1
  14. data/lib/praxis/blueprint.rb +25 -18
  15. data/lib/praxis/blueprint_attribute_group.rb +0 -2
  16. data/lib/praxis/controller.rb +4 -0
  17. data/lib/praxis/docs/open_api/operation_object.rb +9 -0
  18. data/lib/praxis/docs/open_api/paths_object.rb +2 -2
  19. data/lib/praxis/docs/open_api_generator.rb +51 -21
  20. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +4 -4
  21. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +6 -12
  22. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +2 -0
  23. data/lib/praxis/extensions/pagination/pagination_params.rb +2 -2
  24. data/lib/praxis/field_expander.rb +1 -1
  25. data/lib/praxis/mapper/active_model_compat.rb +4 -6
  26. data/lib/praxis/mapper/selector_generator.rb +1 -1
  27. data/lib/praxis/media_type_identifier.rb +4 -4
  28. data/lib/praxis/request.rb +1 -1
  29. data/lib/praxis/tasks/console.rb +3 -0
  30. data/lib/praxis/types/multipart_array.rb +3 -3
  31. data/lib/praxis/version.rb +1 -1
  32. data/praxis.gemspec +2 -4
  33. data/spec/praxis/application_spec.rb +11 -0
  34. data/spec/praxis/blueprint_spec.rb +307 -17
  35. data/spec/praxis/controller_spec.rb +9 -0
  36. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +28 -0
  37. data/spec/praxis/request_spec.rb +10 -0
  38. data/spec/support/spec_blueprints.rb +6 -4
  39. data/tasks/thor/model.rb +3 -1
  40. data/tasks/thor/scaffold.rb +35 -3
  41. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +1 -0
  42. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
  43. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +11 -14
  44. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +3 -7
  45. metadata +23 -38
@@ -34,7 +34,7 @@ module Praxis
34
34
  @model = model
35
35
  @filters_map = filters_map
36
36
  @logger = debug ? Logger.new($stdout) : nil
37
- @active_record_version_maj = ActiveRecord.gem_version.segments[0]
37
+ @active_record_version = ActiveRecord.gem_version
38
38
  end
39
39
 
40
40
  def debug_query(msg, query)
@@ -86,7 +86,8 @@ module Praxis
86
86
  )
87
87
  end
88
88
 
89
- if @active_record_version_maj < 6
89
+
90
+ if @active_record_version < Gem::Version.new('6')
90
91
  # ActiveRecord < 6 does not support '.and' so no nested things can be done
91
92
  # But we can still support the case of 1+ flat conditions of the same AND/OR type
92
93
  if root_parent_group.is_a?(FilteringParams::Condition)
@@ -347,8 +348,7 @@ module Praxis
347
348
  end
348
349
 
349
350
  # The value that we need to stick in the references method is different in the latest Rails
350
- maj, min, = ActiveRecord.gem_version.segments
351
- if maj == 5 || (maj == 6 && min.zero?)
351
+ if ActiveRecord.gem_version < Gem::Version.new('6')
352
352
  # In AR 6 (and 6.0) the references are simple strings
353
353
  def self.build_reference_value(column_prefix, **_args)
354
354
  column_prefix
@@ -2,17 +2,11 @@
2
2
 
3
3
  require 'active_record'
4
4
 
5
- maj, min, = ActiveRecord.gem_version.segments
6
-
7
- case maj
8
- when 5
5
+ if ActiveRecord.gem_version < Gem::Version.new('6')
9
6
  require_relative 'active_record_patches/5x'
10
- when 6
11
- if min.zero?
12
- require_relative 'active_record_patches/6_0'
13
- else
14
- require_relative 'active_record_patches/6_1_plus'
15
- end
7
+ elsif ActiveRecord.gem_version < Gem::Version.new('6.1')
8
+ require_relative 'active_record_patches/6_0'
16
9
  else
17
- raise 'Filtering only supported for ActiveRecord >= 5 && <= 6'
18
- end
10
+ # As of 7.0.4 our 6.1-plus patches still work
11
+ require_relative 'active_record_patches/6_1_plus'
12
+ end
@@ -45,6 +45,8 @@ module Praxis
45
45
  quoted_prefix = AttributeFiltering::ActiveRecordFilterQueryBuilder.quote_column_path(query: query, prefix: column_prefix, column_name: info[:attribute])
46
46
  order_clause = Arel.sql(ActiveRecord::Base.sanitize_sql_array("#{quoted_prefix} #{direction}"))
47
47
  query = query.order(order_clause)
48
+ # Add a select for any order clause (unless we're already selecting *), as latest MySQL versions require it for DISTINCT queries
49
+ query = query.select(quoted_prefix) unless query.select_values.empty?
48
50
  end
49
51
  query
50
52
  end
@@ -89,7 +89,7 @@ module Praxis
89
89
  end
90
90
 
91
91
  def default(spec)
92
- unless spec.is_a?(Hash) && spec.keys.size == 1 && %i[by page].include?(spec.keys.first)
92
+ unless spec.is_a?(::Hash) && spec.keys.size == 1 && %i[by page].include?(spec.keys.first)
93
93
  raise "'default' syntax for pagination takes exactly one key specification. Either by: <:fieldname> or page: <num>" \
94
94
  "#{spec} is invalid"
95
95
  end
@@ -101,7 +101,7 @@ module Praxis
101
101
 
102
102
  { by: value }
103
103
  when :page
104
- raise "Error setting default pagination. Initial page should be a integer (but got #{value})" unless value.is_a?(Integer)
104
+ raise "Error setting default pagination. Initial page should be a integer (but got #{value})" unless value.is_a?(::Integer)
105
105
  raise 'Cannot define a default pagination that is page-based, if page-based pagination is disallowed.' if target.defaults[:disallow_paging]
106
106
 
107
107
  { page: value }
@@ -68,7 +68,7 @@ module Praxis
68
68
  end
69
69
 
70
70
  # just include the full thing if it has no attributes
71
- return true if object.attributes.empty?
71
+ return fields if object.attributes.empty?
72
72
 
73
73
  # True, expands to the default fieldset for blueprints
74
74
  fields = object.default_fieldset if object < Praxis::Blueprint && fields == true
@@ -67,19 +67,17 @@ module Praxis
67
67
  end
68
68
 
69
69
  def _join_foreign_key_for(assoc_reflection)
70
- maj, min, = ActiveRecord.gem_version.segments
71
- if maj >= 6 && min >= 1
70
+ if ActiveRecord.gem_version >= Gem::Version.new('6.1')
72
71
  assoc_reflection.join_foreign_key.to_sym
73
- else
72
+ else # below 6.1
74
73
  assoc_reflection.join_keys.foreign_key.to_sym
75
74
  end
76
75
  end
77
76
 
78
77
  def _join_primary_key_for(assoc_reflection)
79
- maj, min, = ActiveRecord.gem_version.segments
80
- if maj >= 6 && min >= 1
78
+ if ActiveRecord.gem_version >= Gem::Version.new('6.1')
81
79
  assoc_reflection.join_primary_key.to_sym
82
- else
80
+ else # below 6.1
83
81
  assoc_reflection.join_keys.key.to_sym
84
82
  end
85
83
  end
@@ -321,7 +321,7 @@ module Praxis
321
321
  end
322
322
 
323
323
  def inspect
324
- "<#{self.class}# @root=#{@root.inspect}>"
324
+ "#<#{self.class} @resource=#{@resource.name.inspect} @select=#{@select.inspect} @select_star=#{@select_star.inspect} @tracking.keys=#{@tracks.keys} (recursion omitted)>"
325
325
  end
326
326
  end
327
327
 
@@ -45,7 +45,7 @@ module Praxis
45
45
  # @see Attributor::Model#load
46
46
  def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, recurse: false, **options)
47
47
  case value
48
- when String
48
+ when ::String
49
49
  return nil if value.blank?
50
50
 
51
51
  base, *parameters = value.split(PARAMETER_SEPARATOR)
@@ -66,7 +66,7 @@ module Praxis
66
66
  else
67
67
  obj.type = 'application'
68
68
  obj.subtype = base.split(WORD_SEPARATOR, 2).first
69
- obj.suffix = String.new
69
+ obj.suffix = ::String.new
70
70
  obj.parameters = {}
71
71
  end
72
72
  obj
@@ -126,7 +126,7 @@ module Praxis
126
126
  # @return [String] canonicalized representation of the media type including all suffixes and parameters
127
127
  def to_s
128
128
  # Our handcrafted media types consist of a rich chocolatey center
129
- s = String.new("#{type}/#{subtype}")
129
+ s = ::String.new("#{type}/#{subtype}")
130
130
 
131
131
  # coated in a hard candy shell
132
132
  s << '+' << suffix unless suffix.empty?
@@ -204,7 +204,7 @@ module Praxis
204
204
  obj.type = type
205
205
  obj.subtype = subtype
206
206
  target_suffix = suffix || self.suffix
207
- obj.suffix = redundant_suffix(target_suffix) ? String.new : target_suffix
207
+ obj.suffix = redundant_suffix(target_suffix) ? ::String.new : target_suffix
208
208
  obj.parameters = self.parameters.merge(parameters)
209
209
 
210
210
  obj
@@ -184,7 +184,7 @@ module Praxis
184
184
  # Override the inspect instance method of a request, as, by default, the kernel inspect will go nuts
185
185
  # traversing the action and app_instance and therefore all associated instance variables reachable through that
186
186
  def inspect
187
- "'@env' => #{@env.inspect},\n'@headers' => #{@headers.inspect},\n'@params' => #{@params.inspect},\n'@query' => #{@query.inspect}"
187
+ "#<#{self.class}##{object_id} @action=#{@action.inspect} @params=#{@params.inspect}>"
188
188
  end
189
189
  end
190
190
  end
@@ -27,6 +27,9 @@ namespace :praxis do
27
27
  PROMPT_C: "%N(#{nickname}):%03n:%i* "
28
28
  }
29
29
 
30
+ # Disable inefficient, distracting autocomplete
31
+ IRB.conf[:USE_AUTOCOMPLETE] = false
32
+
30
33
  # Set the IRB main object.
31
34
  IRB.irb(nil, Praxis::Application.instance)
32
35
  end
@@ -219,13 +219,13 @@ module Praxis
219
219
  hash[:part_name] = { type: name_type.describe(true) }
220
220
 
221
221
  unless shallow
222
- hash[:attributes] = {} if attributes.keys.any? { |name| name.is_a? String }
222
+ hash[:attributes] = {} if attributes.keys.any? { |name| name.is_a? ::String }
223
223
  hash[:pattern_attributes] = {} if attributes.keys.any? { |name| name.is_a? Regexp }
224
224
 
225
225
  if hash.key?(:attributes) || hash.key?(:pattern_attributes)
226
226
  describe_attributes(shallow, example: example).each do |name, sub_hash|
227
227
  case name
228
- when String
228
+ when ::String
229
229
  hash[:attributes][name] = sub_hash
230
230
  when Regexp
231
231
  hash[:pattern_attributes][name.source] = sub_hash
@@ -305,7 +305,7 @@ module Praxis
305
305
  return self << part
306
306
  elsif self.class.options[:case_insensitive_load]
307
307
  name = self.class.attributes.keys.find do |k|
308
- k.is_a?(String) && key.downcase == k.downcase
308
+ k.is_a?(::String) && key.downcase == k.downcase
309
309
  end
310
310
  if name
311
311
  part.name = name
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
- VERSION = '2.0.pre.31'
4
+ VERSION = '2.0.pre.33'
5
5
  end
data/praxis.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.executables << 'praxis'
24
24
 
25
25
  spec.add_dependency 'activesupport', '>= 3'
26
- spec.add_dependency 'attributor', '>= 6.5'
26
+ spec.add_dependency 'attributor', '>= 7.0'
27
27
  spec.add_dependency 'mime', '~> 0'
28
28
  spec.add_dependency 'mustermann', '>=1.1'
29
29
  spec.add_dependency 'rack', '>= 1'
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_development_dependency 'bundler'
34
34
  spec.add_development_dependency 'rake', '>= 12.3.3'
35
+ spec.add_development_dependency "appraisal"
35
36
 
36
37
  if RUBY_PLATFORM !~ /java/
37
38
  spec.add_development_dependency 'pry'
@@ -50,7 +51,4 @@ Gem::Specification.new do |spec|
50
51
  spec.add_development_dependency 'rspec', '~> 3'
51
52
  spec.add_development_dependency 'rspec-collection_matchers', '~> 1'
52
53
  spec.add_development_dependency 'rspec-its', '~> 1'
53
- # Just for the query selector extensions etc...
54
- spec.add_development_dependency 'activerecord', '> 4', '< 7'
55
- spec.add_development_dependency 'sequel', '~> 5'
56
54
  end
@@ -97,6 +97,17 @@ describe Praxis::Application do
97
97
  end
98
98
  end
99
99
 
100
+ describe '#inspect' do
101
+ let(:klass) { Class.new(Praxis::Application) }
102
+ subject { klass.instance }
103
+
104
+ it 'includes name, object ID and root' do
105
+ SomeApplication = klass # de-anonymize class name
106
+ klass.instance.instance_variable_set(:@root, '/tmp')
107
+ expect(subject.inspect).to match(%r{#<SomeApplication#[0-9]+ @root=/tmp>})
108
+ end
109
+ end
110
+
100
111
  describe '#setup' do
101
112
  subject { Class.new(Praxis::Application).instance }
102
113
 
@@ -7,6 +7,280 @@ describe Praxis::Blueprint do
7
7
 
8
8
  its(:family) { should eq('hash') }
9
9
 
10
+ # This context might seem a duplication of tests that should be covered by the underlying attributor gem
11
+ # but while it is in structure, it is different because we're doing it with Blueprints (not Attributor Models)
12
+ # to make sure our Blueprints are behaving as expected.
13
+ context 'type resolution, option inheritance for attributes with and without references' do
14
+ # Overall strategy
15
+ # 1) When no type is specified:
16
+ # 1.1) if it is a leaf (no block)
17
+ # 1.1.1) with an reference with an attr with the same name
18
+ # - type copied from reference
19
+ # - reference options are inherited as well (and can be overridden by local attribute ones)
20
+ # 1.1.2) without a ref (or the ref does not have same attribute name)
21
+ # - Fail. Cannot determine type
22
+ # 1.2) if it has a block
23
+ # 1.2.1) with an reference with an attr with the same name
24
+ # - We assume you're re/defining a new Struct (or Struct[]), and we will incorporate the reference type
25
+ # for the matching name in case you are indeed redefining a subset of the attributes, so you can enjoy inheritance
26
+ # 1.2.2) without a ref (or the ref does not have same attribute name)
27
+ # - defaulted to Struct (if you meant Collection.of(Struct) things would fail later somehow)
28
+ # - options are NOT inherited at all (This is something we should ponder more about)
29
+ # 2) When type is specified:
30
+ # 2.1) if it is a leaf (no block)
31
+ # - ignore ref if there is one (with or without matching attribute name).
32
+ # - simply use provided type, and provided options (no inheritance)
33
+ # 2.2) if it has a block
34
+ # - Same as above: use type and options provided, ignore ref if there is one (with or without matching attribute name).
35
+
36
+ let(:mytype) do
37
+ Praxis::Blueprint.finalize!
38
+ Class.new(Praxis::Blueprint, &myblock).tap{|c| c._finalize!}
39
+ end
40
+ context 'with no explicit type specified' do
41
+ context 'without a block (if it is a leaf)' do
42
+ context 'that has a reference with an attribute with the same name' do
43
+ let(:myblock) {
44
+ Proc.new do
45
+ attributes reference: PersonBlueprint do
46
+ attribute :age, required: true, min: 42
47
+ end
48
+ end
49
+ }
50
+ it 'uses type from reference' do
51
+ expect(mytype.attributes).to have_key(:age)
52
+ expect(mytype.attributes[:age].type).to eq(PersonBlueprint.attributes[:age].type)
53
+ end
54
+ it 'copies over reference options and allows the attribute to override/add some' do
55
+ merged_options = PersonBlueprint.attributes[:age].options.merge(required: true, min: 42)
56
+ expect(mytype.attributes[:age].options).to include(merged_options)
57
+ end
58
+ end
59
+ context 'with a reference, but that does not have a matching attribute name' do
60
+ let(:myblock) {
61
+ Proc.new do
62
+ attributes reference: AddressBlueprint do
63
+ attribute :age
64
+ end
65
+ end
66
+ }
67
+ it 'fails resolving' do
68
+ expect{mytype.attributes}.to raise_error(/Type for attribute with name: age could not be determined./)
69
+ end
70
+ end
71
+ context 'without a reference' do
72
+ let(:myblock) {
73
+ Proc.new do
74
+ attributes do
75
+ attribute :age
76
+ end
77
+ end
78
+ }
79
+ it 'fails resolving' do
80
+ expect{mytype.attributes}.to raise_error(/Type for attribute with name: age could not be determined./)
81
+ end
82
+ end
83
+ end
84
+ context 'with block (if it is NOT a leaf)' do
85
+ context 'that has a reference with an attribute with the same name' do
86
+ context 'which is not a collection type' do
87
+ let(:myblock) {
88
+ Proc.new do
89
+ attributes reference: PersonBlueprint do
90
+ attribute :age , description: 'I am fully redefining' do
91
+ attribute :foobar, Integer, min: 42
92
+ end
93
+ end
94
+ end
95
+ }
96
+ it 'picks Struct, and makes sure to pass the reference of the attribute along' do
97
+ expect(mytype.attributes).to have_key(:age)
98
+ age_attribute = mytype.attributes[:age]
99
+ # Resolves to Struct
100
+ expect(age_attribute.type).to be < Attributor::Struct
101
+ # does NOT brings any ref options (except the right reference)
102
+ expect(age_attribute.options).to include(description: 'I am fully redefining')
103
+ # Yes, there is no way we can ever use an Integer when we're defining a Struct...but if the parent was a Struct, we would
104
+ expect(age_attribute.options).to include(reference: Attributor::Integer)
105
+ # And the nested attribute is correctly resolved as well, and ensures options are there
106
+ expect(age_attribute.type.attributes[:foobar].type).to eq(Attributor::Integer)
107
+ expect(age_attribute.type.attributes[:foobar].options).to eq(min: 42)
108
+ end
109
+ end
110
+ context 'which is a collection type' do
111
+ let(:myblock) {
112
+ Proc.new do
113
+ attributes reference: PersonBlueprint do
114
+ attribute :prior_addresses , description: 'I am fully redefining' do
115
+ attribute :street, required: true
116
+ attribute :new_attribute, String, default: 'foo'
117
+ end
118
+ end
119
+ end
120
+ }
121
+ it 'picks Struct, and makes sure to pass the reference of the attribute along' do
122
+ expect(mytype.attributes).to have_key(:prior_addresses)
123
+ prior_addresses_attribute = mytype.attributes[:prior_addresses]
124
+ # Resolves to Struct[]
125
+ expect(prior_addresses_attribute.type).to be < Attributor::Collection
126
+ expect(prior_addresses_attribute.type.member_type).to be < Attributor::Struct
127
+ # does NOT brings any ref options (except the right reference)
128
+ expect(prior_addresses_attribute.options).to include(description: 'I am fully redefining')
129
+ # Yes, there is no way we can ever use an Integer when we're defining a Struct...but if the parent was a Struct, we would
130
+ expect(prior_addresses_attribute.options).to include(reference: PersonBlueprint.attributes[:prior_addresses].type.member_type)
131
+ # And the nested attributes are correctly resolved as well, and ensures options are there
132
+ street_options_from_ref = PersonBlueprint.attributes[:prior_addresses].type.member_type.attributes[:street].options
133
+ expect(prior_addresses_attribute.type.member_type.attributes[:street].type).to eq(Attributor::String)
134
+ expect(prior_addresses_attribute.type.member_type.attributes[:street].options).to eq(street_options_from_ref.merge(required: true))
135
+
136
+ expect(prior_addresses_attribute.type.member_type.attributes[:new_attribute].type).to eq(Attributor::String)
137
+ expect(prior_addresses_attribute.type.member_type.attributes[:new_attribute].options).to eq(default: 'foo')
138
+ end
139
+ end
140
+ context 'in the unlikely case that the reference type has an anonymous Struct (or collection of)' do
141
+ let(:myblock) {
142
+ Proc.new do
143
+ attributes reference: PersonBlueprint do
144
+ attribute :funny_attribute, description: 'Funny business' do
145
+ attribute :foobar, Integer, min: 42
146
+ end
147
+ end
148
+ end
149
+ }
150
+ it 'correctly inherits it (same result as defaulting to Struct) and brings in the reference' do
151
+ expect(mytype.attributes).to have_key(:funny_attribute)
152
+ # Resolves to Struct, and brings (and merges) the ref options with the attribute's
153
+ expect(mytype.attributes[:funny_attribute].type).to be < Attributor::Struct
154
+ merged_options = {reference: PersonBlueprint.attributes[:funny_attribute].type}.merge(description: 'Funny business')
155
+ expect(mytype.attributes[:funny_attribute].options).to include(merged_options)
156
+ # And the nested attribute is correctly resolved as well, and ensures options are there
157
+ expect(mytype.attributes[:funny_attribute].type.attributes[:foobar].type).to eq(Attributor::Integer)
158
+ expect(mytype.attributes[:funny_attribute].type.attributes[:foobar].options).to eq(min: 42)
159
+ end
160
+ end
161
+ end
162
+ context 'with a reference, but that does not have a matching attribute name' do
163
+ let(:myblock) {
164
+ Proc.new do
165
+ attributes reference: AddressBlueprint do
166
+ attribute :age, description: 'I am redefining' do
167
+ attribute :foobar, Integer, min: 42
168
+ end
169
+ end
170
+ end
171
+ }
172
+ it 'correctly defaults to Struct uses only the local options (same exact as if it had no reference)' do
173
+ expect(mytype.attributes).to have_key(:age)
174
+ age_attribute = mytype.attributes[:age]
175
+ # Resolves to Struct
176
+ expect(age_attribute.type).to be < Attributor::Struct
177
+ # does NOT brings any ref options
178
+ expect(age_attribute.options).to eq(description: 'I am redefining')
179
+ # And the nested attribute is correctly resolved as well, and ensures options are there
180
+ expect(age_attribute.type.attributes[:foobar].type).to eq(Attributor::Integer)
181
+ expect(age_attribute.type.attributes[:foobar].options).to eq(min: 42)
182
+ end
183
+ end
184
+ context 'without a reference' do
185
+ let(:myblock) {
186
+ Proc.new do
187
+ attributes do
188
+ attribute :age, description: 'I am redefining' do
189
+ attribute :foobar, Integer, min: 42
190
+ end
191
+ end
192
+ end
193
+ }
194
+ it 'correctly defaults to Struct uses only the local options' do
195
+ expect(mytype.attributes).to have_key(:age)
196
+ age_attribute = mytype.attributes[:age]
197
+ # Resolves to Struct
198
+ expect(age_attribute.type).to be < Attributor::Struct
199
+ # does NOT brings any ref options
200
+ expect(age_attribute.options).to eq(description: 'I am redefining')
201
+ # And the nested attribute is correctly resolved as well, and ensures options are there
202
+ expect(age_attribute.type.attributes[:foobar].type).to eq(Attributor::Integer)
203
+ expect(age_attribute.type.attributes[:foobar].options).to eq(min: 42)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ context 'with an explicit type specified' do
209
+ context 'without a reference' do
210
+ let(:myblock) {
211
+ Proc.new do
212
+ attributes do
213
+ attribute :age, String, description: 'I am a String now'
214
+ end
215
+ end
216
+ }
217
+ it 'always uses the provided type and local options specified' do
218
+ expect(mytype.attributes).to have_key(:age)
219
+ age_attribute = mytype.attributes[:age]
220
+ # Resolves to String
221
+ expect(age_attribute.type).to eq(Attributor::String)
222
+ # copies local options
223
+ expect(age_attribute.options).to eq(description: 'I am a String now')
224
+ end
225
+ end
226
+ context 'with a reference' do
227
+ let(:myblock) {
228
+ Proc.new do
229
+ attributes reference: PersonBlueprint do
230
+ attribute :age, String, description: 'I am a String now'
231
+ end
232
+ end
233
+ }
234
+ it 'always uses the provided type and local options specified (same as if it had no reference)' do
235
+ expect(mytype.attributes).to have_key(:age)
236
+ age_attribute = mytype.attributes[:age]
237
+ # Resolves to String
238
+ expect(age_attribute.type).to eq(Attributor::String)
239
+ # copies local options
240
+ expect(age_attribute.options).to eq(description: 'I am a String now')
241
+ end
242
+ end
243
+
244
+ context 'with a reference, which can further percolate down' do
245
+ let(:myblock) {
246
+ Proc.new do
247
+ attributes reference: PersonBlueprint do
248
+ attribute :age, String, description: 'I am a String now'
249
+ attribute :address, Struct, description: 'Address subset' do
250
+ attribute :street, required: true
251
+ end
252
+ attribute :tags
253
+ end
254
+ end
255
+ }
256
+
257
+ it 'brings the child reference for address so we can redefine it' do
258
+ expect(mytype.attributes.keys).to eq([:age, :address, :tags])
259
+ age_attribute = mytype.attributes[:age]
260
+ expect(age_attribute.type).to eq(Attributor::String)
261
+ expect(age_attribute.options).to eq(description: 'I am a String now')
262
+
263
+ address_attribute = mytype.attributes[:address]
264
+ expect(address_attribute.type).to be < Attributor::Struct
265
+ # It brings in our local options AND percolates down the reference type for address
266
+ expect(address_attribute.options).to include(description: 'Address subset', reference: AddressBlueprint)
267
+
268
+ # Address fields are properly resolved to match the corresponding AddressBlueprint
269
+ expect(address_attribute.type.attributes.keys).to eq([:street])
270
+ street_attribute = address_attribute.type.attributes[:street]
271
+ expect(street_attribute.type).to eq(AddressBlueprint.attributes[:street].type)
272
+ # Makes sure our local options on the street are kept
273
+ expect(street_attribute.options).to include(required: true)
274
+ # And brings in other options from the inherited street attribute
275
+ expect(street_attribute.options).to include(description: 'The street')
276
+
277
+ # It also properly resolves the direct tags attribute from the reference, pointing to the same type
278
+ tags_attribute = mytype.attributes[:tags]
279
+ expect(tags_attribute.type).to eq PersonBlueprint.attributes[:tags].type
280
+ end
281
+ end
282
+ end
283
+ end
10
284
  context 'deterministic examples' do
11
285
  it 'works' do
12
286
  person1 = PersonBlueprint.example('person 1')
@@ -75,7 +349,7 @@ describe Praxis::Blueprint do
75
349
  end
76
350
 
77
351
  it 'has an inner Struct class for the attributes' do
78
- expect(blueprint_class.attribute.type).to be blueprint_class::Struct
352
+ expect(blueprint_class.attribute.type).to be blueprint_class::InnerStruct
79
353
  end
80
354
 
81
355
  context 'an instance' do
@@ -166,6 +440,12 @@ describe Praxis::Blueprint do
166
440
  it { should be_empty }
167
441
  end
168
442
 
443
+ context 'with a valid nested blueprint' do
444
+ let(:hash) { { name: 'bob', myself: { name: 'PseudoBob'}} }
445
+
446
+ it { should be_empty }
447
+ end
448
+
169
449
  context 'with invalid sub-attribute' do
170
450
  let(:hash) { { name: 'bob', address: { state: 'ME' } } }
171
451
 
@@ -173,6 +453,15 @@ describe Praxis::Blueprint do
173
453
  its(:first) { should =~ /Attribute \$.address.state/ }
174
454
  end
175
455
 
456
+ context 'with an invalid nested blueprint' do
457
+ let(:hash) { { name: 'bob', myself: { name: 'PseudoBob', address: { state: 'ME' }}} }
458
+
459
+ it { should have(1).item }
460
+ its(:first) { should =~ /Attribute \$.myself.address.state/ }
461
+
462
+ end
463
+
464
+
176
465
  context 'for objects of the wrong type' do
177
466
  it 'raises an error' do
178
467
  expect do
@@ -206,22 +495,23 @@ describe Praxis::Blueprint do
206
495
  end
207
496
  end
208
497
 
209
- context 'with a provided :reference option on attributes' do
210
- context 'that does not match the value set on the class' do
211
- subject(:mismatched_reference) do
212
- Class.new(Praxis::Blueprint) do
213
- self.reference = Class.new(Praxis::Blueprint)
214
- attributes(reference: Class.new(Praxis::Blueprint)) {}
215
- end
216
- end
217
-
218
- it 'should raise an error' do
219
- expect do
220
- mismatched_reference.attributes
221
- end.to raise_error(/Reference mismatch/)
222
- end
223
- end
224
- end
498
+ # TODO: Think about this 'feature' ...
499
+ # context 'with a provided :reference option on attributes' do
500
+ # context 'that does not match the value set on the class' do
501
+ # subject(:mismatched_reference) do
502
+ # Class.new(Praxis::Blueprint) do
503
+ # self.reference = Class.new(Praxis::Blueprint)
504
+ # attributes(reference: Class.new(Praxis::Blueprint)) {}
505
+ # end
506
+ # end
507
+
508
+ # it 'should raise an error' do
509
+ # expect do
510
+ # mismatched_reference.attributes
511
+ # end.to raise_error(/Reference mismatch/)
512
+ # end
513
+ # end
514
+ # end
225
515
 
226
516
  context '.example' do
227
517
  context 'with some attribute values provided' do
@@ -31,4 +31,13 @@ describe Praxis::Controller do
31
31
  expect(subject).to eq(PeopleResource.controller)
32
32
  end
33
33
  end
34
+
35
+ describe '#inspect' do
36
+ it 'includes name, object ID and request' do
37
+ SomeController = subject # de-anonymize class name
38
+ expect(subject.new('eioio').inspect).to match(
39
+ /#<SomeController#[0-9]+ @request="eioio">/
40
+ )
41
+ end
42
+ end
34
43
  end