attributor 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +3 -0
- data/Guardfile +12 -0
- data/LICENSE +22 -0
- data/README.md +62 -0
- data/Rakefile +28 -0
- data/attributor.gemspec +40 -0
- data/lib/attributor.rb +89 -0
- data/lib/attributor/attribute.rb +271 -0
- data/lib/attributor/attribute_resolver.rb +116 -0
- data/lib/attributor/dsl_compiler.rb +106 -0
- data/lib/attributor/exceptions.rb +38 -0
- data/lib/attributor/extensions/randexp.rb +10 -0
- data/lib/attributor/type.rb +117 -0
- data/lib/attributor/types/boolean.rb +26 -0
- data/lib/attributor/types/collection.rb +135 -0
- data/lib/attributor/types/container.rb +42 -0
- data/lib/attributor/types/csv.rb +10 -0
- data/lib/attributor/types/date_time.rb +36 -0
- data/lib/attributor/types/file_upload.rb +11 -0
- data/lib/attributor/types/float.rb +27 -0
- data/lib/attributor/types/hash.rb +337 -0
- data/lib/attributor/types/ids.rb +26 -0
- data/lib/attributor/types/integer.rb +63 -0
- data/lib/attributor/types/model.rb +316 -0
- data/lib/attributor/types/object.rb +19 -0
- data/lib/attributor/types/string.rb +25 -0
- data/lib/attributor/types/struct.rb +50 -0
- data/lib/attributor/types/tempfile.rb +36 -0
- data/lib/attributor/version.rb +3 -0
- data/spec/attribute_resolver_spec.rb +227 -0
- data/spec/attribute_spec.rb +597 -0
- data/spec/attributor_spec.rb +25 -0
- data/spec/dsl_compiler_spec.rb +130 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/models.rb +81 -0
- data/spec/support/types.rb +21 -0
- data/spec/type_spec.rb +134 -0
- data/spec/types/boolean_spec.rb +85 -0
- data/spec/types/collection_spec.rb +286 -0
- data/spec/types/container_spec.rb +49 -0
- data/spec/types/csv_spec.rb +17 -0
- data/spec/types/date_time_spec.rb +90 -0
- data/spec/types/file_upload_spec.rb +6 -0
- data/spec/types/float_spec.rb +78 -0
- data/spec/types/hash_spec.rb +372 -0
- data/spec/types/ids_spec.rb +32 -0
- data/spec/types/integer_spec.rb +151 -0
- data/spec/types/model_spec.rb +401 -0
- data/spec/types/string_spec.rb +55 -0
- data/spec/types/struct_spec.rb +189 -0
- data/spec/types/tempfile_spec.rb +6 -0
- 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
data/.rspec
ADDED
data/.travis.yml
ADDED
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
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
|
data/attributor.gemspec
ADDED
@@ -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
|