attributor 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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