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.
- 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
|