attributor 2.6.1 → 3.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 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