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
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Attributor
|
4
|
+
|
5
|
+
|
6
|
+
class AttributeResolver
|
7
|
+
ROOT_PREFIX = '$'.freeze
|
8
|
+
|
9
|
+
class Data < ::Hash
|
10
|
+
include Hashie::Extensions::MethodReader
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :data
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@data = Data.new
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# TODO: support collection queries
|
21
|
+
def query!(key_path, path_prefix=ROOT_PREFIX)
|
22
|
+
# If the incoming key_path is not an absolute path, append the given prefix
|
23
|
+
# NOTE: Need to index key_path by range here because Ruby 1.8 returns a
|
24
|
+
# FixNum for the ASCII code, not the actual character, when indexing by a number.
|
25
|
+
unless key_path[0..0] == ROOT_PREFIX
|
26
|
+
# TODO: prepend path_prefix to path_prefix if it did not include it? hm.
|
27
|
+
key_path = path_prefix + SEPARATOR + key_path
|
28
|
+
end
|
29
|
+
|
30
|
+
# Discard the initial element, which should always be ROOT_PREFIX at this point
|
31
|
+
_root, *path = key_path.split(SEPARATOR)
|
32
|
+
|
33
|
+
# Follow the hierarchy path to the requested node and return it
|
34
|
+
# Example path => ["instance", "ssh_key", "name"]
|
35
|
+
# Example @data => {"instance" => { "ssh_key" => { "name" => "foobar" } }}
|
36
|
+
result = path.inject(@data) do |hash, key|
|
37
|
+
return nil if hash.nil?
|
38
|
+
hash.send key
|
39
|
+
end
|
40
|
+
result
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Query for a certain key in the attribute hierarchy
|
45
|
+
#
|
46
|
+
# @param [String] key_path The name of the key to query and its path
|
47
|
+
# @param [String] path_prefix
|
48
|
+
#
|
49
|
+
# @return [String] The value of the specified attribute/key
|
50
|
+
#
|
51
|
+
def query(key_path,path_prefix=ROOT_PREFIX)
|
52
|
+
query!(key_path,path_prefix)
|
53
|
+
rescue NoMethodError => e
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def register(key_path, value)
|
58
|
+
if key_path.split(SEPARATOR).size > 1
|
59
|
+
raise AttributorException.new("can only register top-level attributes. got: #{key_path}")
|
60
|
+
end
|
61
|
+
|
62
|
+
@data[key_path] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Checks that the the condition is met. This means the attribute identified
|
67
|
+
# by path_prefix and key_path satisfies the optional predicate, which when
|
68
|
+
# nil simply checks for existence.
|
69
|
+
#
|
70
|
+
# @param path_prefix [String]
|
71
|
+
# @param key_path [String]
|
72
|
+
# @param predicate [String|Regexp|Proc|NilClass]
|
73
|
+
#
|
74
|
+
# @returns [Boolean] True if :required_if condition is met, false otherwise
|
75
|
+
#
|
76
|
+
# @raise [AttributorException] When an unsupported predicate is passed
|
77
|
+
#
|
78
|
+
def check(path_prefix, key_path, predicate=nil)
|
79
|
+
value = self.query(key_path, path_prefix)
|
80
|
+
|
81
|
+
# we have a value, any value, which is good enough given no predicate
|
82
|
+
if !value.nil? && predicate.nil?
|
83
|
+
return true
|
84
|
+
end
|
85
|
+
|
86
|
+
case predicate
|
87
|
+
when ::String, ::Regexp, ::Integer, ::Float, ::DateTime, true, false
|
88
|
+
return predicate === value
|
89
|
+
when ::Proc
|
90
|
+
# Cannot use === here as above due to different behavior in Ruby 1.8
|
91
|
+
return predicate.call(value)
|
92
|
+
when nil
|
93
|
+
return !value.nil?
|
94
|
+
else
|
95
|
+
raise AttributorException.new("predicate not supported: #{predicate.inspect}")
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
# TODO: kill this when we also kill Taylor's IdentityMap.current
|
101
|
+
def self.current=(resolver)
|
102
|
+
Thread.current[:_attributor_attribute_resolver] = resolver
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def self.current
|
107
|
+
if resolver = Thread.current[:_attributor_attribute_resolver]
|
108
|
+
return resolver
|
109
|
+
else
|
110
|
+
raise AttributorException, "No AttributeResolver set."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#Container of options and structure definition
|
2
|
+
module Attributor
|
3
|
+
|
4
|
+
# RULES FOR ATTRIBUTES
|
5
|
+
# The type of an attribute is:
|
6
|
+
# the specified type
|
7
|
+
# inferred from a reference type.
|
8
|
+
# it should always end up being an anonymous type, otherwise the Model class will explode
|
9
|
+
# Struct if a block is given
|
10
|
+
|
11
|
+
# The reference option for an attribute is passed if a block is given
|
12
|
+
|
13
|
+
class DSLCompiler
|
14
|
+
|
15
|
+
attr_accessor :options, :target
|
16
|
+
|
17
|
+
def initialize(target, **options)
|
18
|
+
@target = target
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse(*blocks)
|
23
|
+
blocks.push(Proc.new) if block_given?
|
24
|
+
blocks.each { |block| self.instance_eval(&block) }
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def attributes
|
29
|
+
if target.respond_to?(:attributes)
|
30
|
+
target.attributes
|
31
|
+
else
|
32
|
+
target.keys
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def attribute(name, attr_type=nil, **opts, &block)
|
37
|
+
raise AttributorException, "Attribute names must be symbols, got: #{name.inspect}" unless name.kind_of? ::Symbol
|
38
|
+
target.attributes[name] = define(name, attr_type, **opts, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def key(name, attr_type=nil, **opts, &block)
|
42
|
+
unless name.kind_of?(options.fetch(:key_type, Attributor::Object).native_type)
|
43
|
+
raise "Invalid key: #{name.inspect}, must be instance of #{options[:key_type].native_type.name}"
|
44
|
+
end
|
45
|
+
target.keys[name] = define(name, attr_type, **opts, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Creates an Attributor:Attribute with given definition.
|
49
|
+
#
|
50
|
+
# @overload define(name, type, opts, &block)
|
51
|
+
# With an explicit type.
|
52
|
+
# @param [symbol] name describe name param
|
53
|
+
# @param [Attributor::Type] type describe type param
|
54
|
+
# @param [Hash] opts describe opts param
|
55
|
+
# @param [Block] block describe block param
|
56
|
+
# @example
|
57
|
+
# attribute :email, String, example: /[:email:]/
|
58
|
+
# @overload define(name, opts, &block)
|
59
|
+
# Assume a type of Attributor::Struct
|
60
|
+
# @param [symbol] name describe name param
|
61
|
+
# @param [Hash] opts describe opts param
|
62
|
+
# @param [Block] block describe block param
|
63
|
+
# @example
|
64
|
+
# attribute :address do
|
65
|
+
# attribute :number, String
|
66
|
+
# attribute :street, String
|
67
|
+
# attribute :city, String
|
68
|
+
# attribute :state, String
|
69
|
+
# end
|
70
|
+
# @api semiprivate
|
71
|
+
def define(name, attr_type=nil, **opts, &block)
|
72
|
+
if (existing_attribute = attributes[name])
|
73
|
+
if existing_attribute.attributes
|
74
|
+
existing_attribute.type.attributes(&block)
|
75
|
+
return existing_attribute
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if (reference = self.options[:reference])
|
80
|
+
inherited_attribute = reference.attributes[name]
|
81
|
+
else
|
82
|
+
inherited_attribute = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
if attr_type.nil?
|
86
|
+
if inherited_attribute
|
87
|
+
attr_type = inherited_attribute.type
|
88
|
+
# Only inherit opts if no explicit attr_type was given.
|
89
|
+
opts = inherited_attribute.options.merge(opts)
|
90
|
+
elsif block_given?
|
91
|
+
attr_type = Attributor::Struct
|
92
|
+
else
|
93
|
+
raise AttributorException, "type for attribute with name: #{name} could not be determined"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if block_given? && inherited_attribute
|
98
|
+
opts[:reference] = inherited_attribute.type
|
99
|
+
end
|
100
|
+
|
101
|
+
Attributor::Attribute.new(attr_type, opts, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Attributor
|
2
|
+
class AttributorException < ::StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class LoadError < AttributorException
|
6
|
+
end
|
7
|
+
|
8
|
+
class IncompatibleTypeError < LoadError
|
9
|
+
|
10
|
+
def initialize(type:, value_type: , context: )
|
11
|
+
super "Type #{type} cannot load values of type #{value_type} while loading #{Attributor.humanize_context(context)}."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class CoercionError < LoadError
|
16
|
+
def initialize( context: , from: , to:, value: nil)
|
17
|
+
msg = "Error coercing from #{from} to #{to} while loading #{Attributor.humanize_context(context)}."
|
18
|
+
msg += " Received value #{Attributor.errorize_value(value)}" if value
|
19
|
+
super msg
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class DeserializationError < LoadError
|
24
|
+
def initialize( context: , from:, encoding: , value: nil)
|
25
|
+
msg = "Error deserializing a #{from} using #{encoding} while loading #{Attributor.humanize_context(context)}."
|
26
|
+
msg += " Received value #{Attributor.errorize_value(value)}" if value
|
27
|
+
super msg
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class DumpError < AttributorException
|
32
|
+
def initialize( context: , name: , type: , original_exception: )
|
33
|
+
msg = "Error while dumping attribute #{name} of type #{type} for context #{Attributor.humanize_context(context)} ."
|
34
|
+
msg << " Reason: #{original_exception}"
|
35
|
+
super msg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# Will need eventually, but not right now:
|
2
|
+
# Hash
|
3
|
+
# Array
|
4
|
+
# CSV
|
5
|
+
# Ids
|
6
|
+
|
7
|
+
|
8
|
+
module Attributor
|
9
|
+
|
10
|
+
# It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
|
11
|
+
# TODO: should this be a mixin since it is an abstract class?
|
12
|
+
module Type
|
13
|
+
|
14
|
+
def self.included( klass )
|
15
|
+
klass.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# Generic decoding and coercion of the attribute.
|
22
|
+
def load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
23
|
+
return nil if value.nil?
|
24
|
+
unless value.is_a?(self.native_type)
|
25
|
+
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
26
|
+
end
|
27
|
+
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generic encoding of the attribute
|
32
|
+
def dump(value,**opts)
|
33
|
+
value
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: refactor this to take just the options instead of the full attribute?
|
37
|
+
# TODO: delegate to subclass
|
38
|
+
def validate(value,context=Attributor::DEFAULT_ROOT_CONTEXT,attribute)
|
39
|
+
errors = []
|
40
|
+
attribute.options.each do |option, opt_definition|
|
41
|
+
case option
|
42
|
+
when :max
|
43
|
+
errors << "#{Attributor.humanize_context(context)} value (#{value}) is larger than the allowed max (#{opt_definition.inspect})" unless value <= opt_definition
|
44
|
+
when :min
|
45
|
+
errors << "#{Attributor.humanize_context(context)} value (#{value}) is smaller than the allowed min (#{opt_definition.inspect})" unless value >= opt_definition
|
46
|
+
when :regexp
|
47
|
+
errors << "#{Attributor.humanize_context(context)} value (#{value}) does not match regexp (#{opt_definition.inspect})" unless value =~ opt_definition
|
48
|
+
end
|
49
|
+
end
|
50
|
+
errors
|
51
|
+
end
|
52
|
+
|
53
|
+
# Default, overridable valid_type? function
|
54
|
+
def valid_type?(value)
|
55
|
+
return value.is_a?(native_type) if respond_to?(:native_type)
|
56
|
+
|
57
|
+
raise AttributorException.new("#{self} must implement #valid_type? or #native_type")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Default, overridable example function
|
61
|
+
def example(context=nil, options:{})
|
62
|
+
raise AttributorException.new("#{self} must implement #example")
|
63
|
+
# return options[:example] if options.has_key? :example
|
64
|
+
# return options[:default] if options.has_key? :default
|
65
|
+
# if options.has_key? :values
|
66
|
+
# vals = options[:values]
|
67
|
+
# return vals[rand(vals.size)]
|
68
|
+
# end
|
69
|
+
# return nil
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# HELPER FUNCTIONS
|
74
|
+
|
75
|
+
|
76
|
+
def check_option!(name, definition)
|
77
|
+
case name
|
78
|
+
when :min
|
79
|
+
raise AttributorException.new("Value for option :min does not implement '<='. Got: (#{definition.inspect})") unless definition.respond_to?(:<=)
|
80
|
+
when :max
|
81
|
+
raise AttributorException.new("Value for option :max does not implement '>='. Got(#{definition.inspect})") unless definition.respond_to?(:>=)
|
82
|
+
when :regexp
|
83
|
+
# could go for a respoind_to? :=~ here, but that seems overly... cute... and not useful.
|
84
|
+
raise AttributorException.new("Value for option :regexp is not a Regexp object. Got (#{definition.inspect})") unless definition.is_a? ::Regexp
|
85
|
+
else
|
86
|
+
return :unknown
|
87
|
+
end
|
88
|
+
|
89
|
+
return :ok
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def generate_subcontext(context, subname)
|
94
|
+
context + [subname]
|
95
|
+
end
|
96
|
+
|
97
|
+
def dsl_compiler
|
98
|
+
DSLCompiler
|
99
|
+
end
|
100
|
+
|
101
|
+
# By default, non complex types will not have a DSL subdefinition this handles such case
|
102
|
+
def compile_dsl( options, block )
|
103
|
+
raise AttributorException.new("Basic structures cannot take extra block definitions") if block
|
104
|
+
# Simply create a DSL compiler to store the options, and not to parse any DSL
|
105
|
+
sub_definition=dsl_compiler.new( options )
|
106
|
+
return sub_definition
|
107
|
+
end
|
108
|
+
|
109
|
+
# Default describe for simple types...only their name (stripping the base attributor module)
|
110
|
+
def describe(root=false)
|
111
|
+
type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
|
112
|
+
{ :name => type_name.gsub( Attributor::MODULE_PREFIX_REGEX, '' ) }
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Represents a plain old boolean type. TBD: can be nil?
|
2
|
+
#
|
3
|
+
require_relative '../exceptions'
|
4
|
+
|
5
|
+
module Attributor
|
6
|
+
|
7
|
+
class Boolean
|
8
|
+
include Type
|
9
|
+
|
10
|
+
def self.valid_type?(value)
|
11
|
+
value == true || value == false
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.example(context=nil, options: {})
|
15
|
+
[true, false].sample
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
19
|
+
raise CoercionError, context: context, from: value.class, to: self, value: value if value.is_a?(::Float)
|
20
|
+
return false if [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].include?(value)
|
21
|
+
return true if [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].include?(value)
|
22
|
+
raise CoercionError, context: context, from: value.class, to: self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# Represents an unordered collection of attributes
|
2
|
+
#
|
3
|
+
|
4
|
+
module Attributor
|
5
|
+
|
6
|
+
class Collection
|
7
|
+
include Container
|
8
|
+
|
9
|
+
# @param type [Attributor::Type] optional, defines the type of all collection members
|
10
|
+
# @return anonymous class with specified type of collection members
|
11
|
+
#
|
12
|
+
# @example Collection.of(Integer)
|
13
|
+
#
|
14
|
+
def self.of(type)
|
15
|
+
resolved_type = Attributor.resolve_type(type)
|
16
|
+
unless resolved_type.ancestors.include?(Attributor::Type)
|
17
|
+
raise Attributor::AttributorException.new("Collections can only have members that are Attributor::Types")
|
18
|
+
end
|
19
|
+
Class.new(self) do
|
20
|
+
@member_type = resolved_type
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.native_type
|
25
|
+
return ::Array
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.member_type
|
29
|
+
@member_type ||= Attributor::Object
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.member_attribute
|
33
|
+
@member_attribute ||= begin
|
34
|
+
self.construct(nil,{})
|
35
|
+
@member_attribute
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# generates an example Collection
|
41
|
+
# @return An Array of native type objects conforming to the specified member_type
|
42
|
+
def self.example(context=nil, options: {})
|
43
|
+
result = []
|
44
|
+
size = rand(3) + 1
|
45
|
+
context ||= ["Collection-#{result.object_id}"]
|
46
|
+
|
47
|
+
size.times do |i|
|
48
|
+
subcontext = context + ["at(#{i})"]
|
49
|
+
result << self.member_attribute.example(subcontext)
|
50
|
+
end
|
51
|
+
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# The incoming value should be an array here, so the only decoding that we need to do
|
57
|
+
# is from the members (if there's an :member_type defined option).
|
58
|
+
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
59
|
+
if value.nil?
|
60
|
+
return nil
|
61
|
+
elsif value.is_a?(Enumerable)
|
62
|
+
loaded_value = value
|
63
|
+
elsif value.is_a?(::String)
|
64
|
+
loaded_value = decode_string(value,context)
|
65
|
+
else
|
66
|
+
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
67
|
+
end
|
68
|
+
|
69
|
+
return loaded_value.collect { |member| self.member_attribute.load(member,context) }
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def self.decode_string(value,context)
|
74
|
+
decode_json(value,context)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def self.dump(values, **opts)
|
79
|
+
return nil if values.nil?
|
80
|
+
values.collect { |value| member_attribute.dump(value,opts) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.describe(shallow=false)
|
84
|
+
#puts "Collection: #{self.type}"
|
85
|
+
hash = super(shallow)
|
86
|
+
hash[:options] = {} unless hash[:options]
|
87
|
+
hash[:options][:member_attribute] = self.member_attribute.describe
|
88
|
+
hash
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.construct(constructor_block, options)
|
92
|
+
|
93
|
+
member_options = (options[:member_options] || {} ).clone
|
94
|
+
if options.has_key?(:reference) && !member_options.has_key?(:reference)
|
95
|
+
member_options[:reference] = options[:reference]
|
96
|
+
end
|
97
|
+
|
98
|
+
# create the member_attribute, passing in our member_type and whatever constructor_block is.
|
99
|
+
# that in turn will call construct on the type if applicable.
|
100
|
+
@member_attribute = Attributor::Attribute.new self.member_type, member_options, &constructor_block
|
101
|
+
|
102
|
+
# overwrite our type with whatever type comes out of the attribute
|
103
|
+
@member_type = @member_attribute.type
|
104
|
+
|
105
|
+
return self
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def self.check_option!(name, definition)
|
110
|
+
# TODO: support more options like :max_size
|
111
|
+
case name
|
112
|
+
when :reference
|
113
|
+
when :member_options
|
114
|
+
else
|
115
|
+
return :unknown
|
116
|
+
end
|
117
|
+
|
118
|
+
:ok
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param values [Array] Array of values to validate
|
122
|
+
def self.validate(values, context=Attributor::DEFAULT_ROOT_CONTEXT, attribute=nil)
|
123
|
+
values.each_with_index.collect do |value, i|
|
124
|
+
subcontext = context + ["at(#{i})"]
|
125
|
+
self.member_attribute.validate(value, subcontext)
|
126
|
+
end.flatten.compact
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.validate_options( value, context, attribute )
|
130
|
+
errors = []
|
131
|
+
errors
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|