attributor 5.0.2 → 5.1.0
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/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe4a2dca77307723d8c18e4f37ded9cba8e54657
|
4
|
+
data.tar.gz: 175d9d21dd78aaba4e6c534559290cb79d3bf3a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89de22ecae27da14b59f57b2ea87d3552c6d1dbdd338e07934bc29031fd4e65b664f4faf04fb50f56b2e3687cde7580ba217562a413856fa8cdb403c9bc4a335
|
7
|
+
data.tar.gz: d8beb7bba6e226b84d1608d080800e65ce632d3b9af0daeb6fa7415f0477d307d63ac9267b4ffd659fe846b9f1b0b3012d4853d8a1bb6a3d4bad4ff037aaa10b
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.2
|
3
|
+
Style/Documentation:
|
4
|
+
Enabled: false
|
5
|
+
Metrics/MethodLength:
|
6
|
+
Enabled: false
|
7
|
+
Metrics/ClassLength:
|
8
|
+
Enabled: false
|
9
|
+
Metrics/LineLength:
|
10
|
+
Max: 200
|
11
|
+
Style/RedundantSelf:
|
12
|
+
Enabled: false
|
13
|
+
Style/ClassAndModuleChildren:
|
14
|
+
Enabled: false
|
15
|
+
Lint/Debugger:
|
16
|
+
Enabled: false
|
17
|
+
Metrics/AbcSize:
|
18
|
+
Enabled: false
|
19
|
+
Style/CaseEquality:
|
20
|
+
Enabled: false
|
21
|
+
Lint/UnusedMethodArgument:
|
22
|
+
AllowUnusedKeywordArguments: true
|
23
|
+
|
24
|
+
# Offense count: 13
|
25
|
+
Metrics/CyclomaticComplexity:
|
26
|
+
Max: 28
|
27
|
+
|
28
|
+
# Offense count: 11
|
29
|
+
Metrics/PerceivedComplexity:
|
30
|
+
Max: 23
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,10 +2,15 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 5.1
|
6
|
+
|
7
|
+
* Added `Polymorphic` type. See [polymorphics.rb](spec/support/polymorphics.rb) for example usage.
|
8
|
+
|
5
9
|
## 5.0.2
|
6
10
|
|
7
11
|
* Introduce the `Dumpable` (empty) module as an interface to indicate that instances of types that include it
|
8
|
-
will respond to the `.dump` method, as a way to convert their internal substructure to primitive Ruby objects.
|
12
|
+
will respond to the `.dump` method, as a way to convert their internal substructure to primitive Ruby objects.
|
13
|
+
* Currently the only two directly dumpable types are Collection and Hash (with the caveat that there are several others that derive from them..i.e., CSV, Model, etc...)
|
9
14
|
* The rest of types have `native_types` that are already Ruby primitive Objects.
|
10
15
|
* Fixed Hash and Model requirements to treat nil values as missing keys (to be compatible with the `required: true` option on an attribute).
|
11
16
|
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
# A sample Guardfile
|
2
2
|
# More info at https://github.com/guard/guard#readme
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
group :red_green_refactor, halt_on_fail: true do
|
5
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
6
|
+
watch(%r{^spec/.+_spec\.rb$})
|
7
|
+
watch(%r{^lib/attributor/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
8
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
9
|
+
watch('lib/attributor/base.rb') { 'spec' }
|
10
|
+
watch('spec/support/models.rb') { 'spec' }
|
11
|
+
watch('lib/attributor.rb') { 'spec' }
|
12
|
+
end
|
12
13
|
|
14
|
+
guard :rubocop, cli: '--auto-correct --display-cop-names' do
|
15
|
+
watch(/.+\.rb$/)
|
16
|
+
watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
|
17
|
+
end
|
18
|
+
end
|
data/Rakefile
CHANGED
@@ -7,14 +7,13 @@ require 'rspec/core/rake_task'
|
|
7
7
|
require 'bundler/gem_tasks'
|
8
8
|
require 'rake/notes/rake_task'
|
9
9
|
|
10
|
-
|
11
|
-
desc "Run RSpec code examples with simplecov"
|
10
|
+
desc 'Run RSpec code examples with simplecov'
|
12
11
|
RSpec::Core::RakeTask.new do |spec|
|
13
|
-
spec.rspec_opts = [
|
12
|
+
spec.rspec_opts = ['-c']
|
14
13
|
spec.pattern = FileList['spec/**/*_spec.rb']
|
15
14
|
end
|
16
15
|
|
17
|
-
desc
|
16
|
+
desc 'console'
|
18
17
|
task :console do
|
19
18
|
require 'bundler'
|
20
19
|
Bundler.require(:default, :development, :test)
|
@@ -22,7 +21,7 @@ task :console do
|
|
22
21
|
pry
|
23
22
|
end
|
24
23
|
|
25
|
-
task :
|
24
|
+
task default: :spec
|
26
25
|
|
27
26
|
require 'yard'
|
28
27
|
YARD::Rake::YardocTask.new
|
data/attributor.gemspec
CHANGED
@@ -5,38 +5,43 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
require 'attributor/version'
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
|
-
spec.name =
|
9
|
-
spec.version
|
10
|
-
spec.authors = [
|
11
|
-
spec.summary =
|
12
|
-
spec.email = [
|
8
|
+
spec.name = 'attributor'
|
9
|
+
spec.version = Attributor::VERSION
|
10
|
+
spec.authors = ['Josep M. Blanquer', 'Dane Jensen']
|
11
|
+
spec.summary = 'A powerful attribute and type management library for Ruby'
|
12
|
+
spec.email = ['blanquer@gmail.com', 'dane.jensen@gmail.com']
|
13
13
|
|
14
|
-
spec.homepage =
|
15
|
-
spec.license =
|
16
|
-
spec.required_ruby_version =
|
14
|
+
spec.homepage = 'https://github.com/rightscale/attributor'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.required_ruby_version = '>=2.1'
|
17
17
|
|
18
|
-
spec.require_paths = [
|
18
|
+
spec.require_paths = ['lib']
|
19
19
|
spec.files = `git ls-files -z`.split("\x0")
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
21
|
|
22
|
-
spec.add_runtime_dependency(
|
23
|
-
spec.add_runtime_dependency(
|
24
|
-
spec.add_runtime_dependency(
|
25
|
-
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency(
|
30
|
-
spec.add_development_dependency(
|
31
|
-
spec.add_development_dependency(
|
32
|
-
spec.add_development_dependency(
|
33
|
-
spec.add_development_dependency(
|
34
|
-
spec.add_development_dependency(
|
35
|
-
spec.add_development_dependency(
|
36
|
-
spec.add_development_dependency(
|
37
|
-
spec.add_development_dependency(
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
22
|
+
spec.add_runtime_dependency('hashie', ['~> 3'])
|
23
|
+
spec.add_runtime_dependency('randexp', ['~> 0'])
|
24
|
+
spec.add_runtime_dependency('activesupport', ['>= 3'])
|
25
|
+
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
27
|
+
spec.add_development_dependency 'rspec-its'
|
28
|
+
spec.add_development_dependency 'rspec-collection_matchers', '~> 1'
|
29
|
+
spec.add_development_dependency('yard', ['~> 0.8.7'])
|
30
|
+
spec.add_development_dependency('backports', ['~> 3'])
|
31
|
+
spec.add_development_dependency('yardstick', ['~> 0'])
|
32
|
+
spec.add_development_dependency('bundler', ['>= 0'])
|
33
|
+
spec.add_development_dependency('rake-notes', ['~> 0'])
|
34
|
+
spec.add_development_dependency('coveralls')
|
35
|
+
spec.add_development_dependency('guard', ['~> 2'])
|
36
|
+
spec.add_development_dependency('guard-rspec', ['~> 4'])
|
37
|
+
spec.add_development_dependency('pry', ['~> 0'])
|
38
|
+
if RUBY_PLATFORM !~ /java/
|
39
|
+
spec.add_development_dependency('pry-byebug', ['~> 1'])
|
40
|
+
spec.add_development_dependency('pry-stack_explorer', ['~> 0'])
|
41
|
+
end
|
42
|
+
spec.add_development_dependency 'fuubar'
|
43
|
+
spec.add_development_dependency 'rubocop'
|
44
|
+
spec.add_development_dependency 'guard-rubocop'
|
45
|
+
|
46
|
+
spec.add_development_dependency('parslet', ['>= 0'])
|
42
47
|
end
|
data/lib/attributor.rb
CHANGED
@@ -6,7 +6,6 @@ require 'hashie'
|
|
6
6
|
require 'digest/sha1'
|
7
7
|
|
8
8
|
module Attributor
|
9
|
-
|
10
9
|
require_relative 'attributor/dumpable'
|
11
10
|
|
12
11
|
require_relative 'attributor/exceptions'
|
@@ -20,45 +19,41 @@ module Attributor
|
|
20
19
|
|
21
20
|
require_relative 'attributor/extensions/randexp'
|
22
21
|
|
23
|
-
|
24
22
|
# hierarchical separator string for composing human readable attributes
|
25
23
|
SEPARATOR = '.'.freeze
|
26
24
|
DEFAULT_ROOT_CONTEXT = ['$'].freeze
|
27
25
|
|
28
26
|
# @param type [Class] The class of the type to resolve
|
29
27
|
#
|
30
|
-
def self.resolve_type(attr_type, options={}, constructor_block=nil)
|
31
|
-
|
32
|
-
klass = attr_type
|
33
|
-
else
|
34
|
-
name = attr_type.name.split("::").last # TOO EXPENSIVE?
|
35
|
-
|
36
|
-
klass = const_get(name) if const_defined?(name)
|
37
|
-
raise AttributorException.new("Could not find class with name #{name}") unless klass
|
38
|
-
raise AttributorException.new("Could not find attribute type for: #{name} [klass: #{klass.name}]") unless klass < Attributor::Type
|
39
|
-
end
|
28
|
+
def self.resolve_type(attr_type, options = {}, constructor_block = nil)
|
29
|
+
klass = self.find_type(attr_type)
|
40
30
|
|
41
|
-
if klass.constructable?
|
42
|
-
|
43
|
-
|
31
|
+
return klass.construct(constructor_block, options) if klass.constructable?
|
32
|
+
raise AttributorException, "Type: #{attr_type} does not support anonymous generation" if constructor_block
|
33
|
+
|
34
|
+
klass
|
35
|
+
end
|
44
36
|
|
45
|
-
|
37
|
+
def self.find_type(attr_type)
|
38
|
+
return attr_type if attr_type < Attributor::Type
|
39
|
+
name = attr_type.name.split('::').last # TOO EXPENSIVE?
|
46
40
|
|
41
|
+
klass = const_get(name) if const_defined?(name)
|
42
|
+
raise AttributorException, "Could not find class with name #{name}" unless klass
|
43
|
+
raise AttributorException, "Could not find attribute type for: #{name} [klass: #{klass.name}]" unless klass < Attributor::Type
|
47
44
|
klass
|
48
45
|
end
|
49
46
|
|
50
47
|
def self.type_name(type)
|
51
|
-
return
|
48
|
+
return type_name(type.class) unless type.is_a?(::Class)
|
52
49
|
|
53
50
|
type.ancestors.find { |k| k.name && !k.name.empty? }.name
|
54
51
|
end
|
55
52
|
|
56
|
-
def self.humanize_context(
|
57
|
-
return
|
53
|
+
def self.humanize_context(context)
|
54
|
+
return '' unless context
|
58
55
|
|
59
|
-
if context.
|
60
|
-
context = Array(context)
|
61
|
-
end
|
56
|
+
context = Array(context) if context.is_a? ::String
|
62
57
|
|
63
58
|
unless context.is_a? Enumerable
|
64
59
|
raise "INVALID CONTEXT!!! (got: #{context.inspect})"
|
@@ -66,18 +61,18 @@ module Attributor
|
|
66
61
|
|
67
62
|
begin
|
68
63
|
return context.join('.')
|
69
|
-
rescue
|
64
|
+
rescue e
|
70
65
|
raise "Error creating context string: #{e.message}"
|
71
66
|
end
|
72
67
|
end
|
73
68
|
|
74
|
-
def self.errorize_value(
|
75
|
-
inspection =value.inspect
|
76
|
-
inspection = inspection[0..500]+
|
69
|
+
def self.errorize_value(value)
|
70
|
+
inspection = value.inspect
|
71
|
+
inspection = inspection[0..500] + '...[truncated]' if inspection.size > 500
|
77
72
|
inspection
|
78
73
|
end
|
79
74
|
|
80
|
-
MODULE_PREFIX =
|
75
|
+
MODULE_PREFIX = 'Attributor::'.freeze
|
81
76
|
MODULE_PREFIX_REGEX = ::Regexp.new(MODULE_PREFIX)
|
82
77
|
|
83
78
|
require_relative 'attributor/families/numeric'
|
@@ -101,7 +96,7 @@ module Attributor
|
|
101
96
|
require_relative 'attributor/types/model'
|
102
97
|
require_relative 'attributor/types/struct'
|
103
98
|
require_relative 'attributor/types/class'
|
104
|
-
|
99
|
+
require_relative 'attributor/types/polymorphic'
|
105
100
|
|
106
101
|
require_relative 'attributor/types/csv'
|
107
102
|
require_relative 'attributor/types/ids'
|
@@ -110,5 +105,4 @@ module Attributor
|
|
110
105
|
require_relative 'attributor/types/tempfile'
|
111
106
|
require_relative 'attributor/types/file_upload'
|
112
107
|
require_relative 'attributor/types/uri'
|
113
|
-
|
114
108
|
end
|
data/lib/attributor/attribute.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# TODO: profile keys for attributes, test as frozen strings
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
|
5
4
|
class FakeParent < ::BasicObject
|
5
|
+
def respond_to_missing?(_method_name)
|
6
|
+
true
|
7
|
+
end
|
6
8
|
|
7
|
-
def method_missing(name, *
|
8
|
-
::Kernel.warn "Warning, you have tried to access the '#{name}' method of the 'parent' argument of a Proc-defined :default values."
|
9
|
-
"Those Procs should completely ignore the 'parent' attribute for the moment as it will be set to an "
|
10
|
-
|
9
|
+
def method_missing(name, *_args) # rubocop:disable Style/MethodMissing
|
10
|
+
::Kernel.warn "Warning, you have tried to access the '#{name}' method of the 'parent' argument of a Proc-defined :default values." \
|
11
|
+
"Those Procs should completely ignore the 'parent' attribute for the moment as it will be set to an " \
|
12
|
+
'instance of a useless class (until the framework can provide such functionality)'
|
11
13
|
nil
|
12
14
|
end
|
13
15
|
|
@@ -18,58 +20,53 @@ module Attributor
|
|
18
20
|
# It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
|
19
21
|
# TODO: should this be a mixin since it is an abstract class?
|
20
22
|
class Attribute
|
21
|
-
|
22
23
|
attr_reader :type, :options
|
23
24
|
|
24
25
|
# @options: metadata about the attribute
|
25
26
|
# @block: code definition for struct attributes (nil for predefined types or leaf/simple types)
|
26
|
-
def initialize(type, options={}, &block)
|
27
|
+
def initialize(type, options = {}, &block)
|
27
28
|
@type = Attributor.resolve_type(type, options, block)
|
28
29
|
|
29
30
|
@options = options
|
30
|
-
if @type.respond_to?(:options)
|
31
|
-
@options = @type.options.merge(@options)
|
32
|
-
end
|
31
|
+
@options = @type.options.merge(@options) if @type.respond_to?(:options)
|
33
32
|
|
34
33
|
check_options!
|
35
34
|
end
|
36
35
|
|
37
|
-
def ==(
|
38
|
-
raise ArgumentError, "can not compare Attribute with #{
|
36
|
+
def ==(other)
|
37
|
+
raise ArgumentError, "can not compare Attribute with #{other.class.name}" unless other.is_a?(Attribute)
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
type == other.type &&
|
40
|
+
options == other.options
|
42
41
|
end
|
43
42
|
|
43
|
+
def parse(value, context = Attributor::DEFAULT_ROOT_CONTEXT)
|
44
|
+
object = load(value, context)
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
errors = self.validate(object,context)
|
49
|
-
[ object, errors ]
|
50
|
-
end
|
51
|
-
|
46
|
+
errors = validate(object, context)
|
47
|
+
[object, errors]
|
48
|
+
end
|
52
49
|
|
53
|
-
def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT,
|
54
|
-
value = type.load(value,context
|
50
|
+
def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
51
|
+
value = type.load(value, context, **options)
|
55
52
|
|
56
|
-
if value.nil? && self.options.
|
53
|
+
if value.nil? && self.options.key?(:default)
|
57
54
|
defined_val = self.options[:default]
|
58
55
|
val = case defined_val
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
value = val #Need to load?
|
56
|
+
when ::Proc
|
57
|
+
fake_parent = FakeParent.new
|
58
|
+
# TODO: we can only support "context" as a parameter to the proc for now, since we don't have the parent...
|
59
|
+
if defined_val.arity == 2
|
60
|
+
defined_val.call(fake_parent, context)
|
61
|
+
elsif defined_val.arity == 1
|
62
|
+
defined_val.call(fake_parent)
|
63
|
+
else
|
64
|
+
defined_val.call
|
65
|
+
end
|
66
|
+
else
|
67
|
+
defined_val
|
68
|
+
end
|
69
|
+
value = val # Need to load?
|
73
70
|
end
|
74
71
|
|
75
72
|
value
|
@@ -83,148 +80,136 @@ module Attributor
|
|
83
80
|
type.dump(value, **opts)
|
84
81
|
end
|
85
82
|
|
86
|
-
|
87
83
|
def validate_type(value, context)
|
88
84
|
# delegate check to type subclass if it exists
|
89
|
-
unless
|
85
|
+
unless type.valid_type?(value)
|
90
86
|
msg = "Attribute #{Attributor.humanize_context(context)} received value: "
|
91
87
|
msg += "#{Attributor.errorize_value(value)} is of the wrong type "
|
92
|
-
msg += "(got: #{value.class.name}, expected: #{
|
88
|
+
msg += "(got: #{value.class.name}, expected: #{type.name})"
|
93
89
|
return [msg]
|
94
90
|
end
|
95
91
|
[]
|
96
92
|
end
|
97
93
|
|
94
|
+
TOP_LEVEL_OPTIONS = [:description, :values, :default, :example, :required, :required_if, :custom_data].freeze
|
95
|
+
INTERNAL_OPTIONS = [:dsl_compiler, :dsl_compiler_options].freeze # Options we don't want to expose when describing attributes
|
98
96
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
def describe(shallow=true, example: nil)
|
103
|
-
description = { }
|
97
|
+
def describe(shallow = true, example: nil)
|
98
|
+
description = {}
|
104
99
|
# Clone the common options
|
105
100
|
TOP_LEVEL_OPTIONS.each do |option_name|
|
106
|
-
description[option_name] =
|
101
|
+
description[option_name] = options[option_name] if options.key? option_name
|
107
102
|
end
|
108
103
|
|
109
104
|
# Make sure this option definition is not mistaken for the real generated example
|
110
|
-
if (
|
105
|
+
if (ex_def = description.delete(:example))
|
111
106
|
description[:example_definition] = ex_def
|
112
107
|
end
|
113
108
|
|
114
|
-
special_options =
|
109
|
+
special_options = options.keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS
|
115
110
|
description[:options] = {} unless special_options.empty?
|
116
111
|
special_options.each do |opt_name|
|
117
|
-
description[:options][opt_name] =
|
112
|
+
description[:options][opt_name] = options[opt_name]
|
118
113
|
end
|
119
114
|
# Change the reference option to the actual class name.
|
120
|
-
if (
|
115
|
+
if (reference = options[:reference])
|
121
116
|
description[:options][:reference] = reference.name
|
122
117
|
end
|
123
118
|
|
124
|
-
description[:type] =
|
119
|
+
description[:type] = type.describe(shallow, example: example)
|
125
120
|
# Move over any example from the type, into the attribute itself
|
126
|
-
if (
|
127
|
-
description[:example] =
|
121
|
+
if (ex = description[:type].delete(:example))
|
122
|
+
description[:example] = dump(ex)
|
128
123
|
end
|
129
124
|
|
130
125
|
description
|
131
126
|
end
|
132
127
|
|
133
|
-
|
134
128
|
def example_from_options(parent, context)
|
135
|
-
val =
|
129
|
+
val = options[:example]
|
136
130
|
generated = case val
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
131
|
+
when ::Regexp
|
132
|
+
val.gen
|
133
|
+
when ::Proc
|
134
|
+
if val.arity == 2
|
135
|
+
val.call(parent, context)
|
136
|
+
elsif val.arity == 1
|
137
|
+
val.call(parent)
|
138
|
+
else
|
139
|
+
val.call
|
140
|
+
end
|
141
|
+
when nil
|
142
|
+
nil
|
143
|
+
else
|
144
|
+
val
|
145
|
+
end
|
146
|
+
load(generated, context)
|
153
147
|
end
|
154
148
|
|
155
|
-
def example(context=nil, parent: nil, values:{})
|
156
|
-
raise ArgumentError,
|
149
|
+
def example(context = nil, parent: nil, values: {})
|
150
|
+
raise ArgumentError, 'attribute example cannot take a context of type String' if context.is_a? ::String
|
157
151
|
if context
|
158
152
|
ctx = Attributor.humanize_context(context)
|
159
|
-
seed,
|
153
|
+
seed, = Digest::SHA1.digest(ctx).unpack('QQ')
|
160
154
|
Random.srand(seed)
|
161
155
|
else
|
162
156
|
context = Attributor::DEFAULT_ROOT_CONTEXT
|
163
157
|
end
|
164
158
|
|
165
|
-
if
|
159
|
+
if options.key? :example
|
166
160
|
loaded = example_from_options(parent, context)
|
167
|
-
errors =
|
161
|
+
errors = validate(loaded, context)
|
168
162
|
raise AttributorException, "Error generating example for #{Attributor.humanize_context(context)}. Errors: #{errors.inspect}" if errors.any?
|
169
|
-
loaded
|
170
|
-
else
|
171
|
-
if (option_values = self.options[:values])
|
172
|
-
option_values.pick
|
173
|
-
else
|
174
|
-
if type.respond_to?(:attributes)
|
175
|
-
self.type.example(context, values)
|
176
|
-
else
|
177
|
-
self.type.example(context, options: self.options)
|
178
|
-
end
|
179
|
-
end
|
163
|
+
return loaded
|
180
164
|
end
|
181
|
-
end
|
182
165
|
|
166
|
+
return options[:values].pick if options.key? :values
|
183
167
|
|
184
|
-
|
185
|
-
|
186
|
-
type.attributes
|
168
|
+
if type.respond_to?(:attributes)
|
169
|
+
type.example(context, values)
|
187
170
|
else
|
188
|
-
|
171
|
+
type.example(context, options: options)
|
189
172
|
end
|
190
173
|
end
|
191
174
|
|
175
|
+
def attributes
|
176
|
+
type.attributes if @type_has_attributes ||= type.respond_to?(:attributes)
|
177
|
+
end
|
192
178
|
|
193
179
|
# Validates stuff and checks dependencies
|
194
|
-
def validate(object, context=Attributor::DEFAULT_ROOT_CONTEXT
|
180
|
+
def validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT)
|
195
181
|
raise "INVALID CONTEXT!! #{context}" unless context
|
196
182
|
# Validate any requirements, absolute or conditional, and return.
|
197
183
|
|
198
184
|
if object.nil? # == Attributor::UNSET
|
199
185
|
# With no value, we can only validate whether that is acceptable or not and return.
|
200
186
|
# Beyond that, no further validation should be done.
|
201
|
-
return
|
187
|
+
return validate_missing_value(context)
|
202
188
|
end
|
203
189
|
|
204
190
|
# TODO: support validation for other types of conditional dependencies based on values of other attributes
|
205
191
|
|
206
|
-
errors =
|
192
|
+
errors = validate_type(object, context)
|
207
193
|
|
208
194
|
# End validation if we don't even have the proper type to begin with
|
209
195
|
return errors if errors.any?
|
210
196
|
|
211
|
-
if
|
212
|
-
errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{
|
197
|
+
if options[:values] && !options[:values].include?(object)
|
198
|
+
errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{options[:values].inspect} "
|
213
199
|
end
|
214
200
|
|
215
|
-
errors +
|
201
|
+
errors + type.validate(object, context, self)
|
216
202
|
end
|
217
203
|
|
218
|
-
|
219
204
|
def validate_missing_value(context)
|
220
205
|
raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
|
221
206
|
|
222
207
|
# Missing attribute was required if :required option was set
|
223
|
-
return ["Attribute #{Attributor.humanize_context(context)} is required"] if
|
208
|
+
return ["Attribute #{Attributor.humanize_context(context)} is required"] if options[:required]
|
224
209
|
|
225
210
|
# Missing attribute was not required if :required_if (and :required)
|
226
211
|
# option was NOT set
|
227
|
-
requirement =
|
212
|
+
requirement = options[:required_if]
|
228
213
|
return [] unless requirement
|
229
214
|
|
230
215
|
case requirement
|
@@ -237,7 +222,7 @@ module Attributor
|
|
237
222
|
predicate = requirement.values.first
|
238
223
|
else
|
239
224
|
# should never get here if the option validation worked...
|
240
|
-
raise AttributorException
|
225
|
+
raise AttributorException, "unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}"
|
241
226
|
end
|
242
227
|
|
243
228
|
# chop off the last part
|
@@ -253,11 +238,11 @@ module Attributor
|
|
253
238
|
message << "(for #{Attributor.humanize_context(requirement_context)}) "
|
254
239
|
end
|
255
240
|
|
256
|
-
if predicate
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
241
|
+
message << if predicate
|
242
|
+
"matches #{predicate.inspect}."
|
243
|
+
else
|
244
|
+
'is present.'
|
245
|
+
end
|
261
246
|
|
262
247
|
[message]
|
263
248
|
else
|
@@ -265,48 +250,44 @@ module Attributor
|
|
265
250
|
end
|
266
251
|
end
|
267
252
|
|
268
|
-
|
269
253
|
def check_options!
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
end
|
254
|
+
options.each do |option_name, option_value|
|
255
|
+
next unless check_option!(option_name, option_value) == :unknown
|
256
|
+
if type.check_option!(option_name, option_value) == :unknown
|
257
|
+
raise AttributorException, "unsupported option: #{option_name} with value: #{option_value.inspect} for attribute: #{inspect}"
|
275
258
|
end
|
276
259
|
end
|
277
260
|
|
278
261
|
true
|
279
262
|
end
|
280
263
|
|
281
|
-
|
282
264
|
# TODO: override in type subclass
|
283
265
|
def check_option!(name, definition)
|
284
266
|
case name
|
285
267
|
when :values
|
286
|
-
raise AttributorException
|
268
|
+
raise AttributorException, "Allowed set of values requires an array. Got (#{definition})" unless definition.is_a? ::Array
|
287
269
|
when :default
|
288
|
-
raise AttributorException
|
289
|
-
|
270
|
+
raise AttributorException, "Default value doesn't have the correct attribute type. Got (#{definition.inspect})" unless type.valid_type?(definition) || definition.is_a?(Proc)
|
271
|
+
options[:default] = load(definition) unless definition.is_a?(Proc)
|
290
272
|
when :description
|
291
|
-
raise AttributorException
|
273
|
+
raise AttributorException, "Description value must be a string. Got (#{definition})" unless definition.is_a? ::String
|
292
274
|
when :required
|
293
|
-
raise AttributorException
|
294
|
-
raise AttributorException
|
275
|
+
raise AttributorException, 'Required must be a boolean' unless definition == true || definition == false
|
276
|
+
raise AttributorException, 'Required cannot be enabled in combination with :default' if definition == true && options.key?(:default)
|
295
277
|
when :required_if
|
296
|
-
raise AttributorException
|
297
|
-
raise AttributorException
|
278
|
+
raise AttributorException, 'Required_if must be a String, a Hash definition or a Proc' unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc)
|
279
|
+
raise AttributorException, 'Required_if cannot be specified together with :required' if options[:required]
|
298
280
|
when :example
|
299
|
-
unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? ||
|
300
|
-
raise AttributorException
|
281
|
+
unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || type.valid_type?(definition)
|
282
|
+
raise AttributorException, "Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)"
|
301
283
|
end
|
302
284
|
when :custom_data
|
303
|
-
raise AttributorException
|
285
|
+
raise AttributorException, "custom_data must be a Hash. Got (#{definition})" unless definition.is_a?(::Hash)
|
304
286
|
else
|
305
287
|
return :unknown # unknown option
|
306
288
|
end
|
307
289
|
|
308
290
|
:ok # passes
|
309
291
|
end
|
310
|
-
|
311
292
|
end
|
312
293
|
end
|