value_semantics 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 17f5d6eaef0f928cf467ac69b5dc9d4f0dfcc09a6991ad460e44f394c5b94d87
4
+ data.tar.gz: c85d1e46558e4599e52df186a5b45f47193cf88246c4267c76b46f5f959f9f55
5
+ SHA512:
6
+ metadata.gz: 3cd1bfb5150cbb4fef561da3d433dde03d4a4432253f697c94790ac6e30832a1901bc5989f8a48e062f55a9a80f5c9b19cdf99b20b16ac3704e872ebd4851057
7
+ data.tar.gz: bd16975818cb48f84cf07472a4723ffe6b04a74036d52f6a7e3c3fe8a5d04b4ef6270a6ea7d2a25ff137b56d3a3905392706ea91825134b7c7630a1c985fc53b
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.1
4
+ script: bundle exec rspec
5
+ deploy:
6
+ provider: rubygems
7
+ on:
8
+ tags: true
9
+ api_key:
10
+ secure: nL74QuUczEpA0qbhSBN2zjGdviWgKB3wR6vFvwervv1MZNWmwOQUYe99Oq9kPeyc8/x2MR/H6PQm5qbrk/WAfRede01WxlZ/EBUW+9CYGrxcBsGONx9IULO8A0I8/yN/YJHW2vjo3dfR66EwVsXTVWq8U63PRRcwJIyTqnIiUm2sxauMQoPRBbXG+pD9v/EJSn3ugpdtxp0lVYDn8LDKk5Ho4/wbpY4ML11XUJa9mz9CyR/GsAzdy5FTXaDMOwuWOVEx9cab7m4qPOBhmlJY4TrmooFpxTxRwChcvByjq1IboEd2M3RT5on7Q/xDTlHSOuT0OS8mnS2AocGT4a1gC+W/xOlghgEcN+xs2V5mfucR6+iUYlCy32uz1w3ey7T2X5xN4ubut09r1xLi7eu1NisAoAc+GOJ4TIxQNqkeRhY4X/fs8j7SMfOEMDr6pPxSLKZxgSvExt+IbdcZD/uQ7rTBQkadYCbc9MX5dHazBievmar3ZsFffbIf+n13FVDXsaPgRt7DlFM5dqGrEwVwt1jFRhdFuDCjkj4QWOLn7E1uY3XqgrqGvgUBlF8Znwc6qicW8zxV4SIWhqIzCOH6L9WIZGLHNq0remoCd9sq9Ter9av3jL+6UmZRRAr+JceeZfZmsYIXKomECzleM9FXMx7FXlpjJKOlf3JnrfeCTwI=
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in the gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Tom Dalling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # ValueSemantics
2
+
3
+ Create value classes quickly, with all the [conventions of a good value object](https://github.com/zverok/good-value-object).
4
+
5
+ Generates modules that provide value semantics for a given set of attributes.
6
+ Provides the behaviour of an immutable struct-like value class,
7
+ with light-weight validation and coercion.
8
+
9
+ These are intended for internal use, as opposed to validating user input like ActiveRecord.
10
+ Invalid or missing attributes cause an exception intended for developers,
11
+ not an error message intended for the user.
12
+
13
+ ## Basic Usage
14
+
15
+ ```ruby
16
+ require 'value_semantics'
17
+
18
+ class Person
19
+ include ValueSemantics.for_attributes {
20
+ name
21
+ age default: 31
22
+ }
23
+ end
24
+
25
+ tom = Person.new(name: 'Tom')
26
+
27
+
28
+ #
29
+ # Read-only attributes
30
+ #
31
+ tom.name #=> "Tom"
32
+ tom.age #=> 31
33
+
34
+
35
+ #
36
+ # Convert to Hash
37
+ #
38
+ tom.to_h #=> { :name => "Tom", :age => 31 }
39
+
40
+
41
+ #
42
+ # Non-destructive updates
43
+ #
44
+ old_tom = tom.with(age: 99)
45
+
46
+ old_tom #=> #<Person name="Tom" age=99>
47
+ tom #=> #<Person name="Tom" age=31> (unchanged)
48
+
49
+
50
+ #
51
+ # Equality
52
+ #
53
+ other_tom = Person.new(name: 'Tom', age: 31)
54
+
55
+ tom == other_tom #=> true
56
+ tom.eql?(other_tom) #=> true
57
+ tom.hash == other_tom.hash #=> true
58
+ ```
59
+
60
+ The curly bracket syntax used with `ValueSemantics.for_attributes` is, unfortunately,
61
+ mandatory due to Ruby's precedence rules.
62
+ The `do`/`end` syntax will not work unless you surround the whole thing with parenthesis.
63
+
64
+
65
+ ## Validation (Types)
66
+
67
+ Validators are objects that implement the `===` method,
68
+ which means you can use `Class` objects (like `String`) and also `Regexp` objects:
69
+
70
+ ```ruby
71
+ class Person
72
+ include ValueSemantics.for_attributes {
73
+ name String
74
+ birthday /\d\d\d\d-\d\d-\d\d/
75
+ }
76
+ end
77
+
78
+ Person.new(name: 'Tom', ...) # works
79
+ Person.new(name: 5, ...)
80
+ #=> ArgumentError:
81
+ #=> Value for attribute 'name' is not valid: 5
82
+
83
+ Person.new(birthday: "1970-01-01", ...) # works
84
+ Person.new(birthday: "hello", ...)
85
+ #=> ArgumentError:
86
+ #=> Value for attribute 'birthday' is not valid: "hello"
87
+ ```
88
+
89
+ A custom validator might look something like this:
90
+
91
+ ```ruby
92
+ module Odd
93
+ def self.===(value)
94
+ value.odd?
95
+ end
96
+ end
97
+
98
+ class Person
99
+ include ValueSemantics.for_attributes {
100
+ age Odd
101
+ }
102
+ end
103
+
104
+ Person.new(age: 9) # works
105
+ Person.new(age: 8)
106
+ #=> ArgumentError:
107
+ #=> Value for attribute 'age' is not valid: 8
108
+ ```
109
+
110
+ Default attribute values also pass through validation.
111
+
112
+
113
+ ## Coercion
114
+
115
+ Coercion blocks can convert invalid values into valid ones, where possible.
116
+
117
+ ```ruby
118
+ class Server
119
+ include ValueSemantics.for_attributes {
120
+ address IPAddr do |value|
121
+ if value.is_a?(String)
122
+ IPAddr.new(value)
123
+ else
124
+ value
125
+ end
126
+ end
127
+ }
128
+ end
129
+
130
+ Server.new(address: '127.0.0.1') # works
131
+ Server.new(address: IPAddr.new('127.0.0.1')) # works
132
+ Server.new(address: 42)
133
+ #=> ArgumentError:
134
+ #=> Value for attribute 'address' is not valid: 42
135
+ ```
136
+
137
+ If coercion is not possible, the value is to returned unchanged, allowing the validator to fail.
138
+ Another option is to raise an error within the coercion block.
139
+
140
+ Coercion happens before validation.
141
+ Default attribute values also pass through coercion.
142
+
143
+ The coercion block runs in the context of the value object,
144
+ so you can call methods from the value object.
145
+ For example:
146
+
147
+ ```
148
+ class Server
149
+ include ValueSemantics.for_attributes {
150
+ address IPAddr do |value|
151
+ coerce_address(value)
152
+ end
153
+ }
154
+
155
+ def coerce_address(value)
156
+ if value.is_a?(String)
157
+ IPAddr.new(value)
158
+ else
159
+ value
160
+ end
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## All Together
166
+
167
+ ```ruby
168
+ class Coordinate
169
+ include ValueSemantics.for_attributes {
170
+ latitude Float, default: 0 { |value| value.to_f }
171
+ longitude Float, default: 0 { |value| value.to_f }
172
+ }
173
+ end
174
+
175
+ Coordinate.new(longitude: "123")
176
+ #=> #<Coordinate latitude=0.0 longitude=123.0>
177
+ ```
178
+
179
+
180
+ ## Installation
181
+
182
+ Add this line to your application's Gemfile:
183
+
184
+ ```ruby
185
+ gem 'value_semantics'
186
+ ```
187
+
188
+ And then execute:
189
+
190
+ $ bundle
191
+
192
+ Or install it yourself as:
193
+
194
+ $ gem install value_semantics
195
+
196
+
197
+ ## Contributing
198
+
199
+ Bug reports and pull requests are welcome on GitHub at:
200
+ https://github.com/tomdalling/value_semantics
201
+
202
+
203
+ ## License
204
+
205
+ The gem is available as open source under the terms of the [MIT
206
+ License](http://opensource.org/licenses/MIT).
207
+
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "value_semantics"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,164 @@
1
+ module ValueSemantics
2
+ def self.for_attributes(&block)
3
+ attributes = DSL.run(&block)
4
+ generate_module(attributes)
5
+ end
6
+
7
+ def self.generate_module(attributes)
8
+ Module.new.tap do |m|
9
+ # include all the instance methods
10
+ m.include(Semantics)
11
+
12
+ # define the attr readers
13
+ attributes.each do |attr|
14
+ m.module_eval("def #{attr.name}; #{attr.instance_variable}; end")
15
+ end
16
+
17
+ # define BaseClass.attributes class method
18
+ m.const_set(:ATTRIBUTES__, attributes)
19
+ m.define_singleton_method(:included) do |base|
20
+ base.const_set(:ValueSemantics_Generated, m)
21
+ class << base
22
+ def attributes
23
+ self::ATTRIBUTES__
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ module Semantics
31
+ def initialize(given_attrs = {})
32
+ remaining_attrs = given_attrs.dup
33
+
34
+ self.class.attributes.each do |attr|
35
+ key, value = attr.determine_from!(remaining_attrs, self)
36
+ instance_variable_set(attr.instance_variable, value)
37
+ remaining_attrs.delete(key)
38
+ end
39
+
40
+ unless remaining_attrs.empty?
41
+ unrecognised = remaining_attrs.keys.map(&:inspect).join(', ')
42
+ raise ArgumentError, "Unrecognised attributes: #{unrecognised}"
43
+ end
44
+ end
45
+
46
+ def with(new_attrs)
47
+ self.class.new(to_h.merge(new_attrs))
48
+ end
49
+
50
+ def to_h
51
+ self.class.attributes
52
+ .map { |attr| [attr.name, public_send(attr.name)] }
53
+ .to_h
54
+ end
55
+
56
+ def ==(other)
57
+ (other.is_a?(self.class) || is_a?(other.class)) && other.to_h == to_h
58
+ end
59
+
60
+ def eql?(other)
61
+ other.class.equal?(self.class) && other.to_h.eql?(to_h)
62
+ end
63
+
64
+ def hash
65
+ @__hash ||= (to_h.hash ^ self.class.hash)
66
+ end
67
+
68
+ def inspect
69
+ attrs = to_h
70
+ .map { |key, value| "#{key}=#{value.inspect}" }
71
+ .join(" ")
72
+
73
+ "#<#{self.class} #{attrs}>"
74
+ end
75
+ end
76
+
77
+ class Attribute
78
+ attr_reader :name, :has_default, :default_value, :coercer
79
+
80
+ def initialize(name:, has_default:, default_value:, validator:, coercer:)
81
+ @name = name.to_sym
82
+ @has_default = has_default
83
+ @default_value = default_value
84
+ @validator = validator
85
+ @coercer = coercer
86
+ freeze
87
+ end
88
+
89
+ def determine_from!(attr_hash, value_object)
90
+ raw_value = attr_hash.fetch(name) do
91
+ if has_default
92
+ default_value
93
+ else
94
+ raise ArgumentError, "Value missing for attribute '#{name}'"
95
+ end
96
+ end
97
+
98
+ coerced_value = value_object.instance_exec(raw_value, &coercer)
99
+
100
+ if validate?(coerced_value)
101
+ [name, coerced_value]
102
+ else
103
+ raise ArgumentError, "Value for attribute '#{name}' is not valid: #{coerced_value.inspect}"
104
+ end
105
+ end
106
+
107
+ def default_value
108
+ if has_default
109
+ @default_value
110
+ else
111
+ fail "Attribute does not have a default value"
112
+ end
113
+ end
114
+
115
+ def validate?(value)
116
+ !!(@validator === value)
117
+ end
118
+
119
+ def instance_variable
120
+ '@' + name.to_s.chomp('!').chomp('?')
121
+ end
122
+ end
123
+
124
+ class DSL
125
+ NOT_SPECIFIED = Object.new
126
+
127
+ def self.run(&block)
128
+ dsl = new
129
+ dsl.instance_eval(&block)
130
+ dsl.__attributes
131
+ end
132
+
133
+ attr_reader :__attributes
134
+
135
+ def initialize
136
+ @__attributes = []
137
+ end
138
+
139
+ def method_missing(attr_name, validator=AnythingValidator,
140
+ default: NOT_SPECIFIED, &coercion_block)
141
+
142
+ __attributes << Attribute.new(
143
+ name: attr_name,
144
+ has_default: default != NOT_SPECIFIED,
145
+ default_value: default,
146
+ validator: validator,
147
+ coercer: coercion_block || IdentityCoercer,
148
+ )
149
+ end
150
+
151
+ def respond_to_missing?(method_name, include_private = false)
152
+ true
153
+ end
154
+ end
155
+
156
+ module AnythingValidator
157
+ def self.===(value)
158
+ true
159
+ end
160
+ end
161
+
162
+ IdentityCoercer = ->(value) { value }
163
+
164
+ end
@@ -0,0 +1,3 @@
1
+ module ValueSemantics
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "value_semantics/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "value_semantics"
8
+ spec.version = ValueSemantics::VERSION
9
+ spec.authors = ["Tom Dalling"]
10
+ spec.email = [["tom", "@", "tomdalling.com"].join]
11
+
12
+ spec.summary = %q{Create value classes quickly, with all the proper conventions.}
13
+ spec.description = %q{
14
+ Create value classes quickly, with all the proper conventions.
15
+
16
+ Generates modules that provide value semantics for a given set of attributes.
17
+ Provides the behaviour of an immutable struct-like value class,
18
+ with light-weight validation and coercion.
19
+ }
20
+ spec.homepage = "https://github.com/tomdalling/value_semantics"
21
+ spec.license = "MIT"
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
+ f.match(%r{^(test|spec|features)/})
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.15"
31
+ spec.add_development_dependency "rspec", "~> 3.7.0"
32
+ #spec.add_development_dependency "mutant-rspec"
33
+ spec.add_development_dependency "byebug"
34
+ spec.add_development_dependency "gem-release"
35
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: value_semantics
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom Dalling
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.7.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: gem-release
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: "\n Create value classes quickly, with all the proper conventions.\n\n
70
+ \ Generates modules that provide value semantics for a given set of attributes.\n
71
+ \ Provides the behaviour of an immutable struct-like value class,\n with light-weight
72
+ validation and coercion.\n "
73
+ email:
74
+ - tom@tomdalling.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - ".gitignore"
80
+ - ".rspec"
81
+ - ".travis.yml"
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - bin/console
86
+ - bin/setup
87
+ - lib/value_semantics.rb
88
+ - lib/value_semantics/version.rb
89
+ - value_semantics.gemspec
90
+ homepage: https://github.com/tomdalling/value_semantics
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.7.7
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Create value classes quickly, with all the proper conventions.
114
+ test_files: []