attributes_dsl 0.0.2 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a0a8b2957544474ff4116fd2b6d1703de977ba74
4
- data.tar.gz: b61d244f9ea674fdff84404e269dfa083fca6f64
3
+ metadata.gz: 7c399823c440258ebcd951ce049e16877934b630
4
+ data.tar.gz: 345a83666686089656e22eb5a38f88997baccd41
5
5
  SHA512:
6
- metadata.gz: de35390efe7b4feb77a049d040884615e11c5de61e9d8cb3bb595fbf2b71a0b0e3d53b5dd78425c6e775774366bb39cd538b5b6fa3e1af6c67e61ec03863669d
7
- data.tar.gz: 213cac8c92f35d396ce1272b53baace3a03ee635521e573cf039c85fd72bf4f43f384f46ae7d958188f9ca1d6c3424f82bc992e82c94b7fecd5221c6f10fbff4
6
+ metadata.gz: a25c08685b444464ad501a9a0a5a9d945ea854bef5959241b53918470a7dbda28d0c173aec1c6a57709ad637c490ec8864bafad9f2c1cc5a6d622d56450a667f
7
+ data.tar.gz: 36a64b86b318bbad9cfbceddefdf9fb49616f2443f259a2024642461fdb03d6eb143216913e24cfd402938c37ef38c6ea8cce2da8b1f42eec67022931cb40aac
data/.travis.yml CHANGED
@@ -4,14 +4,10 @@ bundler_args: --without metrics --without benchmarks
4
4
  cache: bundler
5
5
  script: bundle exec rake test:coverage:run
6
6
  rvm:
7
- - '1.9.3'
8
- - '2.0'
9
7
  - '2.1'
10
8
  - '2.2'
11
9
  - ruby-head
12
- - rbx-2 --1.9
13
10
  - rbx-2 --2.1
14
- - jruby-1.7-19mode
15
11
  - jruby-9.0.0.0
16
12
  - jruby-head
17
13
  matrix:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## version 0.1.0 2015-12-08
2
+
3
+ Stabilized DSL
4
+
5
+ ### Added
6
+
7
+ * Option `:reader` that allows skipping attribute reader (nepalez)
8
+ * Option `:only` whitelists allowed values (nepalez)
9
+ * Option `:except` blacklists allowed values (nepalez)
10
+ * Option `:coercer` is an alternative to block syntax (nepalez)
11
+ * An instance can be initialized by hash with both symbolic and string keys (nepalez)
12
+
13
+ ### Deleted
14
+
15
+ * Support for rubies < 2.1
16
+
17
+ ### Internal
18
+
19
+ * Removed freezing of values (nepalez)
20
+ * Slightly (20-25%) improved efficiency due to usage of transpfoc (nepalez)
21
+
22
+ [Compare v0.0.2...v0.1.0](https://github.com/nepalez/attributes_dsl/compare/v0.0.2...v0.1.0)
23
+
1
24
  ## version 0.0.2 2015-09-11
2
25
 
3
26
  This version is a result of applying v0.0.1 to the existing gems:
data/README.md CHANGED
@@ -21,40 +21,21 @@ require "attributes_dsl"
21
21
  class User
22
22
  extend AttributesDSL
23
23
 
24
- # `name` is required should be symbolized
25
- attribute :name, required: true do |value|
26
- value.to_s.to_sym
27
- end
28
-
29
- # `sex` is optional and set to `:male` by default.
30
- # It can be set to either :male or :female
31
- attribute :sex, default: :male do |value|
32
- (value == :male )? :male : :female
33
- end
34
-
35
- # `age` is optional and set to `nil` by default.
36
- # Then it is converted to integer
37
- attribute :age, &:to_i
38
-
39
- # `position` is optional and set to `nil` by default
40
- attribute :position
41
-
42
- # All other attributes are ignored
24
+ attribute :name, required: true, coerce: -> v { v.to_s }
25
+ attribute :sex, default: :male, only: /male|female/
26
+ attribute :age, only: 18..25
27
+ attribute :city, reader: false, except: %w(Moscow)
43
28
  end
44
29
 
45
- user = User.new(name: "Jane", sex: :women, age: "26", place: "Moscow")
30
+ user = User.new(name: :Jane, sex: :female, age: 24, city: "Kiev")
46
31
  user.attributes
47
- # => { name: :Jane, sex: :female, age: 26, position: nil }
32
+ # => { name: :Jane, sex: :female, age: 26, city: "Kiev" }
48
33
 
49
34
  # Aliases for attributes[:some_attribute]
50
- user.name # => :Jane
51
- user.sex # => :female
52
- user.age # => 26
53
- user.position # => nil
54
-
55
- # Required attributes should be assigned:
56
- user = User.new(sex: :women, age: "26", place: "Moscow")
57
- # => #<ArgumentError "Undefined attributes: name">
35
+ user.name # => "Jane"
36
+ user.sex # => :female
37
+ user.age # => 26
38
+ user.city # => #<NoMethodError ...>
58
39
  ```
59
40
 
60
41
  Additional Details
@@ -62,10 +43,14 @@ Additional Details
62
43
 
63
44
  ### Attribute declaration
64
45
 
65
- The `attribute` class method takes the `name` and 2 options:
46
+ The `attribute` class method takes the `name` and 3 options:
66
47
 
67
- - `:default` for the default value (otherwise `nil`);
48
+ - `:default` for the default value (otherwise `nil`).
68
49
  - `:required` to declare the attribute as required. It will be ignored if a default value is provided!
50
+ - `:reader` defines whether the attribute reader should be defined (`true` by default).
51
+ - `:only` defines allowed values. You can use procs (`-> v { v.to_s }`), ranges (`18..25`), regexps (`/male|female/`), constants (`String`), or arrays (`[:male, :female]`) to define a restriction.
52
+ - `:except` defines forbidden values.
53
+ - `:coercer` defines a procedure to convert assigned value.
69
54
 
70
55
  It is also takes the block, used to coerce a value. The coercer is applied to the default value too.
71
56
 
@@ -75,10 +60,9 @@ Instance methods (like `#name`) are just aliases for the corresponding value of
75
60
 
76
61
  ```ruby
77
62
  user = User.new(name: "John")
78
- user.attributes # => { name: :John, sex: :male, age: 0, position: nil }
79
- user.name # => :John
63
+ user.attributes # => { name: :John, sex: :male, age: nil, city: nil }
80
64
 
81
- # but
65
+ user.name # => :John
82
66
  user.instance_variable_get :@name # => nil
83
67
  ```
84
68
 
@@ -93,7 +77,8 @@ end
93
77
 
94
78
  user = UserWithRole.new(name: "Sam")
95
79
  user.attributes
96
- # => { name: :Sam, sex: :male, age: 0, position: nil, role: :user }
80
+ user.attributes
81
+ # => { name: :John, sex: :male, age: nil, city: nil, role: :user }
97
82
  ```
98
83
 
99
84
  ### Undefining Attributes
@@ -160,29 +145,21 @@ The results are following:
160
145
 
161
146
  ```
162
147
  -------------------------------------------------
163
- anima 211.638k (± 3.7%) i/s - 1.071M
164
- kwattr 187.276k (± 3.6%) i/s - 947.484k
165
- fast_attributes 160.916k (± 2.4%) i/s - 816.726k
166
- attributes_dsl 71.850k (± 3.0%) i/s - 365.365k
167
- active_attr 71.489k (± 3.6%) i/s - 357.995k
168
- virtus 45.554k (± 7.1%) i/s - 229.338k
169
-
170
- Comparison:
171
- anima: 211637.9 i/s
172
- kwattr: 187276.2 i/s - 1.13x slower
173
- fast_attributes: 160916.1 i/s - 1.32x slower
174
- attributes_dsl: 71850.0 i/s - 2.95x slower
175
- active_attr: 71489.1 i/s - 2.96x slower
176
- virtus: 45553.8 i/s - 4.65x slower
148
+ kwattr: 183416.9 i/s
149
+ anima: 169647.3 i/s - 1.08x slower
150
+ fast_attributes: 156036.2 i/s - 1.18x slower
151
+ attributes_dsl: 74495.9 i/s - 2.46x slower
152
+ active_attr: 74469.4 i/s - 2.46x slower
153
+ virtus: 46587.0 i/s - 3.94x slower
177
154
  ```
178
155
 
179
156
  Results above are pretty reasonable.
180
157
 
181
158
  The gem is faster than `virtus` that has many additional features.
182
159
 
183
- It is as fast as `active_attrs` (but has more customizable coercers).
160
+ It is as fast as `active_attrs` (but has more options).
184
161
 
185
- It is 2 times slower than `fast_attributes` that has no coercer and default values. And it is 3 times slower than `anima` and `kwattr` that provides only the base settings.
162
+ It is 2 times slower than `fast_attributes` that has no coercer and default values. And it is 2.5 times slower than `anima` and `kwattr` that provide only simple attribute's declaration.
186
163
 
187
164
  Installation
188
165
  ------------
@@ -209,12 +186,10 @@ gem install attributes_dsl
209
186
  Compatibility
210
187
  -------------
211
188
 
212
- Tested under rubies [compatible to MRI 1.9+][versions].
189
+ Tested under rubies [compatible to MRI 2.1+][versions].
213
190
 
214
191
  Uses [RSpec][rspec] 3.0+ for testing and [hexx-suit][hexx-suit] for dev/test tools collection.
215
192
 
216
- 100% [mutant]-proof covered.
217
-
218
193
  Contributing
219
194
  ------------
220
195
 
@@ -222,7 +197,6 @@ Contributing
222
197
  * [Fork the project](https://github.com/nepalez/attributes_dsl)
223
198
  * Create your feature branch (`git checkout -b my-new-feature`)
224
199
  * Add tests for it
225
- * Run `rake mutant` or `rake exhort` to ensure 100% [mutant][mutant] coverage
226
200
  * Commit your changes (`git commit -am '[UPDATE] Add some feature'`)
227
201
  * Push to the branch (`git push origin my-new-feature`)
228
202
  * Create a new Pull Request
@@ -2,7 +2,6 @@ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
2
  require "attributes_dsl/version"
3
3
 
4
4
  Gem::Specification.new do |gem|
5
-
6
5
  gem.name = "attributes_dsl"
7
6
  gem.version = AttributesDSL::VERSION.dup
8
7
  gem.author = "Andrew Kozin"
@@ -16,11 +15,11 @@ Gem::Specification.new do |gem|
16
15
  gem.extra_rdoc_files = Dir["README.md", "LICENSE"]
17
16
  gem.require_paths = ["lib"]
18
17
 
19
- gem.required_ruby_version = ">= 1.9.3"
20
-
21
- gem.add_runtime_dependency "ice_nine", "~> 0.11"
22
- gem.add_runtime_dependency "equalizer", "~> 0.0", ">= 0.0.11"
18
+ gem.required_ruby_version = ">= 2.1"
23
19
 
24
- gem.add_development_dependency "hexx-rspec", "~> 0.5", ">= 0.5.2"
20
+ gem.add_runtime_dependency "equalizer", "~> 0.0.11"
21
+ gem.add_runtime_dependency "transproc", "~> 0.4.0"
25
22
 
26
- end # Gem::Specification
23
+ gem.add_development_dependency "hexx-rspec", "~> 0.5.2"
24
+ gem.add_development_dependency "ice_nine", "~> 0.11.1"
25
+ end
data/benchmark/run.rb CHANGED
@@ -6,10 +6,10 @@ module AttributesDSLExample
6
6
  class User
7
7
  extend AttributesDSL
8
8
 
9
- attribute :foo, required: true, &:to_s
10
- attribute :bar, default: :BAR, &:to_s
11
- attribute :baz, default: :BAZ, &:to_s
12
- attribute :qux, &:to_s
9
+ attribute :foo, required: true
10
+ attribute :bar, default: :BAR
11
+ attribute :baz, default: :BAZ
12
+ attribute :qux
13
13
  end
14
14
 
15
15
  def self.call
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "equalizer"
4
- require "ice_nine"
4
+ require "transproc"
5
5
 
6
6
  # Simple DSL for PORO attributes
7
7
  #
@@ -11,6 +11,7 @@ require "ice_nine"
11
11
  #
12
12
  module AttributesDSL
13
13
 
14
+ require_relative "attributes_dsl/transprocs"
14
15
  require_relative "attributes_dsl/attribute"
15
16
  require_relative "attributes_dsl/attributes"
16
17
 
@@ -31,35 +32,26 @@ module AttributesDSL
31
32
  # extend AttributeDSL
32
33
  #
33
34
  # attribute :foo, required: true do |value|
34
- # value.to_i % 5
35
+ # value.to_i % 5 # value coercer
35
36
  # end
36
37
  #
37
- # attribute :bar, default: :BAR
38
+ # attribute :bar, default: :BAR, reader: false
38
39
  # end
39
40
  #
40
41
  # @param [#to_sym] name The unique name of the attribute
41
- # @param [Proc] coercer The proc to coerce values (including the default ones)
42
42
  # @param [Hash] options
43
- #
44
- # @option options [Boolean] :required
45
- # Whether the attribute should be required by the +initializer+
46
- # This option is ignored (set to +false+) when default value is provided
47
- # @option options [Object] :default
48
- # The default value for the attribute
43
+ # @param [Proc] coercer The proc to coerce values (including the default ones)
49
44
  #
50
45
  # @return [undefined]
51
46
  #
52
47
  def attribute(name, options = {}, &coercer)
53
- s_name = name.to_sym
54
- @attributes = attributes.register(s_name, options, &coercer)
55
-
56
- define_method(s_name) { attributes.fetch(s_name) }
48
+ @attributes = attributes.add(name, options, &coercer)
49
+ define_method(name) { attributes.fetch(name) } if attributes.reader? name
57
50
  end
58
51
 
59
52
  # @private
60
53
  def self.extended(klass)
61
- # use __send__ for compatibility to 1.9.3 (where `.include` was private)
62
- klass.__send__(:include, InstanceMethods)
54
+ klass.instance_eval { include InstanceMethods }
63
55
  end
64
56
 
65
57
  # @private
@@ -69,7 +61,6 @@ module AttributesDSL
69
61
 
70
62
  # Defines instance methods for the hash of attributes and its initializer
71
63
  module InstanceMethods
72
-
73
64
  # @!attribute [r] attributes
74
65
  #
75
66
  # @return [Hash] the hash of initialized attributes
@@ -80,12 +71,8 @@ module AttributesDSL
80
71
  #
81
72
  # @param [Hash] attributes
82
73
  #
83
- # @raise [ArgumentError] in case a required attribute is missed
84
- #
85
74
  def initialize(attributes = {})
86
- @attributes = self.class.attributes.extract(attributes)
75
+ @attributes = self.class.attributes.transformer[attributes]
87
76
  end
88
-
89
- end # module InstanceMethods
90
-
91
- end # module AttributesDSL
77
+ end
78
+ end
@@ -9,7 +9,6 @@ module AttributesDSL
9
9
  # @author Andrew Kozin <Andrew.Kozin@gmail.com>
10
10
  #
11
11
  class Attribute
12
-
13
12
  include Equalizer.new(:name)
14
13
 
15
14
  # @!attribute [r] name
@@ -18,52 +17,68 @@ module AttributesDSL
18
17
  #
19
18
  attr_reader :name
20
19
 
21
- # @!attribute [r] default
22
- #
23
- # @return [Object] the default value of the attribute
24
- #
25
- attr_reader :default
26
-
27
- # @!attribute [r] required
28
- #
29
- # @return [Boolean] whether the attribute is required
30
- #
31
- attr_reader :required
32
-
33
- # @!attribute [r] coercer
20
+ # @!attribute [r] reader
34
21
  #
35
- # @return [Proc, nil] the coercer for the attribute
22
+ # @return [Boolean] whether an attribute should be readable
36
23
  #
37
- attr_reader :coercer
24
+ attr_reader :reader
38
25
 
39
26
  # Initializes the attribute
40
27
  #
41
- # @param [Symbol] name
28
+ # @param [#to_sym] name
42
29
  # @param [Hash] options
43
30
  # @param [Proc] coercer
44
31
  #
45
- # @option options [Object] :default
46
- # @option options [Boolean] :required
47
- #
48
32
  def initialize(name, options = {}, &coercer)
49
- @name = name
50
- @default = options.fetch(:default) {}
51
- @required = default.nil? && options.fetch(:required) { false }
52
- @coercer = coercer
53
-
54
- IceNine.deep_freeze(self)
33
+ @name = name.to_sym
34
+ @options = { coercer: coercer }.merge(options)
35
+ @reader = @options.fetch(:reader) { true }
55
36
  end
56
37
 
57
- # Coerces an input assigned to the attribute
38
+ # A proc that transform a hash of attributes using current settings
58
39
  #
59
- # @param [Object] input
40
+ # @return [Proc]
60
41
  #
61
- # @return [Object]
62
- #
63
- def value(input)
64
- coercer ? coercer[input] : input
42
+ def transformer
43
+ convert unless @options.empty?
65
44
  end
66
45
 
67
- end # class Attribute
46
+ private
47
+
48
+ def convert
49
+ @convert ||= Transprocs[:convert, name, presence, absence]
50
+ end
51
+
52
+ def presence
53
+ [whitelist, blacklist, coercer].compact.reduce(:>>) || identity
54
+ end
68
55
 
69
- end # module AttributesDSL
56
+ def absence
57
+ [missed, default].compact.reduce(:>>) || identity
58
+ end
59
+
60
+ def identity
61
+ Transprocs[:identity]
62
+ end
63
+
64
+ def missed
65
+ Transprocs[:missed, name] if @options[:required]
66
+ end
67
+
68
+ def default
69
+ Transprocs[:default, name, @options[:default]] if @options[:default]
70
+ end
71
+
72
+ def whitelist
73
+ Transprocs[:whitelist, name, @options[:only]] if @options[:only]
74
+ end
75
+
76
+ def blacklist
77
+ Transprocs[:blacklist, name, @options[:except]] if @options[:except]
78
+ end
79
+
80
+ def coercer
81
+ Transprocs[:coerce, name, @options[:coercer]] if @options[:coercer]
82
+ end
83
+ end
84
+ end
@@ -10,22 +10,14 @@ module AttributesDSL
10
10
  # @author Andrew Kozin <Andrew.Kozin@gmail.com>
11
11
  #
12
12
  class Attributes
13
-
14
13
  # @!attribute [r] attributes
15
14
  #
16
15
  # Uses the set of attributes to ensure their uniqueness (by name)
17
16
  #
18
17
  # @return [Set] the set of registered attributes
19
18
  #
20
- attr_reader :attributes
21
-
22
- # Initializes an immutable collection with an initial set of attributes
23
- #
24
- # @param [Hash] attributes
25
- #
26
- def initialize(attributes = {})
27
- @attributes = attributes
28
- IceNine.deep_freeze(self)
19
+ def attributes
20
+ @attributes ||= {}
29
21
  end
30
22
 
31
23
  # Initializes the attribute from given arguments
@@ -35,41 +27,49 @@ module AttributesDSL
35
27
  #
36
28
  # @return [AttributesDSL::Attributes]
37
29
  #
38
- def register(name, options = {}, &coercer)
39
- self.class.new(
40
- attributes.merge(name => Attribute.new(name, options, &coercer))
41
- )
30
+ def add(name, options = {}, &coercer)
31
+ name = name.to_sym
32
+ value = Attribute.new(name, options, &coercer)
33
+ clone_with do
34
+ @attributes = attributes.merge(name => value)
35
+ @transformer = nil
36
+ end
42
37
  end
43
38
 
44
- # Extracts instance attributes from the input hash
39
+ # Returns the proc that converts a hash of attributes using current setting
40
+ #
41
+ # @return [Proc]
45
42
  #
46
- # Assigns default values and uses coercions when applicable.
43
+ def transformer
44
+ @transformer ||= transprocs.flatten.compact.reduce(:>>)
45
+ end
46
+
47
+ # Checks whether an attribute reader should be defined
47
48
  #
48
- # @param [Hash] input
49
+ # @param [#to_sym] name
49
50
  #
50
- # @return [Hash]
51
+ # @return [Boolean]
51
52
  #
52
- def extract(input)
53
- validate(input).inject({}) do |a, e|
54
- key = e.name
55
- value = input.fetch(key) { e.default }
56
- a.merge(key => e.value(value))
57
- end
53
+ def reader?(name)
54
+ attributes[name.to_sym].reader
58
55
  end
59
56
 
60
57
  private
61
58
 
62
- def validate(input)
63
- undefined = required - input.keys
64
- return attributes.values if undefined.empty?
65
-
66
- fail ArgumentError.new "Undefined attributes: #{undefined.join(", ")}"
59
+ def transprocs
60
+ [
61
+ Transprocs[:filter, keys],
62
+ attributes.values.map(&:transformer),
63
+ Transprocs[:update, keys]
64
+ ]
67
65
  end
68
66
 
69
- def required
70
- attributes.values.select(&:required).map(&:name)
67
+ def keys
68
+ attributes.keys
71
69
  end
72
70
 
73
- end # class Attributes
74
-
75
- end # module AttributesDSL
71
+ def clone_with(&block)
72
+ dup.tap { |instance| instance.instance_eval(&block) }
73
+ end
74
+ end
75
+ end