attributor 2.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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CHANGELOG.md +52 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +12 -0
  8. data/LICENSE +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +28 -0
  11. data/attributor.gemspec +40 -0
  12. data/lib/attributor.rb +89 -0
  13. data/lib/attributor/attribute.rb +271 -0
  14. data/lib/attributor/attribute_resolver.rb +116 -0
  15. data/lib/attributor/dsl_compiler.rb +106 -0
  16. data/lib/attributor/exceptions.rb +38 -0
  17. data/lib/attributor/extensions/randexp.rb +10 -0
  18. data/lib/attributor/type.rb +117 -0
  19. data/lib/attributor/types/boolean.rb +26 -0
  20. data/lib/attributor/types/collection.rb +135 -0
  21. data/lib/attributor/types/container.rb +42 -0
  22. data/lib/attributor/types/csv.rb +10 -0
  23. data/lib/attributor/types/date_time.rb +36 -0
  24. data/lib/attributor/types/file_upload.rb +11 -0
  25. data/lib/attributor/types/float.rb +27 -0
  26. data/lib/attributor/types/hash.rb +337 -0
  27. data/lib/attributor/types/ids.rb +26 -0
  28. data/lib/attributor/types/integer.rb +63 -0
  29. data/lib/attributor/types/model.rb +316 -0
  30. data/lib/attributor/types/object.rb +19 -0
  31. data/lib/attributor/types/string.rb +25 -0
  32. data/lib/attributor/types/struct.rb +50 -0
  33. data/lib/attributor/types/tempfile.rb +36 -0
  34. data/lib/attributor/version.rb +3 -0
  35. data/spec/attribute_resolver_spec.rb +227 -0
  36. data/spec/attribute_spec.rb +597 -0
  37. data/spec/attributor_spec.rb +25 -0
  38. data/spec/dsl_compiler_spec.rb +130 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/support/models.rb +81 -0
  41. data/spec/support/types.rb +21 -0
  42. data/spec/type_spec.rb +134 -0
  43. data/spec/types/boolean_spec.rb +85 -0
  44. data/spec/types/collection_spec.rb +286 -0
  45. data/spec/types/container_spec.rb +49 -0
  46. data/spec/types/csv_spec.rb +17 -0
  47. data/spec/types/date_time_spec.rb +90 -0
  48. data/spec/types/file_upload_spec.rb +6 -0
  49. data/spec/types/float_spec.rb +78 -0
  50. data/spec/types/hash_spec.rb +372 -0
  51. data/spec/types/ids_spec.rb +32 -0
  52. data/spec/types/integer_spec.rb +151 -0
  53. data/spec/types/model_spec.rb +401 -0
  54. data/spec/types/string_spec.rb +55 -0
  55. data/spec/types/struct_spec.rb +189 -0
  56. data/spec/types/tempfile_spec.rb +6 -0
  57. metadata +348 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 102708c30b51b0ddb7a38488ca440ada38c2f3a8
4
+ data.tar.gz: 4e50cb2a981f4b1d6173e754789a25932153d7bd
5
+ SHA512:
6
+ metadata.gz: 47095d5c279287a05b302867f1e9d5518d511405d1a475e1ebd272f584c3f71284bb8d59eaab9da7ad79043a0d1179ff29514052de69a0ac48fecc9315ad35a4
7
+ data.tar.gz: 495d613202a0b19dd186cd302e5fdffdaeea8e40c059e4c7ba6b7b81a175e10bd06fbf6a8b1349c9fe12c88c93c8da5beca53cf8a7e25151eb9eecd4da5a3b17
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ # simplecov generated
2
+ coverage
3
+
4
+ # rbenv
5
+ .ruby-version
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # For MacOS:
15
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=Fuubar
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.1.2"
4
+ script: bundle exec rspec spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ Attributor Changelog
2
+ ============================
3
+
4
+ next
5
+ ------
6
+
7
+ * First new feature here
8
+
9
+ 2.1.0
10
+ ------
11
+
12
+ * Structs now inherit type-level options from their reference.
13
+ * Add Collection subclasses for CSVs and Ids
14
+ * CSV type for Collection of values serialized as comma-separated strings.
15
+ * Ids type. A helper for creating CSVs with members matching a given a type's :identity option.
16
+ * Allow instances of Models to be initialized with initial data.
17
+ * Supported formats for the data are equivalent to the loading formats (i.e. ruby Hash, a JSON string or another instance of the same model type).
18
+ * Improved context reporting in errors
19
+ * Added contextual information while loading and dumping attributes.
20
+ * `load` takes a new `context` argument (defaulting to a system-wide root) in the form of an array of parent segments.
21
+ * `validate` takes a `context` argument that (instead of a string) is now an array of parent segments.
22
+ * `dump` takes a `context:` option parameter of the same type
23
+ * Enhanced error messages to report the correct context scope.
24
+ * Make Attribute assignments in models to report a special context (not the attributor root)
25
+ * Instead of reporting "$." as the context , when doing model.field_name=value, they'll now report "assignment.of(field_name)" instead
26
+ * Truncate the lenght of values when reporting loading errors when they're long (i.e. >500 chars)
27
+ * `Model.attributes` may now be called more than once to set add or replace attributes. The exact behavior depends upon the types of the attributes being added or replaced. See [model_spec.rb](spec/types/model_spec.rb) for examples.
28
+ * Greately enhanced Hash type with individual key specification (rather than
29
+ simply defining the types of keys)
30
+ * Loaded Hash types now return instances of the class rather than a simple Ruby Hash.
31
+ * Introduced a new FileUpload type. This can be easily used in Web servers to map incoming multipart file uploads.
32
+ * Introduced a new Tempfile type.
33
+
34
+ 2.0.0
35
+ ------
36
+
37
+ * Added new exception subtypes (load methods return more precise errors now)
38
+ * Changed ```Attributor::Model``` to be a class instead of module.
39
+ * Improved handling of ```Attributor::Model``` examples:
40
+ * Support creating examples with specific values. i.e.:
41
+ ```ruby
42
+ person = Person.example(name: "Bob")
43
+ person.name # => "Bob"
44
+ ```
45
+ * Example values are now lazily initialized when used.
46
+ * Terminate sub-attribute generation after ```Attributor::Model::MAX_EXAMPLE_DEPTH``` levels to prevent infinite generation.
47
+ * Added additional options for Attribute :example values:
48
+ * explicit nil values
49
+ * procs that take 2 arguments now receive the context as the second argument.
50
+ * Circular references are now detected and handled in validation and dumping.
51
+ * Fixed bug with Model attribute accessors when using false values.
52
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: 'bundle exec rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/attributor/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ watch('lib/attributor/base.rb') { "spec" }
9
+ watch('spec/support/models.rb') { "spec" }
10
+ watch('lib/attributor.rb') { 'spec' }
11
+ end
12
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 RightScale
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Attributor
2
+
3
+ An Attribute management, self documenting framework, designed for getting rid of most of your parameter handling boilerplate.
4
+ While initially designed to be the backbone for parameter handling in REST services, attribute management can be applied in many other areas.
5
+
6
+ With Attributor you can:
7
+ * Express complex and type-rich attribute structures using an elegant ruby DSL
8
+ * Process incoming values against those designs:
9
+ * By verifying they follow constraints and available options specified in your design
10
+ * By coercing values into the specified types when there's a type impedance mismatch
11
+ * By checking presence requirements and conditional dependencies
12
+ * and all with a powerful error reporting system that describes with great detail what errors were encountered
13
+ * Export structured information (in JSON) about your attribute definitions which allows you to:
14
+ * easily consume it to generate human consumable documentation about parameter expectations
15
+ * easily aggregate it across different systems.
16
+
17
+
18
+ ## General Help
19
+
20
+ ### Running specs:
21
+
22
+ `bundle exec rake spec`
23
+
24
+ Note: This should also compute code coverage. See below for details on viewing code coverage.
25
+
26
+ ### Generating documentation:
27
+
28
+ `bundle exec yard`
29
+
30
+ ### Computing documentation coverage:
31
+
32
+ `bundle exec yardstick 'lib/**/*.rb'`
33
+
34
+ ### Computing code coverage:
35
+
36
+ `bundle exec rake spec`
37
+
38
+ `open coverage/index.html`
39
+
40
+
41
+ ## Contributing to attributor
42
+
43
+ * Check out the latest "master" branch to make sure the feature hasn't been
44
+ implemented or the bug hasn't been fixed yet.
45
+ * Check out the issue tracker to make sure someone already hasn't requested it
46
+ and/or contributed it.
47
+ * Fork the project.
48
+ * Start a feature/bugfix branch.
49
+ * Commit and push until you are happy with your contribution.
50
+ * Make sure to add tests for it.
51
+ ** This is important to ensure it's not broken in a future version. **
52
+ * Please try not to mess with the Rakefile, version, or history.
53
+ If you want your own version, or if it is otherwise necessary, that is fine,
54
+ but please isolate to its own commit so we can cherry-pick around it.
55
+
56
+
57
+
58
+ ## License
59
+
60
+ This software is released under the [MIT License](http://www.opensource.org/licenses/MIT). Please see [LICENSE](LICENSE) for further details.
61
+
62
+ Copyright (c) 2014 RightScale
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ require 'bundler/setup'
5
+ #begin
6
+ # Bundler.setup(:default, :development)
7
+ #rescue Bundler::BundlerError => e
8
+ # $stderr.puts e.message
9
+ # $stderr.puts "Run `bundle install` to install missing gems"
10
+ # exit e.status_code
11
+ #end
12
+ require 'rake'
13
+
14
+ require 'rake/notes/rake_task'
15
+
16
+ require 'rspec/core'
17
+ require 'rspec/core/rake_task'
18
+
19
+ desc "Run RSpec code examples with simplecov"
20
+ RSpec::Core::RakeTask.new do |spec|
21
+ spec.rspec_opts = ["-c"]
22
+ spec.pattern = FileList['spec/**/*_spec.rb']
23
+ end
24
+
25
+ task :default => :spec
26
+
27
+ require 'yard'
28
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,40 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'attributor/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "attributor"
9
+ spec.version = Attributor::VERSION
10
+ spec.authors = ["Josep M. Blanquer","Dane Jensen"]
11
+ spec.date = "2014-08-15"
12
+ spec.summary = "A powerful attribute and type management library for Ruby"
13
+ spec.email = ["blanquer@gmail.com","dane.jensen@gmail.com"]
14
+
15
+ spec.homepage = "https://github.com/rightscale/attributor"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">=2.1"
18
+
19
+ spec.require_paths = ["lib"]
20
+ spec.files = `git ls-files -z`.split("\x0")
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+
23
+ spec.add_runtime_dependency(%q<hashie>, ["~> 3"])
24
+ spec.add_runtime_dependency(%q<randexp>, ["~> 0"])
25
+ spec.add_development_dependency(%q<rspec>, ["< 2.99"])
26
+ spec.add_development_dependency(%q<yard>, ["~> 0.8.7"])
27
+ spec.add_development_dependency(%q<backports>, ["~> 3"])
28
+ spec.add_development_dependency(%q<yardstick>, ["~> 0"])
29
+ spec.add_development_dependency(%q<redcarpet>, ["< 3.0"])
30
+ spec.add_development_dependency(%q<bundler>, [">= 0"])
31
+ spec.add_development_dependency(%q<rake-notes>, ["~> 0"])
32
+ spec.add_development_dependency(%q<simplecov>, ["~> 0"])
33
+ spec.add_development_dependency(%q<guard>, ["~> 2"])
34
+ spec.add_development_dependency(%q<guard-rspec>, ["~> 4"])
35
+ spec.add_development_dependency(%q<pry>, ["~> 0"])
36
+ spec.add_development_dependency(%q<pry-byebug>, ["~> 1"])
37
+ spec.add_development_dependency(%q<pry-stack_explorer>, ["~> 0"])
38
+ spec.add_development_dependency(%q<fuubar>, ["~> 1"])
39
+ end
40
+
data/lib/attributor.rb ADDED
@@ -0,0 +1,89 @@
1
+ require 'json'
2
+ require 'randexp'
3
+
4
+ require 'hashie'
5
+
6
+ require 'digest/sha1'
7
+
8
+ module Attributor
9
+
10
+ require_relative 'attributor/exceptions'
11
+ require_relative 'attributor/attribute'
12
+ require_relative 'attributor/type'
13
+ require_relative 'attributor/dsl_compiler'
14
+ require_relative 'attributor/attribute_resolver'
15
+
16
+
17
+ require_relative 'attributor/extensions/randexp'
18
+
19
+
20
+
21
+ # List of all basic types (i.e. not collections, structs or models)
22
+
23
+ # hierarchical separator string for composing human readable attributes
24
+ SEPARATOR = '.'.freeze
25
+ DEFAULT_ROOT_CONTEXT = ['$'].freeze
26
+
27
+ # @param type [Class] The class of the type to resolve
28
+ #
29
+ def self.resolve_type(attr_type, options={}, constructor_block=nil)
30
+ if attr_type < Attributor::Type
31
+ klass = attr_type
32
+ else
33
+ name = attr_type.name.split("::").last # TOO EXPENSIVE?
34
+
35
+ klass = const_get(name) if const_defined?(name)
36
+ raise AttributorException.new("Could not find class with name #{name}") unless klass
37
+ raise AttributorException.new("Could not find attribute type for: #{name} [klass: #{klass.name}]") unless klass < Attributor::Type
38
+ end
39
+
40
+ if klass.respond_to?(:construct)
41
+ return klass.construct(constructor_block, options)
42
+ end
43
+
44
+ raise AttributorException.new("Type: #{attr_type} does not support anonymous generation") if constructor_block
45
+
46
+ klass
47
+ end
48
+
49
+ def self.humanize_context( context )
50
+ raise "NIL CONTEXT PASSED TO HUMANZE!!" unless context
51
+ raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
52
+ begin
53
+ return context.join('.')
54
+ rescue Exception => e
55
+ raise "Error creating context string: #{e.message}"
56
+ end
57
+ end
58
+
59
+ def self.errorize_value( value )
60
+ inspection =value.inspect
61
+ inspection = inspection[0..500]+ "...[truncated]" if inspection.size>500
62
+ inspection
63
+ end
64
+
65
+ MODULE_PREFIX = "Attributor\:\:".freeze
66
+ MODULE_PREFIX_REGEX = Regexp.new(MODULE_PREFIX)
67
+
68
+ require_relative 'attributor/types/container'
69
+ require_relative 'attributor/types/object'
70
+ require_relative 'attributor/types/integer'
71
+ require_relative 'attributor/types/string'
72
+ require_relative 'attributor/types/model'
73
+ require_relative 'attributor/types/struct'
74
+ require_relative 'attributor/types/boolean'
75
+ require_relative 'attributor/types/date_time'
76
+ require_relative 'attributor/types/float'
77
+ require_relative 'attributor/types/collection'
78
+ require_relative 'attributor/types/hash'
79
+
80
+
81
+ require_relative 'attributor/types/csv'
82
+ require_relative 'attributor/types/ids'
83
+
84
+ # TODO: move these to 'optional types' or 'extra types'... location
85
+ require_relative 'attributor/types/tempfile'
86
+ require_relative 'attributor/types/file_upload'
87
+
88
+
89
+ end
@@ -0,0 +1,271 @@
1
+ # TODO: profile keys for attributes, test as frozen strings
2
+
3
+ module Attributor
4
+
5
+ # It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
6
+ # TODO: should this be a mixin since it is an abstract class?
7
+ class Attribute
8
+
9
+ attr_reader :type, :options
10
+
11
+ # @options: metadata about the attribute
12
+ # @block: code definition for struct attributes (nil for predefined types or leaf/simple types)
13
+ def initialize(type, options={}, &block)
14
+ @type = Attributor.resolve_type(type, options, block)
15
+
16
+ @options = options
17
+ if @type.respond_to?(:options)
18
+ @options = @type.options.merge(@options)
19
+ end
20
+
21
+ check_options!
22
+ end
23
+
24
+ def ==(attribute)
25
+ raise ArgumentError, "can not compare Attribute with #{attribute.class.name}" unless attribute.kind_of?(Attribute)
26
+
27
+ self.type == attribute.type &&
28
+ self.options == attribute.options
29
+ end
30
+
31
+
32
+ def parse(value, context=Attributor::DEFAULT_ROOT_CONTEXT)
33
+ object = self.load(value,context)
34
+
35
+ errors = self.validate(object,context)
36
+ [ object, errors ]
37
+ end
38
+
39
+
40
+ def load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
41
+ value = type.load(value,context,**options) unless value.nil?
42
+
43
+ # just in case type.load(value) returned nil, even though value is not nil.
44
+ if value.nil?
45
+ value = self.options[:default] if self.options[:default]
46
+ end
47
+
48
+ value
49
+ rescue AttributorException, NameError
50
+ raise
51
+ rescue => e
52
+ raise Attributor::LoadError, "Error loading attribute #{Attributor.humanize_context(context)} of type #{type.name} from value #{Attributor.errorize_value(value)}\n#{e.message}"
53
+ end
54
+
55
+ def dump(value, **opts)
56
+ type.dump(value, opts)
57
+ end
58
+
59
+
60
+ def validate_type(value, context)
61
+ # delegate check to type subclass if it exists
62
+ unless self.type.valid_type?(value)
63
+ msg = "Attribute #{Attributor.humanize_context(context)} received value: "
64
+ msg += "#{Attributor.errorize_value(value)} is of the wrong type "
65
+ msg += "(got: #{value.class.name}, expected: #{self.type.name})"
66
+ return [msg]
67
+ end
68
+ []
69
+ end
70
+
71
+
72
+ TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if ]
73
+ INTERNAL_OPTIONS = [:dsl_compiler] # Options we don't want to expose when describing attributes
74
+ def describe(shallow=true)
75
+ description = { }
76
+ # Clone the common options
77
+ TOP_LEVEL_OPTIONS.each do |option_name|
78
+ description[option_name] = self.options[option_name] if self.options.has_key? option_name
79
+ end
80
+
81
+ # Make sure this option definition is not mistaken for the real generated example
82
+ if ( ex_def = description.delete(:example) )
83
+ description[:example_definition] = ex_def
84
+ end
85
+ special_options = self.options.keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS
86
+ description[:options] = {} unless special_options.empty?
87
+ special_options.each do |opt_name|
88
+ description[:options][opt_name] = self.options[opt_name]
89
+ end
90
+ # Change the reference option to the actual class name.
91
+ if ( reference = self.options[:reference] )
92
+ description[:options][:reference] = reference.name
93
+ end
94
+
95
+ description[:type] = self.type.describe(shallow)
96
+ description
97
+ end
98
+
99
+
100
+ def example(context=nil, parent: nil, values:{})
101
+ raise ArgumentError, "attribute example cannot take a context of type String" if (context.is_a? ::String )
102
+ if context
103
+ ctx = Attributor.humanize_context(context)
104
+ seed, _ = Digest::SHA1.digest(ctx).unpack("QQ")
105
+ Random.srand(seed)
106
+ end
107
+
108
+ if self.options.has_key? :example
109
+ val = self.options[:example]
110
+ case val
111
+ when ::String
112
+ # FIXME: spec this properly to use self.type.native_type
113
+ val
114
+ when ::Regexp
115
+ self.load(val.gen,context)
116
+ when ::Array
117
+ # TODO: handle arrays of non native types, i.e. arrays of regexps.... ?
118
+ val.pick
119
+ when ::Proc
120
+ if val.arity == 2
121
+ val.call(parent, context)
122
+ elsif val.arity == 1
123
+ val.call(parent)
124
+ else
125
+ val.call
126
+ end
127
+ when nil
128
+ nil
129
+ else
130
+ raise AttributorException, "unknown example attribute type, got: #{val}"
131
+ end
132
+ else
133
+ if (option_values = self.options[:values])
134
+ option_values.pick
135
+ else
136
+ if type.respond_to?(:attributes)
137
+ self.type.example(context, values)
138
+ else
139
+ self.type.example(context, options: self.options)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+
146
+ def attributes
147
+ if (@type_has_attributes ||= type.respond_to?(:attributes))
148
+ type.attributes
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
154
+
155
+ # Validates stuff and checks dependencies
156
+ def validate(object, context=Attributor::DEFAULT_ROOT_CONTEXT )
157
+ raise "INVALID CONTEXT!! #{context}" unless context
158
+ # Validate any requirements, absolute or conditional, and return.
159
+
160
+ if object.nil? # == Attributor::UNSET
161
+ # With no value, we can only validate whether that is acceptable or not and return.
162
+ # Beyond that, no further validation should be done.
163
+ return self.validate_missing_value(context)
164
+ end
165
+
166
+ # TODO: support validation for other types of conditional dependencies based on values of other attributes
167
+
168
+ errors = self.validate_type(object,context)
169
+
170
+ # End validation if we don't even have the proper type to begin with
171
+ return errors if errors.any?
172
+
173
+ if self.options[:values] && !self.options[:values].include?(object)
174
+ errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{self.options[:values].inspect} "
175
+ end
176
+
177
+ errors + self.type.validate(object,context,self)
178
+ end
179
+
180
+
181
+ def validate_missing_value(context)
182
+ raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
183
+
184
+ # Missing attribute was required if :required option was set
185
+ return ["Attribute #{Attributor.humanize_context(context)} is required"] if self.options[:required]
186
+
187
+ # Missing attribute was not required if :required_if (and :required)
188
+ # option was NOT set
189
+ requirement = self.options[:required_if]
190
+ return [] unless requirement
191
+
192
+ case requirement
193
+ when ::String
194
+ key_path = requirement
195
+ predicate = nil
196
+ when ::Hash
197
+ # TODO: support multiple dependencies?
198
+ key_path = requirement.keys.first
199
+ predicate = requirement.values.first
200
+ else
201
+ # should never get here if the option validation worked...
202
+ raise AttributorException.new("unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}")
203
+ end
204
+
205
+ # chop off the last part
206
+ requirement_context = context[0..-2]
207
+ requirement_context_string = requirement_context.join(Attributor::SEPARATOR)
208
+
209
+ # FIXME: we're having to reconstruct a string context just to use the resolver...smell.
210
+ if AttributeResolver.current.check(requirement_context_string, key_path, predicate)
211
+ message = "Attribute #{Attributor.humanize_context(context)} is required when #{key_path} "
212
+
213
+ # give a hint about what the full path for a relative key_path would be
214
+ unless key_path[0..0] == Attributor::AttributeResolver::ROOT_PREFIX
215
+ message << "(for #{Attributor.humanize_context(requirement_context)}) "
216
+ end
217
+
218
+ if predicate
219
+ message << "matches #{predicate.inspect}."
220
+ else
221
+ message << "is present."
222
+ end
223
+
224
+ [message]
225
+ else
226
+ []
227
+ end
228
+ end
229
+
230
+
231
+ def check_options!
232
+ self.options.each do |option_name, option_value|
233
+ if self.check_option!(option_name, option_value) == :unknown
234
+ if self.type.check_option!(option_name, option_value) == :unknown
235
+ raise AttributorException.new("unsupported option: #{option_name} with value: #{option_value.inspect} for attribute: #{self.inspect}")
236
+ end
237
+ end
238
+ end
239
+
240
+ true
241
+ end
242
+
243
+
244
+ # TODO: override in type subclass
245
+ def check_option!(name, definition)
246
+ case name
247
+ when :values
248
+ raise AttributorException.new("Allowed set of values requires an array. Got (#{definition})") unless definition.is_a? ::Array
249
+ when :default
250
+ raise AttributorException.new("Default value doesn't have the correct attribute type. Got (#{definition.inspect})") unless self.type.valid_type?(definition) || definition.kind_of?(Proc)
251
+ when :description
252
+ raise AttributorException.new("Description value must be a string. Got (#{definition})") unless definition.is_a? ::String
253
+ when :required
254
+ raise AttributorException.new("Required must be a boolean") unless !!definition == definition # Boolean check
255
+ raise AttributorException.new("Required cannot be enabled in combination with :default") if definition == true && options.has_key?(:default)
256
+ when :required_if
257
+ raise AttributorException.new("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)
258
+ raise AttributorException.new("Required_if cannot be specified together with :required") if self.options[:required]
259
+ when :example
260
+ unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition)
261
+ raise AttributorException.new("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)")
262
+ end
263
+ else
264
+ return :unknown # unknown option
265
+ end
266
+
267
+ :ok # passes
268
+ end
269
+
270
+ end
271
+ end