attributor 2.6.1 → 3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab74d0c797f7d246ba2e19564b9a215fcb1e8e73
4
- data.tar.gz: 780a566c96c190af27f559aaf9052ccddcc5d237
3
+ metadata.gz: a9b60ce0b5ac69797dca8bc43ce9164e8a666eeb
4
+ data.tar.gz: d87a417b41105b3ed4ae10098bd14542d88d7f96
5
5
  SHA512:
6
- metadata.gz: df140c8719de7cbc33cf48c2b92d3b5afb3fc15119a84bacc1e414757e2a54288e72fc46ddce64485ac87ed927cca93ce7a387398064d5dfac235af4eccfae94
7
- data.tar.gz: 5128f01b9a28a1b7e2e112d23bd82037600af17b127f6f4c9ad8911d00d04ffe829a10c5bc0288dbcd9517d379052b0680db0b4892cbf7ec4c0976fbf48335f3
6
+ metadata.gz: 660346014295ff874c70f0cc79931b98c109c0034e1b235f516108aa5857fa71688a2a2fcd7aab8c9636c91b87a64d1b82d09ae1a7d247b0bf7a23cf232a80d2
7
+ data.tar.gz: 174c4d2859d2ad0611c55b3c16c70eaa80440fcdee5308fe84a917192d624383e65bfae19c5bd593732ac59ba0d13d908e4c018406f0256fe0f1c30864911fc1
data/.travis.yml CHANGED
@@ -1,7 +1,8 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - "2.1.5"
4
+ - 2.1.5
5
+ - 2.2.2
5
6
  script: bundle exec rspec spec
6
7
  branches:
7
8
  only:
data/CHANGELOG.md CHANGED
@@ -1,11 +1,26 @@
1
- Attributor Changelog
2
- ============================
3
-
4
- next
5
- ----
6
-
7
- 2.6.1
8
- -----
1
+ # Attributor Changelog
2
+
3
+ ## 3.0.0
4
+
5
+ * Small enhancements on `describe` for types
6
+ * avoid creating empty `:attributes` key for `Model`
7
+ * ensure embedding `key_type` in `Hash` using `shallow` mode
8
+ * Added `Hash#delete`.
9
+ * Changed the schema for describing `Hash` to use `attributes` instead of `keys`
10
+ * It makes more sense, and it is compatible with Model and Structs too.
11
+ * Undefine JRuby package helper methods in `Model` (org, java...)
12
+ * Added support to `Collection.load` for any value that responds to `to_a`
13
+ * Fixed `Collection.validate` to complain when value object is not a valida type
14
+ * Fixed bug where defining an attribute that references a `Collection` would not properly support defining sub-attributes in a provided block.
15
+ * Enhanced the type/attribute `describe` methods of types so that they generate an example if an `example` argument is passed in.
16
+ * Complex (sub-structured) types will not output examples, only 'leaf' ones.
17
+ * Improved handling of exceptions during attribute definitions for `Hash`/`Model` that would previously leave the set of attributes in an undefined state. Now, any attempts to use the type will throw an `InvalidDefinition` exception and include the original exception. (#127)
18
+ * Removed `undef :empty?` from `Model`
19
+ * Made `Collection` a subclass of Array, and `load` create new instances of it.
20
+ * Built in proper loading and validation of any `Attribute#example` when the `:example` option is used.
21
+
22
+
23
+ ## 2.6.1
9
24
 
10
25
  * Add the `:custom_data` option for attributes. This is a hash that is passed through to `describe` - Attributor does no processing or handling of this option.
11
26
  * Added `Type.family` which returns a more-generic "family name". It's defined for all built-in types, and is included in `Type.describe`.
@@ -14,8 +29,7 @@ next
14
29
  * Fix common hash methods created for example instances (to play well with lazy attributes)
15
30
  * Avoid storing the `Hash#insensitive_map` unless insensitivity enabled
16
31
 
17
- 2.6.0
18
- -----
32
+ ## 2.6.0
19
33
 
20
34
  * Fixed bug in `example_mixin` where lazy_attributes were not evaluated.
21
35
  * Fixed bug in `Hash` where the class would refuse to load from another `Attributor::Hash` when there were no keys defined and they were seemingly compatible.
@@ -27,8 +41,8 @@ next
27
41
  * Added `Hash#merge` that works with two identically-typed hashes
28
42
  * Added `Hash#each_pair` for better duck-type compatibility with ::Hash.
29
43
 
30
- 2.5.0
31
- ----
44
+
45
+ ## 2.5.0
32
46
 
33
47
  * Partial support for defining `:default` values through Procs.
34
48
  * Note: this is only "partially" supported the `parent` argument of the Proc will NOT contain the correct attribute parent yet. It will contain a fake class, that will loudly complain about any attempt to use any of its methods.
@@ -38,17 +52,16 @@ next
38
52
  * `Time`, `DateTime`, and `Date` now all return ISO 8601 formatted values from `.dump` (via calling `iso8601` on the value).
39
53
  * Added `Type.id`, a unique value based on the type's class name.
40
54
 
41
- 2.4.0
42
- ------
43
55
 
44
- * `Model` is now a subclass of `Hash`.
56
+ ## 2.4.0
57
+
58
+ * `Model` is now a subclass of `Hash`.
45
59
  * The interface for `Model` instances is almost entirely unchanged, except for the addition of `Hash`-like methods (i.e., you can now do `some_model[:key]` to access attributes).
46
60
  * This fixes numerous incompatabilities between models and hashes, as well as confusing differences between the behavior when loading a model vs a hash.
47
61
  * `String.load` now raises `IncompatibleTypeError` for `Enumerable` values.
48
- * Added `Symbol` type, use with caution as it will automatically call `#to_sym` on anything loaded.
62
+ * Added `Symbol` type, use with caution as it will automatically call `#to_sym` on anything loaded.
49
63
 
50
- 2.3.0
51
- ------
64
+ ## 2.3.0
52
65
 
53
66
  * Added `recurse` option to `Type.load` that is used by `Model` and `Hash` to force the loading of values (specifically, so that default values are assigned) even if the loaded value is `nil`.
54
67
  * Fix `Attributor::CSV` to dump `String` values and generate `String` examples.
@@ -59,16 +72,15 @@ next
59
72
  * Added `Hash#get`, for retrieving keys using the same logic the `case_insensitive_load` and `allow_extra` with defined `extra` key.
60
73
 
61
74
 
62
- 2.2.1
63
- ------
75
+ ## 2.2.1
64
76
 
65
77
  * Dumping attributes will now load the values if they're not in the native type.
66
78
  * `Model.valid_type?` now accepts hashes.
67
79
  * `Hash`:
68
80
  * Added `:has_key?` to delegation
69
81
 
70
- 2.2.0
71
- ------
82
+
83
+ ## 2.2.0
72
84
 
73
85
  * Fix example generation for Hash and Collection to handle a non-Array context parameter.
74
86
  * Hash:
@@ -78,8 +90,8 @@ next
78
90
  * Added `Hash#set` to encapsulate the above options and attribute loading.
79
91
  * Added `extra` command in the `keys` DSL, which lets you define a key (whose value should be a Hash), to group any unspecified keys during load.
80
92
 
81
- 2.1.0
82
- ------
93
+
94
+ ## 2.1.0
83
95
 
84
96
  * Structs now inherit type-level options from their reference.
85
97
  * Add Collection subclasses for CSVs and Ids
@@ -103,8 +115,8 @@ next
103
115
  * Introduced a new FileUpload type. This can be easily used in Web servers to map incoming multipart file uploads.
104
116
  * Introduced a new Tempfile type.
105
117
 
106
- 2.0.0
107
- ------
118
+
119
+ ## 2.0.0
108
120
 
109
121
  * Added new exception subtypes (load methods return more precise errors now)
110
122
  * Changed ```Attributor::Model``` to be a class instead of module.
data/lib/attributor.rb CHANGED
@@ -14,7 +14,7 @@ module Attributor
14
14
  require_relative 'attributor/attribute_resolver'
15
15
 
16
16
  require_relative 'attributor/example_mixin'
17
-
17
+
18
18
  require_relative 'attributor/extensions/randexp'
19
19
 
20
20
 
@@ -45,16 +45,16 @@ module Attributor
45
45
  end
46
46
 
47
47
  def self.humanize_context( context )
48
- raise "NIL CONTEXT PASSED TO HUMANZE!!" unless context
48
+ return "" unless context
49
49
 
50
50
  if context.kind_of? ::String
51
51
  context = Array(context)
52
52
  end
53
53
 
54
54
  unless context.is_a? Enumerable
55
- raise "INVALID CONTEXT!!! (got: #{context.inspect})"
55
+ raise "INVALID CONTEXT!!! (got: #{context.inspect})"
56
56
  end
57
-
57
+
58
58
  begin
59
59
  return context.join('.')
60
60
  rescue Exception => e
@@ -73,10 +73,10 @@ module Attributor
73
73
 
74
74
  require_relative 'attributor/families/numeric'
75
75
  require_relative 'attributor/families/temporal'
76
-
76
+
77
77
  require_relative 'attributor/types/container'
78
78
  require_relative 'attributor/types/object'
79
-
79
+
80
80
  require_relative 'attributor/types/bigdecimal'
81
81
  require_relative 'attributor/types/integer'
82
82
  require_relative 'attributor/types/string'
@@ -90,7 +90,7 @@ module Attributor
90
90
  require_relative 'attributor/types/hash'
91
91
  require_relative 'attributor/types/model'
92
92
  require_relative 'attributor/types/struct'
93
-
93
+
94
94
 
95
95
  require_relative 'attributor/types/csv'
96
96
  require_relative 'attributor/types/ids'
@@ -98,5 +98,6 @@ module Attributor
98
98
  # TODO: move these to 'optional types' or 'extra types'... location
99
99
  require_relative 'attributor/types/tempfile'
100
100
  require_relative 'attributor/types/file_upload'
101
+ require_relative 'attributor/types/uri'
101
102
 
102
103
  end
@@ -3,14 +3,14 @@
3
3
  module Attributor
4
4
 
5
5
  class FakeParent < ::BasicObject
6
-
6
+
7
7
  def method_missing(name, *args)
8
8
  ::Kernel.warn "Warning, you have tried to access the '#{name}' method of the 'parent' argument of a Proc-defined :default values." +
9
9
  "Those Procs should completely ignore the 'parent' attribute for the moment as it will be set to an " +
10
10
  "instance of a useless class (until the framework can provide such functionality)"
11
11
  nil
12
12
  end
13
-
13
+
14
14
  def class
15
15
  FakeParent
16
16
  end
@@ -80,7 +80,7 @@ module Attributor
80
80
  end
81
81
 
82
82
  def dump(value, **opts)
83
- type.dump(value, opts)
83
+ type.dump(value, **opts)
84
84
  end
85
85
 
86
86
 
@@ -98,7 +98,7 @@ module Attributor
98
98
 
99
99
  TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if, :custom_data ]
100
100
  INTERNAL_OPTIONS = [:dsl_compiler,:dsl_compiler_options] # Options we don't want to expose when describing attributes
101
- def describe(shallow=true)
101
+ def describe(shallow=true, example: nil )
102
102
  description = { }
103
103
  # Clone the common options
104
104
  TOP_LEVEL_OPTIONS.each do |option_name|
@@ -109,6 +109,7 @@ module Attributor
109
109
  if ( ex_def = description.delete(:example) )
110
110
  description[:example_definition] = ex_def
111
111
  end
112
+
112
113
  special_options = self.options.keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS
113
114
  description[:options] = {} unless special_options.empty?
114
115
  special_options.each do |opt_name|
@@ -119,40 +120,55 @@ module Attributor
119
120
  description[:options][:reference] = reference.name
120
121
  end
121
122
 
122
- description[:type] = self.type.describe(shallow)
123
+ description[:type] = self.type.describe(shallow, example: example )
124
+ # Move over any example from the type, into the attribute itself
125
+ if ( ex = description[:type].delete(:example) )
126
+ description[:example] = self.dump( ex ).to_s
127
+ end
128
+
123
129
  description
124
130
  end
125
131
 
126
132
 
133
+ def example_from_options(parent, context)
134
+ val = self.options[:example]
135
+ generated = case val
136
+ when ::Regexp
137
+ val.gen
138
+ when ::Array
139
+ # TODO: handle arrays of non native types, i.e. arrays of regexps.... ?
140
+ val.pick
141
+ when ::Proc
142
+ if val.arity == 2
143
+ val.call(parent, context)
144
+ elsif val.arity == 1
145
+ val.call(parent)
146
+ else
147
+ val.call
148
+ end
149
+ when nil
150
+ nil
151
+ else
152
+ val
153
+ end
154
+ self.load( generated, context )
155
+ end
156
+
127
157
  def example(context=nil, parent: nil, values:{})
128
158
  raise ArgumentError, "attribute example cannot take a context of type String" if (context.is_a? ::String )
129
159
  if context
130
160
  ctx = Attributor.humanize_context(context)
131
161
  seed, _ = Digest::SHA1.digest(ctx).unpack("QQ")
132
162
  Random.srand(seed)
163
+ else
164
+ context = Attributor::DEFAULT_ROOT_CONTEXT
133
165
  end
134
166
 
135
167
  if self.options.has_key? :example
136
- val = self.options[:example]
137
- case val
138
- when ::Regexp
139
- self.load(val.gen,context)
140
- when ::Array
141
- # TODO: handle arrays of non native types, i.e. arrays of regexps.... ?
142
- val.pick
143
- when ::Proc
144
- if val.arity == 2
145
- val.call(parent, context)
146
- elsif val.arity == 1
147
- val.call(parent)
148
- else
149
- val.call
150
- end
151
- when nil
152
- nil
153
- else
154
- self.load(val)
155
- end
168
+ loaded = example_from_options(parent, context)
169
+ errors = self.validate(loaded, context)
170
+ raise AttributorException, "Error generating example for #{Attributor.humanize_context(context)}. Errors: #{errors.inspect}" if errors.any?
171
+ loaded
156
172
  else
157
173
  if (option_values = self.options[:values])
158
174
  option_values.pick
@@ -100,7 +100,13 @@ module Attributor
100
100
  # determine attribute type to use
101
101
  if attr_type.nil?
102
102
  if block_given?
103
- attr_type = Attributor::Struct
103
+ attr_type = if inherited_attribute && inherited_attribute.type < Attributor::Collection
104
+ # override the reference to be the member_attribute's type for collections
105
+ opts[:reference] = inherited_attribute.type.member_attribute.type
106
+ Attributor::Collection.of(Struct)
107
+ else
108
+ Attributor::Struct
109
+ end
104
110
  elsif inherited_attribute
105
111
  attr_type = inherited_attribute.type
106
112
  else
@@ -10,9 +10,9 @@ module Attributor
10
10
 
11
11
 
12
12
  module ClassMethods
13
-
14
- # Does this type support the generation of subtypes?
15
- def constructable?
13
+
14
+ # Does this type support the generation of subtypes?
15
+ def constructable?
16
16
  false
17
17
  end
18
18
 
@@ -20,7 +20,7 @@ module Attributor
20
20
  def load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
21
21
  return nil if value.nil?
22
22
  unless value.is_a?(self.native_type)
23
- raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
23
+ raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
24
24
  end
25
25
 
26
26
  value
@@ -89,7 +89,7 @@ module Attributor
89
89
 
90
90
 
91
91
  def generate_subcontext(context, subname)
92
- context + [subname]
92
+ context + [subname]
93
93
  end
94
94
 
95
95
  def dsl_compiler
@@ -105,15 +105,17 @@ module Attributor
105
105
  end
106
106
 
107
107
  # Default describe for simple types...only their name (stripping the base attributor module)
108
- def describe(root=false)
108
+ def describe(root=false, example: nil)
109
109
  type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
110
- {
110
+ hash = {
111
111
  name: type_name.gsub(Attributor::MODULE_PREFIX_REGEX, ''),
112
112
  family: self.family,
113
113
  id: self.id
114
114
  }
115
+ hash[:example] = example if example
116
+ hash
115
117
  end
116
-
118
+
117
119
  def id
118
120
  return nil if self.name.nil?
119
121
  self.name.gsub('::'.freeze,'-'.freeze)
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Attributor
5
5
 
6
- class Collection
6
+ class Collection < Array
7
7
  include Container
8
8
 
9
9
  # @param type [Attributor::Type] optional, defines the type of all collection members
@@ -21,14 +21,31 @@ module Attributor
21
21
  end
22
22
  end
23
23
 
24
+ @options = {}
25
+
26
+ def self.inherited(klass)
27
+ klass.instance_eval do
28
+ @options = {}
29
+ end
30
+ end
31
+
32
+ def self.options
33
+ @options
34
+ end
35
+
36
+
24
37
  def self.native_type
25
- return ::Array
38
+ self
39
+ end
40
+
41
+ def self.valid_type?(type)
42
+ type.kind_of?(self) || type.kind_of?(::Enumerable)
26
43
  end
27
44
 
28
45
  def self.family
29
46
  'array'
30
47
  end
31
-
48
+
32
49
  def self.member_type
33
50
  @member_type ||= Attributor::Object
34
51
  end
@@ -51,16 +68,20 @@ module Attributor
51
68
  context ||= ["Collection-#{result.object_id}"]
52
69
  context = Array(context)
53
70
 
71
+ # avoid infinite recursion in example generation
72
+ example_depth = context.size
73
+ size = 0 if example_depth > Hash::MAX_EXAMPLE_DEPTH
74
+
54
75
  size.times do |i|
55
76
  subcontext = context + ["at(#{i})"]
56
77
  result << self.member_attribute.example(subcontext)
57
78
  end
58
79
 
59
- result
80
+ self.new(result)
60
81
  end
61
82
 
62
83
 
63
- # The incoming value should be an array here, so the only decoding that we need to do
84
+ # The incoming value should be array-like here, so the only decoding that we need to do
64
85
  # is from the members (if there's an :member_type defined option).
65
86
  def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
66
87
  if value.nil?
@@ -69,11 +90,13 @@ module Attributor
69
90
  loaded_value = value
70
91
  elsif value.is_a?(::String)
71
92
  loaded_value = decode_string(value,context)
93
+ elsif value.respond_to?(:to_a)
94
+ loaded_value = value.to_a
72
95
  else
73
96
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
74
97
  end
75
98
 
76
- return loaded_value.collect { |member| self.member_attribute.load(member,context) }
99
+ self.new(loaded_value.collect { |member| self.member_attribute.load(member,context) })
77
100
  end
78
101
 
79
102
 
@@ -87,20 +110,21 @@ module Attributor
87
110
  values.collect { |value| member_attribute.dump(value,opts) }
88
111
  end
89
112
 
90
- def self.describe(shallow=false)
113
+ def self.describe(shallow=false, example: nil)
91
114
  hash = super(shallow)
92
115
  hash[:options] = {} unless hash[:options]
93
- hash[:member_attribute] = self.member_attribute.describe
116
+ member_example = example.first if example
117
+ hash[:member_attribute] = self.member_attribute.describe(true, example: member_example )
94
118
  hash
95
119
  end
96
120
 
97
121
 
98
- def self.constructable?
122
+ def self.constructable?
99
123
  true
100
124
  end
101
125
 
102
- def self.construct(constructor_block, options)
103
126
 
127
+ def self.construct(constructor_block, options)
104
128
  member_options = (options[:member_options] || {} ).clone
105
129
  if options.has_key?(:reference) && !member_options.has_key?(:reference)
106
130
  member_options[:reference] = options[:reference]
@@ -131,6 +155,16 @@ module Attributor
131
155
 
132
156
  # @param values [Array] Array of values to validate
133
157
  def self.validate(values, context=Attributor::DEFAULT_ROOT_CONTEXT, attribute=nil)
158
+
159
+ unless self.valid_type?(values)
160
+ descriptive_type =if self.member_type != Object
161
+ "Collection.of(#{self.member_type})"
162
+ else
163
+ self
164
+ end
165
+ raise Attributor::IncompatibleTypeError, context: context, value_type: values.class, type: descriptive_type
166
+ end
167
+
134
168
  values.each_with_index.collect do |value, i|
135
169
  subcontext = context + ["at(#{i})"]
136
170
  self.member_attribute.validate(value, subcontext)
@@ -142,5 +176,10 @@ module Attributor
142
176
  errors
143
177
  end
144
178
 
179
+
180
+ def dump(**opts)
181
+ self.collect { |value| self.class.member_attribute.dump(value,opts) }
182
+ end
183
+
145
184
  end
146
185
  end