contracted_value 0.1.0.alpha.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: 1475b67e1a9de52ca20803fc2b32de21ce69a061b772c0d089bfc3e11dceea1f
4
+ data.tar.gz: 8b80844c114b4224ac252cbcb5694d418a31b586ee69a12c22ef194cba05dc6e
5
+ SHA512:
6
+ metadata.gz: 5956bdb3fc30b074a6443bd014ab8be70e431676d5c4c3b552568539e364c8b2f2f53679991bd840035c8011364204de205cb22283ff5a8eac84988da81bbc3d
7
+ data.tar.gz: a1ee38d4e9a23d95397133e6e6b1fe28884c122cf453a508ead4b1ffb49f2ee949e52d53a545a2410d14c3a1485a2d591df6352d13e0f40fe8ab4da1f35e9fdb
data/.codeclimate.yml ADDED
@@ -0,0 +1,17 @@
1
+ ---
2
+ engines:
3
+ duplication:
4
+ enabled: true
5
+ config:
6
+ languages:
7
+ - ruby
8
+ fixme:
9
+ enabled: true
10
+ rubocop:
11
+ enabled: true
12
+ channel: rubocop-0-70
13
+ ratings:
14
+ paths:
15
+ - "**.rb"
16
+ exclude_paths:
17
+ - spec/
data/.editorconfig ADDED
@@ -0,0 +1,24 @@
1
+ # EditorConfig is awesome: http://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ # default configuration
7
+ [*]
8
+ indent_style = space
9
+ indent_size = 2
10
+ insert_final_newline = true
11
+ trim_trailing_whitespace = true
12
+
13
+ # Unix-style newlines with a newline ending every file
14
+ end_of_line = lf
15
+
16
+ # Set default charset
17
+ charset = utf-8
18
+
19
+ # Tab indentation (no size specified)
20
+ [Makefile]
21
+ indent_style = tab
22
+
23
+ [*.{md,markdown}]
24
+ trim_trailing_whitespace = false
data/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ # Because this is a gem, ignore Gemfile.lock:
2
+
3
+ Gemfile.lock
4
+
5
+ # And because this is Ruby, ignore the following
6
+ # (source: https://github.com/github/gitignore/blob/master/Ruby.gitignore):
7
+
8
+ *.gem
9
+ *.rbc
10
+ .bundle
11
+ .config
12
+ coverage
13
+ InstalledFiles
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
21
+
22
+ # YARD artifacts
23
+ .yardoc
24
+ _yardoc
25
+ doc/
26
+
27
+ # IDEA
28
+ /.idea/
29
+
30
+ # RSpec
31
+ /.rspec
32
+
33
+ # Appraisal
34
+ /gemfiles/.bundle/
35
+ /gemfiles/*.gemfile.lock
data/.rubocop.yml ADDED
@@ -0,0 +1,41 @@
1
+ ---
2
+ AllCops:
3
+ Exclude:
4
+ - "Gemfile*"
5
+ - "gemfiles/**/*"
6
+ - "spec/**/*"
7
+ - "node_modules/**/*"
8
+ TargetRubyVersion: 2.4
9
+
10
+ Layout/DotPosition:
11
+ Enabled: true
12
+ Severity: warning
13
+ EnforcedStyle: trailing
14
+
15
+ Metrics/MethodLength:
16
+ Enabled: false
17
+
18
+ Style/Documentation:
19
+ Enabled: false
20
+ Style/RaiseArgs:
21
+ Enabled: false
22
+ Style/StringLiterals:
23
+ Enabled: true
24
+ Severity: warning
25
+ EnforcedStyle: double_quotes
26
+ Style/StringLiteralsInInterpolation:
27
+ Enabled: true
28
+ Severity: warning
29
+ EnforcedStyle: double_quotes
30
+ Style/TrailingCommaInArguments:
31
+ Enabled: true
32
+ Severity: warning
33
+ EnforcedStyleForMultiline: consistent_comma
34
+ Style/TrailingCommaInArrayLiteral:
35
+ Enabled: true
36
+ Severity: warning
37
+ EnforcedStyleForMultiline: consistent_comma
38
+ Style/TrailingCommaInHashLiteral:
39
+ Enabled: true
40
+ Severity: warning
41
+ EnforcedStyleForMultiline: consistent_comma
data/.travis.yml ADDED
@@ -0,0 +1,25 @@
1
+ # Send builds to container-based infrastructure
2
+ # http://docs.travis-ci.com/user/workers/container-based-infrastructure/
3
+ sudo: false
4
+ language: ruby
5
+ before_install:
6
+ # This is required since 2.6.8 has issue when installing `rainbow`
7
+ # https://github.com/sickill/rainbow/issues/48
8
+ - gem update --system
9
+ - gem --version
10
+ cache:
11
+ - bundler
12
+ rvm:
13
+ - 2.4
14
+ - 2.5
15
+ - 2.6
16
+ - 2.7
17
+ - ruby-head
18
+ gemfile:
19
+ - gemfiles/contracts_15_0.gemfile
20
+ - gemfiles/contracts_16_0.gemfile
21
+ matrix:
22
+ fast_finish: true
23
+ allow_failures:
24
+ - rvm: 2.7
25
+ - rvm: ruby-head
data/Appraisals ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise "contracts_15_0" do
4
+ gem "contracts", "~> 0.15.0"
5
+ end
6
+
7
+ appraise "contracts_16_0" do
8
+ gem "contracts", "~> 0.16.0"
9
+ end
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT LICENSE
2
+
3
+ Copyright (c) PikachuEXE <pikachuexe@gmail.com>
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,346 @@
1
+ # ContractedValue
2
+
3
+ Library for creating contracted immutable(by default) value objects
4
+
5
+ [![Build Status](http://img.shields.io/travis/PikachuEXE/contracted_value.svg?style=flat-square)](https://travis-ci.org/PikachuEXE/contracted_value)
6
+
7
+ [![Code Climate](https://img.shields.io/codeclimate/maintainability/PikachuEXE/contracted_value.svg?style=flat-square)](https://codeclimate.com/github/PikachuEXE/contracted_value)
8
+ [![Coverage Status](https://img.shields.io/codecov/c/github/PikachuEXE/contracted_value.svg?style=flat-square)](https://codecov.io/gh/PikachuEXE/contracted_value)
9
+ [![Inch CI](https://inch-ci.org/github/PikachuEXE/contracted_value.svg?branch=master)](https://inch-ci.org/github/PikachuEXE/contracted_value)
10
+
11
+
12
+ This gem allows creation of value objects which are
13
+ - contracted (enforced by [`contracts.ruby`](https://github.com/egonSchiele/contracts.ruby))
14
+ - immutable (enforced by [`ice_nine`](https://github.com/dkubb/ice_nine))
15
+
16
+ See details explanation in below sections
17
+
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ # `require` can be set to `true` safely without too much side effect
25
+ # (except having additional modules & classes defined which could be wasting memory).
26
+ # But there is no point requiring it unless in test
27
+ # Also maybe add it inside a "group"
28
+ gem "contracted_value", require: false
29
+ ```
30
+
31
+ And then execute:
32
+
33
+ ```bash
34
+ $ bundle
35
+ ```
36
+
37
+ Or install it yourself as:
38
+
39
+ ```bash
40
+ $ gem install contracted_value
41
+ ```
42
+
43
+
44
+ ## Usage
45
+
46
+ The examples below might contain some of my habbits,
47
+ like including [`contracts.ruby`](https://github.com/egonSchiele/contracts.ruby) modules in class
48
+ You **don't** have to do it
49
+
50
+
51
+ ### Attribute Declaration
52
+
53
+ You can declare with or without contract/default value
54
+ But an attribute **cannot** be declared twice
55
+
56
+ ```ruby
57
+ module ::Geometry
58
+ end
59
+
60
+ module ::Geometry::LocationRange
61
+ class Entry < ::ContractedValue::Value
62
+ include ::Contracts::Core
63
+ include ::Contracts::Builtin
64
+
65
+ attribute(
66
+ :latitude,
67
+ contract: Numeric,
68
+ )
69
+ attribute(
70
+ :longitude,
71
+ contract: Numeric,
72
+ )
73
+
74
+ attribute(
75
+ :radius_in_meter,
76
+ contract: And[Numeric, Send[:positive?]],
77
+ )
78
+
79
+ attribute(
80
+ :latitude,
81
+ ) # => error, declared already
82
+ end
83
+ end
84
+
85
+ location_range = ::Geometry::LocationRange::Entry.new(
86
+ latitude: 22.2,
87
+ longitude: 114.4,
88
+ radius_in_meter: 1234,
89
+ )
90
+ ```
91
+
92
+
93
+ ### Attribute Assignment
94
+
95
+ Only `Hash` and `ContractedValue::Value` can be passed to `.new`
96
+
97
+ ```ruby
98
+ module ::Geometry
99
+ end
100
+
101
+ module ::Geometry::Location
102
+ class Entry < ::ContractedValue::Value
103
+ include ::Contracts::Core
104
+ include ::Contracts::Builtin
105
+
106
+ attribute(
107
+ :latitude,
108
+ contract: Numeric,
109
+ )
110
+ attribute(
111
+ :longitude,
112
+ contract: Numeric,
113
+ )
114
+ end
115
+ end
116
+
117
+ module ::Geometry::LocationRange
118
+ class Entry < ::ContractedValue::Value
119
+ include ::Contracts::Core
120
+ include ::Contracts::Builtin
121
+
122
+ attribute(
123
+ :latitude,
124
+ contract: Numeric,
125
+ )
126
+ attribute(
127
+ :longitude,
128
+ contract: Numeric,
129
+ )
130
+
131
+ attribute(
132
+ :radius_in_meter,
133
+ contract: Maybe[And[Numeric, Send[:positive?]]],
134
+ default_value: nil,
135
+ )
136
+ end
137
+ end
138
+
139
+ location = ::Geometry::Location::Entry.new(
140
+ latitude: 22.2,
141
+ longitude: 114.4,
142
+ )
143
+ location_range = ::Geometry::LocationRange::Entry.new(location)
144
+
145
+ ```
146
+
147
+
148
+ ### Input Validation
149
+
150
+ Input values are validated on object creation (instead of on attribute value access) with 2 validations:
151
+ - Value contract
152
+ - Value presence
153
+
154
+ #### Value contract
155
+ An attribute can be declared without any contract, and any input value would be pass the validation
156
+ But you can pass a contract via `contract` option (must be a [`contracts.ruby`](https://github.com/egonSchiele/contracts.ruby) contract)
157
+ Passing input value violating an attribute's contract would cause an error
158
+
159
+ ```ruby
160
+ class YetAnotherRationalNumber < ::ContractedValue::Value
161
+ include ::Contracts::Core
162
+ include ::Contracts::Builtin
163
+
164
+ attribute(
165
+ :numerator,
166
+ contract: ::Integer,
167
+ )
168
+ attribute(
169
+ :denominator,
170
+ contract: And[::Integer, Not[Send[:zero?]]],
171
+ )
172
+ end
173
+
174
+ YetAnotherRationalNumber.new(
175
+ numerator: 1,
176
+ denominator: 0,
177
+ ) # => Error
178
+
179
+ ```
180
+
181
+ #### Value presence
182
+ An attribute declared should be provided a value on object creation, even the input value is `nil`
183
+ Otherwise an error is raised
184
+ You can pass default value via option `default_value`
185
+ The default value will need to confront to the contract passed in `contract` option too
186
+
187
+
188
+ ```ruby
189
+ module ::WhatIsThis
190
+ class Entry < ::ContractedValue::Value
191
+ include ::Contracts::Core
192
+ include ::Contracts::Builtin
193
+
194
+ attribute(
195
+ :something_required,
196
+ )
197
+ attribute(
198
+ :something_optional,
199
+ default_value: nil,
200
+ )
201
+ attribute(
202
+ :something_with_error,
203
+ contract: NatPos,
204
+ default_value: 0,
205
+ ) # => error
206
+ end
207
+ end
208
+
209
+ WhatIsThis::Entry.new(
210
+ something_required: 123,
211
+ ).something_optional # => nil
212
+ ```
213
+
214
+
215
+ ### Object Freezing
216
+ All input values are frozen using [`ice_nine`](https://github.com/dkubb/ice_nine) by default
217
+ But some objects won't work properly when deeply frozen (rails obviously)
218
+ So you can specify how input value should be frozen (or not frozen) with option `refrigeration_mode`
219
+ Possible values are:
220
+ - `:deep` (default)
221
+ - `:shallow`
222
+ - `:none`
223
+
224
+ However the value object itself is always frozen
225
+ Any lazy method caching with use of instance var would cause `FrozenError`
226
+ (Many Rails classes use lazy caching heavily so most rails object can't be frozen to work properly)
227
+
228
+ ```ruby
229
+ class SomeDataEntry < ::ContractedValue::Value
230
+ include ::Contracts::Core
231
+ include ::Contracts::Builtin
232
+
233
+ attribute(
234
+ :cold_hash,
235
+ contract: ::Hash,
236
+ )
237
+ attribute(
238
+ :cool_hash,
239
+ contract: ::Hash,
240
+ refrigeration_mode: :shallow,
241
+ )
242
+ attribute(
243
+ :warm_hash,
244
+ contract: ::Hash,
245
+ refrigeration_mode: :none,
246
+ )
247
+
248
+ def cached_hash
249
+ @cached_hash ||= {}
250
+ end
251
+ end
252
+
253
+ entry = SomeDataEntry.new(
254
+ cold_hash: {a: {b: 0}},
255
+ cool_hash: {a: {b: 0}},
256
+ warm_hash: {a: {b: 0}},
257
+ )
258
+
259
+ entry.cold_hash[:a].delete(:b) # => `FrozenError`
260
+
261
+ entry.cool_hash[:a].delete(:b) # => fine
262
+ entry.cool_hash.delete(:a) # => `FrozenError`
263
+
264
+ entry.warm_hash.delete(:a) # => fine
265
+
266
+ entry.cached_hash # => `FrozenError`
267
+
268
+ ```
269
+
270
+ Beware that the value passed to `default_value` option when declaring an attribute is always deeply frozen
271
+ This is to avoid any in-place change which changes the default value of any value object class attribute
272
+
273
+
274
+ ### Value Object Class Inheritance
275
+ You can create a value object class inheriting an existing value class instead of `::ContractedValue::Value`
276
+
277
+ #### All existing attributes can be used
278
+ No need to explain right?
279
+ ```ruby
280
+ class Pokemon < ::ContractedValue::Value
281
+ attribute(:name)
282
+ end
283
+
284
+ class Pikachu < ::Pokemon
285
+ attribute(:type, default_value: "Thunder")
286
+ end
287
+
288
+ # Ya I love using pokemon as examples, problem?
289
+ pikachu = Pikachu.new(name: "PikaPika")
290
+ ```
291
+
292
+ #### All existing attributes can be redeclared
293
+ Within the same class you cannot redefine an attribute
294
+ But in subclasses you can
295
+ ```ruby
296
+ class Pokemon < ::ContractedValue::Value
297
+ attribute(:name)
298
+ end
299
+
300
+ class Pikachu < ::Pokemon
301
+ include ::Contracts::Core
302
+ include ::Contracts::Builtin
303
+
304
+ attribute(
305
+ :name,
306
+ contract: And[::String, Not[Send[:empty?]]],
307
+ default_value: String.new("Pikachu"),
308
+ refrigeration_mode: :none,
309
+ )
310
+ end
311
+
312
+ # Ya I love using pokemon as examples, problem?
313
+ Pikachu.new.name # => "Pikachu"
314
+ Pikachu.new.name.frozen? # => true, as mentioned above default value are always deeply frozen
315
+ Pikachu.new(name: "Pikaaaachuuu").name.frozen? # => false
316
+ ```
317
+
318
+
319
+ ## Related gems
320
+ Here is a list of gems which I found and I have tried some of them.
321
+ But eventually I am unsatisfied so I build this gem.
322
+
323
+ - [values](https://github.com/tcrayford/values)
324
+ - [active_attr](https://github.com/cgriego/active_attr)
325
+ - [dry-struct](https://github.com/dry-rb/dry-struct)
326
+
327
+ ### [values](https://github.com/tcrayford/values)
328
+ I used to use this a bit
329
+ But I keep having to write the attribute names in `Values.new`,
330
+ then the same attribute names again with `attr_reader` + contract (since I want to use contract)
331
+ Also the input validation happens on attribute value access instead of on object creation
332
+
333
+ ### [active_attr](https://github.com/cgriego/active_attr)
334
+ Got similar issue as `values`
335
+
336
+ ### [dry-struct](https://github.com/dry-rb/dry-struct)
337
+ Seems more suitable for form objects instead of just value objects (for me)
338
+
339
+
340
+ ## Contributing
341
+
342
+ 1. Fork it ( https://github.com/PikachuEXE/contracted_value/fork )
343
+ 2. Create your branch (Preferred to be prefixed with `feature`/`fix`/other sensible prefixes)
344
+ 3. Commit your changes (No version related changes will be accepted)
345
+ 4. Push to the branch on your forked repo
346
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appraisal"
4
+ require "rubygems"
5
+ require "bundler/gem_tasks"
6
+ require "rspec/core/rake_task"
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
11
+ task :default do
12
+ sh "appraisal install && rake appraisal spec"
13
+ end
14
+ else
15
+ task default: :spec
16
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable all
4
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
5
+
6
+ author_name = "PikachuEXE"
7
+ gem_name = "contracted_value"
8
+
9
+ require "#{gem_name}/version"
10
+
11
+ Gem::Specification.new do |s|
12
+ s.platform = Gem::Platform::RUBY
13
+ s.name = gem_name
14
+ s.version = ContractedValue::VERSION
15
+ s.summary = "Some Tweaks for ActiveRecord"
16
+ s.description = <<-DOC
17
+ ActiveRecord is great, but could be better. Here are some tweaks for it.
18
+ DOC
19
+
20
+ s.license = "MIT"
21
+
22
+ s.authors = [author_name]
23
+ s.email = ["pikachuexe@gmail.com"]
24
+ s.homepage = "http://github.com/#{author_name}/#{gem_name}"
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
29
+ s.require_paths = ["lib"]
30
+
31
+ s.add_dependency "contracts", "~> 0.15"
32
+ s.add_dependency "ice_nine"
33
+
34
+ s.add_development_dependency "bundler", ">= 1.0.0"
35
+ s.add_development_dependency "rake", ">= 10.0", "<= 13.0"
36
+ s.add_development_dependency "pry"
37
+
38
+ s.add_development_dependency "appraisal", "~> 2.0"
39
+
40
+ s.add_development_dependency "rspec", "~> 3.0"
41
+ s.add_development_dependency "rspec-its", "~> 1.0"
42
+
43
+ s.add_development_dependency "simplecov"
44
+ s.add_development_dependency "codecov"
45
+
46
+ s.add_development_dependency "gem-release", ">= 0.7"
47
+
48
+ s.add_development_dependency "inch", "~> 0.6"
49
+
50
+ s.add_development_dependency "rubocop", ">= 0.70"
51
+
52
+ s.required_ruby_version = ">= 2.3.0"
53
+
54
+ s.required_rubygems_version = ">= 1.4.0"
55
+ end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "contracts", "~> 0.15.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "contracts", "~> 0.16.0"
6
+
7
+ gemspec path: "../"