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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +4 -1
- data/Appraisals +11 -0
- data/CHANGELOG.md +144 -104
- data/Gemfile +6 -6
- data/bin/praxis +24 -1
- data/gemfiles/active_6.gemfile +16 -0
- data/gemfiles/active_6.gemfile.lock +199 -0
- data/gemfiles/active_7.gemfile +16 -0
- data/gemfiles/active_7.gemfile.lock +197 -0
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
- data/lib/praxis/application.rb +1 -1
- data/lib/praxis/blueprint.rb +25 -18
- data/lib/praxis/blueprint_attribute_group.rb +0 -2
- data/lib/praxis/controller.rb +4 -0
- data/lib/praxis/docs/open_api/operation_object.rb +9 -0
- data/lib/praxis/docs/open_api/paths_object.rb +2 -2
- data/lib/praxis/docs/open_api_generator.rb +51 -21
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +4 -4
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +6 -12
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +2 -0
- data/lib/praxis/extensions/pagination/pagination_params.rb +2 -2
- data/lib/praxis/field_expander.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +4 -6
- data/lib/praxis/mapper/selector_generator.rb +1 -1
- data/lib/praxis/media_type_identifier.rb +4 -4
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/tasks/console.rb +3 -0
- data/lib/praxis/types/multipart_array.rb +3 -3
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +2 -4
- data/spec/praxis/application_spec.rb +11 -0
- data/spec/praxis/blueprint_spec.rb +307 -17
- data/spec/praxis/controller_spec.rb +9 -0
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +28 -0
- data/spec/praxis/request_spec.rb +10 -0
- data/spec/support/spec_blueprints.rb +6 -4
- data/tasks/thor/model.rb +3 -1
- data/tasks/thor/scaffold.rb +35 -3
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +1 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +11 -14
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +3 -7
- 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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
18
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
"
|
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
|
data/lib/praxis/request.rb
CHANGED
@@ -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
|
-
"
|
187
|
+
"#<#{self.class}##{object_id} @action=#{@action.inspect} @params=#{@params.inspect}>"
|
188
188
|
end
|
189
189
|
end
|
190
190
|
end
|
data/lib/praxis/tasks/console.rb
CHANGED
@@ -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
|
data/lib/praxis/version.rb
CHANGED
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', '>=
|
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::
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|