wardrobe 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 +7 -0
- data/.codeclimate.yml +10 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +18 -0
- data/Guardfile +42 -0
- data/LICENSE +19 -0
- data/README.md +227 -0
- data/Rakefile +20 -0
- data/examples/new_york_times_article.rb +55 -0
- data/lib/wardrobe/attribute.rb +76 -0
- data/lib/wardrobe/attribute_store.rb +16 -0
- data/lib/wardrobe/block_setup.rb +124 -0
- data/lib/wardrobe/boolean.rb +6 -0
- data/lib/wardrobe/class_methods.rb +127 -0
- data/lib/wardrobe/getter_setter.rb +48 -0
- data/lib/wardrobe/instance_methods.rb +56 -0
- data/lib/wardrobe/module_methods.rb +51 -0
- data/lib/wardrobe/option.rb +30 -0
- data/lib/wardrobe/option_store.rb +22 -0
- data/lib/wardrobe/plugin.rb +39 -0
- data/lib/wardrobe/plugin_store.rb +28 -0
- data/lib/wardrobe/plugins/alias_setters.rb +35 -0
- data/lib/wardrobe/plugins/coercible/refinements/array.rb +63 -0
- data/lib/wardrobe/plugins/coercible/refinements/boolean.rb +36 -0
- data/lib/wardrobe/plugins/coercible/refinements/date.rb +31 -0
- data/lib/wardrobe/plugins/coercible/refinements/date_time.rb +29 -0
- data/lib/wardrobe/plugins/coercible/refinements/float.rb +23 -0
- data/lib/wardrobe/plugins/coercible/refinements/hash.rb +65 -0
- data/lib/wardrobe/plugins/coercible/refinements/integer.rb +23 -0
- data/lib/wardrobe/plugins/coercible/refinements/object.rb +15 -0
- data/lib/wardrobe/plugins/coercible/refinements/set.rb +33 -0
- data/lib/wardrobe/plugins/coercible/refinements/string.rb +21 -0
- data/lib/wardrobe/plugins/coercible/refinements/symbol.rb +21 -0
- data/lib/wardrobe/plugins/coercible/refinements/time.rb +30 -0
- data/lib/wardrobe/plugins/coercible/refinements/unsupported_error.rb +11 -0
- data/lib/wardrobe/plugins/coercible.rb +52 -0
- data/lib/wardrobe/plugins/configurable/configurable_store.rb +26 -0
- data/lib/wardrobe/plugins/configurable.rb +40 -0
- data/lib/wardrobe/plugins/default.rb +33 -0
- data/lib/wardrobe/plugins/dirty_tracker.rb +66 -0
- data/lib/wardrobe/plugins/equality.rb +19 -0
- data/lib/wardrobe/plugins/html_initializer.rb +38 -0
- data/lib/wardrobe/plugins/immutable.rb +164 -0
- data/lib/wardrobe/plugins/ivy_presenter.rb +42 -0
- data/lib/wardrobe/plugins/json_initializer.rb +41 -0
- data/lib/wardrobe/plugins/nil_if_empty.rb +27 -0
- data/lib/wardrobe/plugins/nil_if_zero.rb +19 -0
- data/lib/wardrobe/plugins/optional_getter.rb +21 -0
- data/lib/wardrobe/plugins/optional_setter.rb +24 -0
- data/lib/wardrobe/plugins/presenter/refinements/array.rb +15 -0
- data/lib/wardrobe/plugins/presenter/refinements/hash.rb +15 -0
- data/lib/wardrobe/plugins/presenter/refinements/object.rb +15 -0
- data/lib/wardrobe/plugins/presenter/refinements/time.rb +19 -0
- data/lib/wardrobe/plugins/presenter/refinements.rb +6 -0
- data/lib/wardrobe/plugins/presenter.rb +25 -0
- data/lib/wardrobe/plugins/validation/block_handler.rb +83 -0
- data/lib/wardrobe/plugins/validation/class_methods.rb +13 -0
- data/lib/wardrobe/plugins/validation/deep_merge.rb +27 -0
- data/lib/wardrobe/plugins/validation/error_store.rb +27 -0
- data/lib/wardrobe/plugins/validation/instance_methods.rb +39 -0
- data/lib/wardrobe/plugins/validation/refinements/_size.rb +30 -0
- data/lib/wardrobe/plugins/validation/refinements/array.rb +26 -0
- data/lib/wardrobe/plugins/validation/refinements/date.rb +15 -0
- data/lib/wardrobe/plugins/validation/refinements/hash.rb +27 -0
- data/lib/wardrobe/plugins/validation/refinements/integer.rb +21 -0
- data/lib/wardrobe/plugins/validation/refinements/nil_class.rb +19 -0
- data/lib/wardrobe/plugins/validation/refinements/numeric.rb +31 -0
- data/lib/wardrobe/plugins/validation/refinements/object.rb +35 -0
- data/lib/wardrobe/plugins/validation/refinements/set.rb +25 -0
- data/lib/wardrobe/plugins/validation/refinements/string.rb +55 -0
- data/lib/wardrobe/plugins/validation/refinements/symbol.rb +26 -0
- data/lib/wardrobe/plugins/validation/refinements.rb +13 -0
- data/lib/wardrobe/plugins/validation/validation.rb +63 -0
- data/lib/wardrobe/plugins/validation/validation_error.rb +19 -0
- data/lib/wardrobe/plugins/validation/validaton_runner.rb +38 -0
- data/lib/wardrobe/plugins/validation/validator.rb +119 -0
- data/lib/wardrobe/plugins/validation.rb +28 -0
- data/lib/wardrobe/root_config.rb +31 -0
- data/lib/wardrobe/store.rb +62 -0
- data/lib/wardrobe/stores.rb +106 -0
- data/lib/wardrobe/version.rb +5 -0
- data/lib/wardrobe.rb +34 -0
- data/sandbox/Gemfile +5 -0
- data/sandbox/Gemfile.lock +52 -0
- data/sandbox/bench_mutable_coercion.rb +91 -0
- data/sandbox/bench_test_1.rb +50 -0
- data/sandbox/bench_test_2.rb +48 -0
- data/sandbox/bench_test_3.rb +92 -0
- data/sandbox/cnn_article_find.rb +20 -0
- data/sandbox/define_method_procs.rb +139 -0
- data/sandbox/hanami_validations.rb +40 -0
- data/sandbox/middleware_bench_test.rb +97 -0
- data/sandbox/middleware_bench_v2_test.rb +130 -0
- data/test/assertions.rb +22 -0
- data/test/test_helper.rb +37 -0
- data/test/unit/atrs_config_test.rb +33 -0
- data/test/unit/atrs_root_module_test.rb +19 -0
- data/test/unit/attribute_test.rb +44 -0
- data/test/unit/block_runner/array_test.rb +29 -0
- data/test/unit/block_setup_test.rb +41 -0
- data/test/unit/class_methods_test.rb +92 -0
- data/test/unit/create_class_from_hash_test.rb +28 -0
- data/test/unit/define_getter_test.rb +94 -0
- data/test/unit/define_setter_test.rb +0 -0
- data/test/unit/disable_coercion_test.rb +19 -0
- data/test/unit/inheritance_composition_test.rb +36 -0
- data/test/unit/method_override_test.rb +18 -0
- data/test/unit/method_polution_test.rb +48 -0
- data/test/unit/option_test.rb +17 -0
- data/test/unit/plugin_test.rb +12 -0
- data/test/unit/plugins/alias_setters_test.rb +24 -0
- data/test/unit/plugins/coercible/array_test.rb +79 -0
- data/test/unit/plugins/coercible/boolean_test.rb +68 -0
- data/test/unit/plugins/coercible/custom_class_test.rb +61 -0
- data/test/unit/plugins/coercible/date_test.rb +38 -0
- data/test/unit/plugins/coercible/date_time_test.rb +44 -0
- data/test/unit/plugins/coercible/float_test.rb +42 -0
- data/test/unit/plugins/coercible/hash_test.rb +62 -0
- data/test/unit/plugins/coercible/integer_test.rb +42 -0
- data/test/unit/plugins/coercible/nested_wardrobe_test.rb +63 -0
- data/test/unit/plugins/coercible/set_test.rb +49 -0
- data/test/unit/plugins/coercible/string_test.rb +36 -0
- data/test/unit/plugins/coercible/symbol_test.rb +30 -0
- data/test/unit/plugins/coercible/time_test.rb +43 -0
- data/test/unit/plugins/configurable_test.rb +64 -0
- data/test/unit/plugins/default_value_test.rb +78 -0
- data/test/unit/plugins/dirty_tracker_test.rb +78 -0
- data/test/unit/plugins/equality_test.rb +36 -0
- data/test/unit/plugins/html_initializer_test.rb +19 -0
- data/test/unit/plugins/immutable_test.rb +149 -0
- data/test/unit/plugins/ivy_presenter_test.rb +57 -0
- data/test/unit/plugins/json_initializer_test.rb +67 -0
- data/test/unit/plugins/nil_if_empty_test.rb +39 -0
- data/test/unit/plugins/nil_if_zero_test.rb +26 -0
- data/test/unit/plugins/presenter_test.rb +68 -0
- data/test/unit/plugins/validate/inheritance_test.rb +36 -0
- data/test/unit/plugins/validate/nested_model_test.rb +40 -0
- data/test/unit/plugins/validate/predicates/each_key_each_value_test.rb +34 -0
- data/test/unit/plugins/validate/predicates/each_test.rb +33 -0
- data/test/unit/plugins/validate/predicates/empty_test.rb +37 -0
- data/test/unit/plugins/validate/predicates/even_test.rb +33 -0
- data/test/unit/plugins/validate/predicates/excluded_from_test.rb +55 -0
- data/test/unit/plugins/validate/predicates/filled_test.rb +49 -0
- data/test/unit/plugins/validate/predicates/format_test.rb +28 -0
- data/test/unit/plugins/validate/predicates/gt_test.rb +36 -0
- data/test/unit/plugins/validate/predicates/gteq_test.rb +36 -0
- data/test/unit/plugins/validate/predicates/included_in_test.rb +55 -0
- data/test/unit/plugins/validate/predicates/lt_test.rb +36 -0
- data/test/unit/plugins/validate/predicates/lteq_test.rb +36 -0
- data/test/unit/plugins/validate/predicates/max_size_test.rb +43 -0
- data/test/unit/plugins/validate/predicates/min_size_test.rb +43 -0
- data/test/unit/plugins/validate/predicates/none_test.rb +49 -0
- data/test/unit/plugins/validate/predicates/odd_test.rb +33 -0
- data/test/unit/plugins/validate/predicates/size_range_test.rb +43 -0
- data/test/unit/plugins/validate/predicates/size_test.rb +43 -0
- data/test/unit/plugins/validate/predicates/type_test.rb +77 -0
- data/test/unit/plugins/validate/predicates_test.rb +73 -0
- data/test/unit/plugins/validate/validate_bang_test.rb +30 -0
- data/test/unit/store_test.rb +15 -0
- data/test/unit/stores_test.rb +18 -0
- data/wardrobe.gemspec +26 -0
- metadata +344 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 07fb6fbf62eded9e51c5fe99a69a1fcabefad551
|
4
|
+
data.tar.gz: 295843bee2fd360be1843b0bdd80639904f5ede8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 599f59179ee85174a42e79536a18e845bd4e00f008d664d93e6cea9f8aeb0aec3acce76f68ca90908c955c7c26ba2c2fba2cc41410a55b279b2f27b303e983ae
|
7
|
+
data.tar.gz: 87e8ea4d93b076dff56ed050ff15ea5122b38468015eae10eccec74e0e93a7d5bdca5d2c575973b3341f38dea7946ffce14920887ba82c08b891fa3af4edaed4
|
data/.codeclimate.yml
ADDED
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.0
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'multi_json', '~> 1.12'
|
7
|
+
gem 'nokogiri', '~> 1.7'
|
8
|
+
gem 'pry', '~> 0.10.4'
|
9
|
+
gem 'pry-byebug', '~> 3.4', platforms: :mri
|
10
|
+
gem 'pry-debugger-jruby', '~> 1.1', platforms: :jruby
|
11
|
+
gem 'rubocop', '~> 0.48', platforms: :mri
|
12
|
+
gem 'ruby-prof', '~> 0.16', platforms: :mri
|
13
|
+
gem 'virtus', '~> 1.0'
|
14
|
+
|
15
|
+
group :test do
|
16
|
+
gem 'codeclimate-test-reporter', '~> 1.0.0'
|
17
|
+
gem 'simplecov'
|
18
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
guard :minitest do
|
19
|
+
# with Minitest::Unit
|
20
|
+
watch(%r{^test/(.*)\/?test_(.*)\.rb$})
|
21
|
+
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
22
|
+
watch(%r{^test/test_helper\.rb$}) { 'test' }
|
23
|
+
|
24
|
+
# with Minitest::Spec
|
25
|
+
# watch(%r{^spec/(.*)_spec\.rb$})
|
26
|
+
# watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
27
|
+
# watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
|
28
|
+
|
29
|
+
# Rails 4
|
30
|
+
# watch(%r{^app/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
|
31
|
+
# watch(%r{^app/controllers/application_controller\.rb$}) { 'test/controllers' }
|
32
|
+
# watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" }
|
33
|
+
# watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" }
|
34
|
+
# watch(%r{^lib/(.+)\.rb$}) { |m| "test/lib/#{m[1]}_test.rb" }
|
35
|
+
# watch(%r{^test/.+_test\.rb$})
|
36
|
+
# watch(%r{^test/test_helper\.rb$}) { 'test' }
|
37
|
+
|
38
|
+
# Rails < 4
|
39
|
+
# watch(%r{^app/controllers/(.*)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" }
|
40
|
+
# watch(%r{^app/helpers/(.*)\.rb$}) { |m| "test/helpers/#{m[1]}_test.rb" }
|
41
|
+
# watch(%r{^app/models/(.*)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
|
42
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2017 Agens AS - https://www.agens.no
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# Wardrobe
|
2
|
+
[](https://travis-ci.org/agensdev/wardrobe)
|
3
|
+
[](https://codeclimate.com/github/agensdev/wardrobe)
|
4
|
+
[](https://codeclimate.com/github/agensdev/wardrobe/coverage)
|
5
|
+
<!-- [](https://rubygems.org/gems/wardrobe) -->
|
6
|
+
|
7
|
+
Wardrobe is a gem that simplifies creating Ruby objects with attributes. Wardrobe bundles a multitude of plugins. See [list](#bundled-plugins) below.
|
8
|
+
|
9
|
+
## Requirements
|
10
|
+
|
11
|
+
Wardrobe requires Ruby 2.4.0 or later. Read more about why [here](#ruby-24).
|
12
|
+
JRuby should be supported once [9.2.0.0](https://github.com/jruby/jruby/milestone/53) is released
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
```
|
17
|
+
gem install wardrobe
|
18
|
+
```
|
19
|
+
|
20
|
+
## Getting started
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
require 'wardrobe'
|
24
|
+
|
25
|
+
class User
|
26
|
+
include Wardrobe
|
27
|
+
attribute :name, String
|
28
|
+
end
|
29
|
+
|
30
|
+
User.new(name: 'Wardrobe User')
|
31
|
+
```
|
32
|
+
|
33
|
+
## Composition
|
34
|
+
|
35
|
+
Wardrobe allows you to compose models from multiple modules for easy reuse.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
module Name
|
39
|
+
include Wardrobe
|
40
|
+
attribute :first_name, String
|
41
|
+
attribute :last_name, String
|
42
|
+
end
|
43
|
+
|
44
|
+
class Person
|
45
|
+
include Name
|
46
|
+
attribute :age, Integer
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
## Coercion
|
51
|
+
|
52
|
+
Coercion is enabled by default and works with most types available in Ruby.
|
53
|
+
|
54
|
+
Example:
|
55
|
+
```ruby
|
56
|
+
class User
|
57
|
+
include Wardrobe
|
58
|
+
attribute :id, Integer
|
59
|
+
attribute :name, String
|
60
|
+
attribute :status, Symbol
|
61
|
+
attribute :friends, Array[User]
|
62
|
+
attribute :interests, Hash[String => Symbol]
|
63
|
+
end
|
64
|
+
|
65
|
+
user = User.new(
|
66
|
+
id: 1.1,
|
67
|
+
name: :'Example User',
|
68
|
+
status: 'active',
|
69
|
+
friends: [
|
70
|
+
{
|
71
|
+
id: '0045',
|
72
|
+
name: 'Another User',
|
73
|
+
status: 'inactive'
|
74
|
+
}
|
75
|
+
],
|
76
|
+
interests: {
|
77
|
+
'architecture' => 'medium',
|
78
|
+
:sports => 'low',
|
79
|
+
:travel => :high
|
80
|
+
}
|
81
|
+
)
|
82
|
+
|
83
|
+
# <User:0x007fcc160851f8
|
84
|
+
# @id=1,
|
85
|
+
# @name="Example User",
|
86
|
+
# @status=:active,
|
87
|
+
# @friends=[
|
88
|
+
# <User:0x007fcc16084b68
|
89
|
+
# @id=45,
|
90
|
+
# @name="Another User",
|
91
|
+
# @status=:inactive,
|
92
|
+
# @friends=[],
|
93
|
+
# @interests={}>
|
94
|
+
# ],
|
95
|
+
# @interests={
|
96
|
+
# "architecture"=>:medium,
|
97
|
+
# "sports"=>:low,
|
98
|
+
# "travel"=>:high
|
99
|
+
# }>
|
100
|
+
```
|
101
|
+
|
102
|
+
Coercion also works when mutating `Array`, `Hash` and `Set`. Based on the example above adding a friend to the friends array would coerce the given hash into a `User` object:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
user.friends << { id: '22', name: 'Added later' }
|
106
|
+
# => [
|
107
|
+
# #<User:0x007fb242c0c960
|
108
|
+
# @id=45,
|
109
|
+
# @name="Another User",
|
110
|
+
# @status=:inactive,
|
111
|
+
# @friends=[],
|
112
|
+
# @interests={}>,
|
113
|
+
# #<User:0x007fb242bd66a8
|
114
|
+
# @id=22,
|
115
|
+
# @name="Added later",
|
116
|
+
# @status=nil,
|
117
|
+
# @friends=[],
|
118
|
+
# @interests={}>
|
119
|
+
# ]
|
120
|
+
```
|
121
|
+
## Block syntax
|
122
|
+
|
123
|
+
Many plugins expose options for attributes. These can be enabled on each attribute needed, or you can use a block to enable for a group of attributes.
|
124
|
+
|
125
|
+
Per attribute syntax:
|
126
|
+
```ruby
|
127
|
+
class User
|
128
|
+
include Wardrobe
|
129
|
+
plugin :nil_if_empty
|
130
|
+
|
131
|
+
attribute :first_name, String, nil_if_empty: true
|
132
|
+
attribute :last_name, String, nil_if_empty: true
|
133
|
+
attribute :friends, Array, nil_if_empty: true
|
134
|
+
end
|
135
|
+
User.new(first_name: '', last_name: '', friends: [])
|
136
|
+
# => #<User:0x007fb242b5e798 @friends=nil, @first_name=nil, @last_name=nil>
|
137
|
+
```
|
138
|
+
|
139
|
+
Block syntax:
|
140
|
+
```ruby
|
141
|
+
class User
|
142
|
+
include Wardrobe
|
143
|
+
plugin :nil_if_empty
|
144
|
+
|
145
|
+
attributes do
|
146
|
+
nil_if_empty true do
|
147
|
+
attribute :first_name, String
|
148
|
+
attribute :last_name, String
|
149
|
+
attribute :friends, Array
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
User.new(first_name: '', last_name: '', friends: [])
|
154
|
+
# => #<User:0x007fb242b5e798 @friends=nil, @first_name=nil, @last_name=nil>
|
155
|
+
```
|
156
|
+
|
157
|
+
## Bundled Plugins
|
158
|
+
|
159
|
+
Wardrobe comes with numerous plugins and aims at making it easy to write your own.
|
160
|
+
|
161
|
+
|Name |Exposed options |Development state |Description |
|
162
|
+
|-------------------|---------------------|-------------------|------------------|
|
163
|
+
|validation |`validates` |BETA |dry-validation and Hanami inspired validations for your attributes|
|
164
|
+
|immutable |`immutable` |BETA |makes your object immutable. Exposes a #mutate method that will return a new immutable object|
|
165
|
+
|dirty_tracker |`track` |BETA |tracks instances and exposes a #_changed? method|
|
166
|
+
|default |`default` |BETA |default values for attributes|
|
167
|
+
|configurable | |BETA |allows you to add class level immutable configuration to your classes|
|
168
|
+
|nil_if_empty |`nil_if_empty` |BETA |Converts empty objects like `''`, `{}` and `[]` to nil when initializing.|
|
169
|
+
|nil_if_zero |`nil_if_zero` |BETA |
|
170
|
+
|alias_setters |`alias_setter(Array)`|BETA |
|
171
|
+
|optional_setter |`setter` |BETA |disable the setter|
|
172
|
+
|optional_getter |`getter` |BETA |disable the getter|
|
173
|
+
|equality |`include_in_equality`|BETA |check if to wardrobe instances are equal|
|
174
|
+
|presenter | |POC |presents your instance as a hash|
|
175
|
+
|json_initializer | |POC |initialize your model with a json string|
|
176
|
+
|html_initializer | |POC |initialize your model with a html string|
|
177
|
+
|xml_initializer | |NOT IMPLEMENTED |initialize your model with a xml string|
|
178
|
+
|
179
|
+
## Writing your own plugin
|
180
|
+
|
181
|
+
### Example
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
require 'wardrobe'
|
185
|
+
|
186
|
+
Wardrobe.register_setter(
|
187
|
+
name: :capitalize,
|
188
|
+
priority: 25,
|
189
|
+
use_if: ->(atr) { atr.options[:capitalize] && atr.klass == String },
|
190
|
+
setter: lambda do |value, _atr, _instance|
|
191
|
+
value ? value.capitalize : value
|
192
|
+
end
|
193
|
+
)
|
194
|
+
|
195
|
+
module Capitalize
|
196
|
+
extend Wardrobe::Plugin
|
197
|
+
option :capitalize, Wardrobe::Boolean, setter: :capitalize
|
198
|
+
end
|
199
|
+
|
200
|
+
Wardrobe.register_plugin(:capitalize, Capitalize)
|
201
|
+
|
202
|
+
class Person
|
203
|
+
include Wardrobe
|
204
|
+
plugin :capitalize
|
205
|
+
|
206
|
+
attribute :name, String, capitalize: true
|
207
|
+
attribute :gender, String
|
208
|
+
end
|
209
|
+
|
210
|
+
Person.new(name: 'foo', gender: 'male')
|
211
|
+
#=> #<Person:0x007fba42a273b8 @name="Foo", @gender="male">
|
212
|
+
```
|
213
|
+
|
214
|
+
## Goals
|
215
|
+
|
216
|
+
Wardrobe should:
|
217
|
+
|
218
|
+
* be faster than Virtus
|
219
|
+
* have no dependencies (plugins may)
|
220
|
+
* not pollute the instance level with any methods other than ones prefixed with `_`
|
221
|
+
* should be immutable in the config layer allowing subclasses or singleton classes to modify the config
|
222
|
+
* be easy to extend with plugins
|
223
|
+
* simplify coercions through refinements
|
224
|
+
|
225
|
+
## Ruby 2.4
|
226
|
+
|
227
|
+
When working on the first "Proof of Concept" for Wardrobe I wanted to use refinements for coercion. This was right before Ruby 2.4 was released that added support for using Kernel#send to call a method defined in a refined class. This was needed to get my first POC working and is why Wardrobe requires ruby 2.4 or above.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'test'
|
8
|
+
t.pattern = 'test/**/*_test.rb'
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::TestTask.new(:bench) do |t|
|
12
|
+
t.libs << 'test'
|
13
|
+
t.pattern = 'test/**/*_bench.rb'
|
14
|
+
end
|
15
|
+
|
16
|
+
task :console do
|
17
|
+
sh "irb -I #{File.dirname(__FILE__)}/lib -r wardrobe"
|
18
|
+
end
|
19
|
+
|
20
|
+
task default: :test
|
@@ -0,0 +1,55 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'wardrobe'
|
3
|
+
require 'http'
|
4
|
+
require 'pry'
|
5
|
+
require 'pry-byebug'
|
6
|
+
require 'awesome_print'
|
7
|
+
|
8
|
+
class Image
|
9
|
+
include Wardrobe
|
10
|
+
plugin :html_initializer
|
11
|
+
plugin :presenter
|
12
|
+
attribute :caption, String, html_selector: proc { |doc| doc.at_css('figcaption span.caption-text')&.text }
|
13
|
+
attribute :credits, String, html_selector: proc { |doc| doc.at_css('figcaption span.credit').xpath('text()')&.text&.strip }
|
14
|
+
attribute :url, String, html_selector: proc { |doc| doc.at_css('div.image img')&.attribute('data-jumbosrc')&.value || doc.attribute('itemid')&.value }
|
15
|
+
end
|
16
|
+
|
17
|
+
class Article
|
18
|
+
include Wardrobe
|
19
|
+
plugin :html_initializer
|
20
|
+
plugin :presenter
|
21
|
+
attribute :title, String, html_selector: proc { |doc| doc.at_css('header div#story-meta h1#headline')&.text }
|
22
|
+
attribute :headline, String, html_selector: proc { |doc| doc.at_css('header div#story-meta p#story-deck')&.text }
|
23
|
+
attribute :featured_images, Image, html_selector: proc { |doc| doc.at_css('header figure.media') }
|
24
|
+
attribute :published_at, Time, html_selector: proc { |doc| doc.at_css('meta[property="article:published"]').attribute('content')&.value }
|
25
|
+
attribute :modified_at, Time, html_selector: proc { |doc| doc.at_css('meta[property="article:modified"]').attribute('content')&.value }
|
26
|
+
attribute :tags, Array[String], html_selector: proc { |doc| doc.css('meta[property="article:tag"]').map { |t| t.attribute('content')&.value } }
|
27
|
+
attribute :body, Array, html_selector: proc { |doc, _atr, instance|
|
28
|
+
instance.class.parse_body_html(doc.at_css('article#story').children)
|
29
|
+
}
|
30
|
+
|
31
|
+
def self.find(url)
|
32
|
+
res = HTTP.get('https://www.nytimes.com' + url)
|
33
|
+
res = HTTP.cookies(res.cookies).get(res.headers['Location'])
|
34
|
+
res = HTTP.cookies(res.cookies).get(res.headers['Location'])
|
35
|
+
new res.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.parse_body_html(html)
|
39
|
+
html.css('p.story-body-text, figure.media').map do |item|
|
40
|
+
if item.name == 'p'
|
41
|
+
item.to_html
|
42
|
+
elsif item.name == 'figure'
|
43
|
+
# Support video
|
44
|
+
Image.new(item)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
url = '/2017/05/07/sports/baseball/wrigley-field-bullpens-renovations.html'
|
52
|
+
# url = '/2017/05/03/magazine/a-look-inside-airbuss-epic-assembly-line.html'
|
53
|
+
article = Article.find(url)
|
54
|
+
ap article._present
|
55
|
+
ap Article
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wardrobe
|
4
|
+
class UnavailableOptionError < StandardError; end
|
5
|
+
|
6
|
+
# Attribute class
|
7
|
+
class Attribute
|
8
|
+
attr_reader :name, :klass, :options, :ivar_name, :setter_name, :store,
|
9
|
+
:setters, :getters
|
10
|
+
|
11
|
+
def initialize(name, klass, defining_object, config, **options)
|
12
|
+
@name = name
|
13
|
+
@ivar_name = "@#{name}"
|
14
|
+
@setter_name = "#{name}="
|
15
|
+
@klass = validate_klass(klass)
|
16
|
+
@options = validate_options(options, config, defining_object)
|
17
|
+
@getters ||= build_getter_array(defining_object)
|
18
|
+
@setters ||= build_setter_array(defining_object)
|
19
|
+
freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_klass(klass)
|
23
|
+
if klass.is_a?(Array) && klass.count != 1
|
24
|
+
raise StandardError, %(
|
25
|
+
`Array#{klass.map(&:name)}' contains two many classes.
|
26
|
+
No more than one is allowed.
|
27
|
+
)
|
28
|
+
elsif klass.is_a?(Hash)
|
29
|
+
# TODO: Validate hash
|
30
|
+
end
|
31
|
+
klass
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge(other, defining_object, config)
|
35
|
+
merged_options = merge_options(other.options)
|
36
|
+
self.class.new(name, other.klass, defining_object, config, merged_options)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def merge_options(other)
|
42
|
+
merged_options = options.dup
|
43
|
+
other.each do |key, value|
|
44
|
+
next if merged_options[key] == value
|
45
|
+
if merged_options[key]
|
46
|
+
raise 'FIX ME! Currently only hash merge is supported' unless value.is_a?(Hash)
|
47
|
+
merged_options[key] = merged_options[key].merge(value)
|
48
|
+
else
|
49
|
+
merged_options[key] = value.dup
|
50
|
+
end
|
51
|
+
end
|
52
|
+
merged_options
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_getter_array(klass)
|
56
|
+
(klass.option_store.values.map do |option|
|
57
|
+
option.getter if option.use_getter_for_atr?(self)
|
58
|
+
end + klass.default_getters).compact.sort
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_setter_array(klass)
|
62
|
+
(klass.option_store.values.map do |option|
|
63
|
+
option.setter if option.use_setter_for_atr?(self)
|
64
|
+
end + klass.default_setters).compact.sort
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_options(options, config, defining_object)
|
68
|
+
options.each do |name, _|
|
69
|
+
unless config.option_store[name]
|
70
|
+
Wardrobe.logger.error "Option '#{name}' is unavailable for attribute '#{self.name}' on '#{defining_object}'"
|
71
|
+
raise UnavailableOptionError
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wardrobe
|
4
|
+
class AttributeStore < Store
|
5
|
+
def add(name, klass, defining_object, config, **args)
|
6
|
+
mutate do
|
7
|
+
attribute = Attribute.new(name, klass, defining_object, config, **args)
|
8
|
+
if store[name]
|
9
|
+
store[name] = store[name].merge(attribute, defining_object, config)
|
10
|
+
else
|
11
|
+
store[name] = attribute
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wardrobe
|
4
|
+
class BlockSetup
|
5
|
+
attr_reader :calling_klass, :block_options
|
6
|
+
|
7
|
+
def initialize(calling_klass)
|
8
|
+
@calling_klass = calling_klass
|
9
|
+
@block_options = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(**kargs, &blk)
|
13
|
+
if (karg = kargs.first)
|
14
|
+
kargs.delete(karg.first)
|
15
|
+
send(*karg) do
|
16
|
+
run(**kargs, &blk)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
instance_exec(&blk)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def attribute(name, klass, **keyargs, &blk)
|
24
|
+
calling_klass.attribute(name, klass, **merged_options_for_attribute(keyargs), &blk)
|
25
|
+
end
|
26
|
+
|
27
|
+
def attributes(**kargs, &blk)
|
28
|
+
run(**kargs, &blk)
|
29
|
+
end
|
30
|
+
|
31
|
+
def merged_options_for_attribute(args)
|
32
|
+
result = args.dup
|
33
|
+
block_options.map do |k, v|
|
34
|
+
option = Wardrobe.options[k]
|
35
|
+
result[k] = send("merge_#{option.klass_name}", result, k, v)
|
36
|
+
end
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
def merge_boolean(result, key, value)
|
41
|
+
result[key] || value.last
|
42
|
+
end
|
43
|
+
|
44
|
+
def merge_set(result, key, value)
|
45
|
+
convert_to_array_if_not_array(result, key)
|
46
|
+
if result[key]
|
47
|
+
Set.new(value.dup + result[key])
|
48
|
+
else
|
49
|
+
Set.new(value.dup)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge_array(result, key, value)
|
54
|
+
convert_to_array_if_not_array(result, key)
|
55
|
+
if result[key]
|
56
|
+
value.dup + result[key]
|
57
|
+
else
|
58
|
+
value.dup
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def merge_hash(result, key, value)
|
63
|
+
if result[key]
|
64
|
+
result[key].merge(value)
|
65
|
+
else
|
66
|
+
value.dup
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def convert_to_array_if_not_array(result, key)
|
71
|
+
return unless result[key] && !result[key].is_a?(Array)
|
72
|
+
result[key] = [result[key]]
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
class << self
|
77
|
+
def register_option(option)
|
78
|
+
return if option.klass == Proc || option.klass == Object
|
79
|
+
send("create_#{option.klass_name}_method", option.name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_array_method(name)
|
83
|
+
define_method(name) do |*values, &blk|
|
84
|
+
begin
|
85
|
+
block_options[name] ||= []
|
86
|
+
block_options[name].push(*values)
|
87
|
+
instance_exec(&blk) if blk || block_given?
|
88
|
+
ensure
|
89
|
+
block_options[name].pop(values.length)
|
90
|
+
block_options.delete(name) if block_options[name].empty?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
alias_method :create_set_method, :create_array_method
|
96
|
+
|
97
|
+
def create_hash_method(name)
|
98
|
+
define_method(name) do |**args, &blk|
|
99
|
+
begin
|
100
|
+
block_options[name] ||= {}
|
101
|
+
block_options[name].merge!(args)
|
102
|
+
instance_exec(&blk) if blk || block_given?
|
103
|
+
ensure
|
104
|
+
args.keys.map { |key| block_options[name].delete(key) }
|
105
|
+
block_options.delete(name) if block_options[name].empty?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_boolean_method(name)
|
111
|
+
define_method(name) do |val = true, &blk|
|
112
|
+
begin
|
113
|
+
block_options[name] ||= []
|
114
|
+
block_options[name] << val
|
115
|
+
instance_exec(&blk) if blk || block_given?
|
116
|
+
ensure
|
117
|
+
block_options[name].pop
|
118
|
+
block_options.delete(name) if block_options[name].empty?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end # class BlockSetup
|
124
|
+
end # module Wardrobe
|