class_composer 1.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +18 -4
- data/Dockerfile +1 -1
- data/Gemfile +5 -5
- data/Gemfile.lock +24 -30
- data/README.md +14 -126
- data/bin/setup +0 -2
- data/class_composer.gemspec +1 -8
- data/docker-compose.yml +0 -2
- data/docs/array_usage.md +25 -0
- data/docs/basic_composer.md +118 -0
- data/docs/basic_composer_example.md +35 -0
- data/docs/composer_blocking.md +84 -0
- data/docs/freezing.md +58 -0
- data/docs/generating_initializer.md +74 -0
- data/lib/class_composer/generate_config.rb +125 -0
- data/lib/class_composer/generator.rb +157 -16
- data/lib/class_composer/version.rb +1 -1
- metadata +13 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd01814260b83eaa363cdbdff3d1f078ee85b7d6850841a847e7d89edcef6e29
|
4
|
+
data.tar.gz: 43d27db959b1dfdbd364cc1be9af76d0debe4786031822d290d7dac6292afefd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2802e0cae4ad7be7d5f866436250e24e89e5ef032d1196d0951b517e18de28b7db6473f757277f239519c167de6de3f15f10a19853fdef9818ae2e400e13e3af
|
7
|
+
data.tar.gz: 175613d8ea5f331652eabad44e6090fb16f50703dbcb099dfb72bf96a5a2cd564eb8b3c491ed411124e908adada075ec6603b6b85a41f64e9661195b665e9b00
|
data/.circleci/config.yml
CHANGED
@@ -10,10 +10,10 @@ workflows:
|
|
10
10
|
- cst/enforce-gem-version-bump
|
11
11
|
- cst/rspec-ruby:
|
12
12
|
rspec-system-args: "SIMPLE_COV_RUN=true"
|
13
|
-
cc-report-collect-ruby: "
|
13
|
+
cc-report-collect-ruby: "3.3.5"
|
14
14
|
matrix:
|
15
15
|
parameters:
|
16
|
-
ruby-version: ["
|
16
|
+
ruby-version: ["3.1.5" , "3.2.4", "3.3.5"]
|
17
17
|
alias: required-matrix-tests
|
18
18
|
name: test-ruby<< matrix.ruby-version >>
|
19
19
|
- cst/publish-gem:
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -4,12 +4,26 @@
|
|
4
4
|
|
5
5
|
**Major:** x - Major Releases with new features that constitute a breaking change
|
6
6
|
|
7
|
-
**Minor:** x.x - Minor changes or features added which are
|
7
|
+
**Minor:** x.x - Minor changes or features added which are backwards compatible
|
8
8
|
|
9
9
|
**Patch:** x.x.x - Patch Updates
|
10
10
|
|
11
11
|
# Release Notes
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
## v2.0.0 (Nov 2024)
|
14
|
+
- Minimum Ruby Version bumped to v3.1
|
15
|
+
- Initializer File Generator
|
16
|
+
- Add(`desc:`) key to your configs and ClassComposer can create a Initializer for you with all options
|
17
|
+
- Automatically will track new options added
|
18
|
+
- Options added:
|
19
|
+
- `desc:` -- Describe what the configuration is doing. Recommended but optional
|
20
|
+
- `default_shown:` With Config Initializer file Generation, you can add a custom value to display as the default (For example: Use this options with ENV variables or sensitive arguments)
|
21
|
+
- `&block`: Provide a block to the composer class method. This block gets executed on assignment after validation.
|
22
|
+
- Methods Added:
|
23
|
+
- `add_composer_blocking`(Class): Recommended composer method to use when default is another ClassComposer included class (See Readme for more details)
|
24
|
+
- `class_composer_assign_defaults!` (Instance): Class Composer values are lazily loaded. This option allows you to load configured options by calling a method (See Readme for more details)
|
25
|
+
- `class_composer_freeze_objects!`(Instance): Call this instance method if you want to make the instance immutable to changes. Helpful to ensure all changes are made before the application code runs (See Readme for more details)
|
26
|
+
- `composer_generate_config`(Instance): Create the Configuration text for an initializer file. (See Readme for more details)
|
27
|
+
|
28
|
+
## v1.0.0 June 2022
|
29
|
+
- Initial Launch!
|
data/Dockerfile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
FROM ruby:3.
|
1
|
+
FROM ruby:3.3.6
|
2
2
|
RUN cd /tmp && curl -L --output ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.12.0/ghr_v0.12.0_linux_amd64.tar.gz && \
|
3
3
|
tar -xzvf ghr.tar.gz && chmod +x ghr_v0.12.0_linux_amd64/ghr && mv ghr_v0.12.0_linux_amd64/ghr /usr/local/bin/ghr && rm -rf /tmp/*
|
4
4
|
|
data/Gemfile
CHANGED
@@ -5,8 +5,8 @@ source "https://rubygems.org"
|
|
5
5
|
# Specify your gem's dependencies in GEMNAME.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
gem
|
9
|
-
gem
|
10
|
-
gem
|
11
|
-
gem
|
12
|
-
gem
|
8
|
+
gem "faker"
|
9
|
+
gem "pry"
|
10
|
+
gem "rspec"
|
11
|
+
gem "rspec_junit_formatter"
|
12
|
+
gem "simplecov"
|
data/Gemfile.lock
CHANGED
@@ -1,60 +1,54 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
class_composer (0.
|
4
|
+
class_composer (2.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
byebug (11.1.3)
|
10
9
|
coderay (1.1.3)
|
11
|
-
concurrent-ruby (1.
|
12
|
-
diff-lcs (1.5.
|
13
|
-
docile (1.4.
|
14
|
-
faker (
|
10
|
+
concurrent-ruby (1.3.4)
|
11
|
+
diff-lcs (1.5.1)
|
12
|
+
docile (1.4.1)
|
13
|
+
faker (3.5.1)
|
15
14
|
i18n (>= 1.8.11, < 2)
|
16
|
-
i18n (1.
|
15
|
+
i18n (1.14.6)
|
17
16
|
concurrent-ruby (~> 1.0)
|
18
|
-
method_source (1.
|
19
|
-
pry (0.
|
17
|
+
method_source (1.1.0)
|
18
|
+
pry (0.15.0)
|
20
19
|
coderay (~> 1.1)
|
21
20
|
method_source (~> 1.0)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
rspec (3.
|
27
|
-
rspec-
|
28
|
-
|
29
|
-
rspec-mocks (~> 3.11.0)
|
30
|
-
rspec-core (3.11.0)
|
31
|
-
rspec-support (~> 3.11.0)
|
32
|
-
rspec-expectations (3.11.0)
|
21
|
+
rspec (3.13.0)
|
22
|
+
rspec-core (~> 3.13.0)
|
23
|
+
rspec-expectations (~> 3.13.0)
|
24
|
+
rspec-mocks (~> 3.13.0)
|
25
|
+
rspec-core (3.13.2)
|
26
|
+
rspec-support (~> 3.13.0)
|
27
|
+
rspec-expectations (3.13.3)
|
33
28
|
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
-
rspec-support (~> 3.
|
35
|
-
rspec-mocks (3.
|
29
|
+
rspec-support (~> 3.13.0)
|
30
|
+
rspec-mocks (3.13.2)
|
36
31
|
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
-
rspec-support (~> 3.
|
38
|
-
rspec-support (3.
|
39
|
-
rspec_junit_formatter (0.
|
32
|
+
rspec-support (~> 3.13.0)
|
33
|
+
rspec-support (3.13.1)
|
34
|
+
rspec_junit_formatter (0.6.0)
|
40
35
|
rspec-core (>= 2, < 4, != 2.12.0)
|
41
|
-
simplecov (0.
|
36
|
+
simplecov (0.22.0)
|
42
37
|
docile (~> 1.1)
|
43
38
|
simplecov-html (~> 0.11)
|
44
39
|
simplecov_json_formatter (~> 0.1)
|
45
|
-
simplecov-html (0.
|
40
|
+
simplecov-html (0.13.1)
|
46
41
|
simplecov_json_formatter (0.1.4)
|
47
42
|
|
48
43
|
PLATFORMS
|
44
|
+
aarch64-linux
|
49
45
|
x86_64-linux
|
50
46
|
|
51
47
|
DEPENDENCIES
|
52
48
|
class_composer!
|
53
49
|
faker
|
54
50
|
pry
|
55
|
-
|
56
|
-
rake (~> 12.0)
|
57
|
-
rspec (~> 3.0)
|
51
|
+
rspec
|
58
52
|
rspec_junit_formatter
|
59
53
|
simplecov
|
60
54
|
|
data/README.md
CHANGED
@@ -22,7 +22,7 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
### Basic
|
25
|
+
### Basic Add Composer
|
26
26
|
|
27
27
|
`add_composer` is the driving method behind the composer gem. It will
|
28
28
|
- Add a setter Method
|
@@ -32,144 +32,32 @@ Or install it yourself as:
|
|
32
32
|
|
33
33
|
In short, Composer will behave similarly to `attr_accessor`
|
34
34
|
|
35
|
+
Check out [Basic Composer](docs/basic_composer.md) for usage details
|
35
36
|
|
36
|
-
|
37
|
-
require 'class_composer'
|
38
|
-
|
39
|
-
class MyConfigurableClass
|
40
|
-
include ClassComposer::Generator
|
41
|
-
|
42
|
-
ALLOWED_FIBONACCI = [0, 2, 8, 13, 34]
|
43
|
-
|
44
|
-
add_composer :status, allowed: Integer, default: 35
|
45
|
-
add_composer :number, allowed: Integer, default: 0, validator: -> (val) { val < 50 }
|
46
|
-
# when no default is provided, nil will be returned
|
47
|
-
add_composer :fibbonacci, allowed: Array, validator: ->(val) { val.all? {|i| i.is_a?(Integer) } && val.all? { |i| ALLOWED_FIBONACCI.include?(i) } }, invalid_message: ->(val) { "We only allow #{ALLOWED_FIBONACCI} numbers. Received #{val}" }
|
48
|
-
# Allowed can be passed an array of allowed class types
|
49
|
-
add_composer :type, allowed: [Proc, Integer], default: 35
|
50
|
-
end
|
51
|
-
|
52
|
-
instance = MyConfigurableClass.new
|
53
|
-
instance.type
|
54
|
-
=> 35
|
55
|
-
instance.number = 75
|
56
|
-
ClassComposer::ValidatorError: MyConfigurableClass.number failed validation. number is expected to be Integer.
|
57
|
-
from /gem/lib/class_composer/generator.rb:71:in `block in __composer_assignment__`
|
58
|
-
instance.number = 15
|
59
|
-
=> 15
|
60
|
-
instance.number
|
61
|
-
=> 15
|
62
|
-
instance.fibbonacci
|
63
|
-
=> nil
|
64
|
-
instance.fibbonacci = [1,2,3]
|
65
|
-
ClassComposer::ValidatorError: MyConfigurableClass.fibbonacci failed validation. fibbonacci is expected to be [Array]. We only allow [0, 2, 8, 13, 34] numbers. Received [1, 2, 3]
|
66
|
-
from /gem/lib/class_composer/generator.rb:71:in `block in __composer_assignment__`
|
67
|
-
instance.fibbonacci = [0,13,34]
|
68
|
-
=> [0, 13, 34]
|
69
|
-
instance.fibbonacci
|
70
|
-
=> [0, 13, 34]
|
71
|
-
```
|
37
|
+
### Usage with Arrays
|
72
38
|
|
73
|
-
|
39
|
+
Composer allows interactions with arrays in a native way. When the allowed type is `Array`, ClassComposer will add a custom method `<<` to overwrite the native Array method. This ensures that ClassComposer Validations run when adding to the Array.
|
74
40
|
|
75
|
-
|
76
|
-
allowed
|
77
|
-
- Required: True
|
78
|
-
- What: Expected value of the name of the composed method
|
79
|
-
- Type: Array of Class types or Single Class type
|
80
|
-
|
81
|
-
validator
|
82
|
-
- Required: False
|
83
|
-
- What: Custom way to validate the value of the composed method
|
84
|
-
- Type: Proc
|
85
|
-
- Default: ->(_) { true }
|
86
|
-
- By default validation happens on the `allowed` KWARG first and then the passed in validator function. Proc should expect that the type passed in is one of `allowed`
|
87
|
-
|
88
|
-
validation_error_klass
|
89
|
-
- Required: false
|
90
|
-
- What: Class to raise when a validation error occurs from `allowed` KWarg or from the passed in `validator` proc
|
91
|
-
- Type: Class
|
92
|
-
- Default: ClassComposer::ValidatorError
|
93
|
-
|
94
|
-
validation_error_klass
|
95
|
-
- Required: false
|
96
|
-
- What: Class to raise when a errors occur outside of validation. This can be for composer method errors or proc errors during validation
|
97
|
-
- Type: Class
|
98
|
-
- Default: ClassComposer::Error
|
99
|
-
|
100
|
-
default
|
101
|
-
- Required: false
|
102
|
-
- What: This is the default value to set for the composed method
|
103
|
-
- Type: Should match the `allowed` KWarg
|
104
|
-
- Default: nil
|
105
|
-
- Note: When no default value is provided, the return value from the getter will be `nil`. However, this does not mean that NilClass will be an acceptable value during the setter method
|
106
|
-
|
107
|
-
invalid_message
|
108
|
-
- Required: False
|
109
|
-
- What: Message to add to the base invalid setter method
|
110
|
-
- Type: Proc or String
|
111
|
-
- Proc: ->(val) { } # where val is the failed value of the setter method
|
41
|
+
For more information, check out [Array Usage](docs/array_usage.md)
|
112
42
|
|
113
|
-
|
43
|
+
### Freezing Objects
|
114
44
|
|
115
|
-
|
45
|
+
Sometimes you want freeze an instance of a Configuration. Freezing it will make it immutable from changes Users may try to make.
|
116
46
|
|
117
|
-
|
118
|
-
Arrays are treated special with the composed methods. `ClassComposer` will inject a custom method `<<` so that it can be treated as a regular array with the added benefit of validation still occuring.
|
47
|
+
For more information, check out [Freezing Class Composer Instances](docs/freezing.md)
|
119
48
|
|
120
|
-
|
121
|
-
class CustomArrayClass
|
122
|
-
include ClassComposer::Generator
|
49
|
+
### Complex usage: Composer Blocking
|
123
50
|
|
124
|
-
|
125
|
-
|
51
|
+
Composer blocking builds on top of the [Basic Composer](docs/basic_composer.md) to help with nested Configurations that include `ClassCompser::Generator`.
|
52
|
+
Nested configuration's allow complex configurations for entire projects to work seamlessly together.
|
126
53
|
|
127
|
-
|
128
|
-
instance.array << 1
|
129
|
-
instance.array << 2
|
130
|
-
instance.array
|
131
|
-
=> [1, 2]
|
132
|
-
instance.array << 50
|
133
|
-
ClassComposer::ValidatorError: CustomArrayClass.array failed validation. array is expected to be Array. Array sum of [53] must be less than 40
|
134
|
-
|
135
|
-
```
|
136
|
-
|
137
|
-
#### Usage with complex configuration
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
class ComplexDependencies
|
141
|
-
include ClassComposer::Generator
|
142
|
-
|
143
|
-
add_composer :use_scope, allowed: [TrueClass, FalseClass], default: false
|
144
|
-
add_composer :scope, allowed: Proc
|
145
|
-
|
146
|
-
def scope
|
147
|
-
# skip unless use_scope is explicitly set
|
148
|
-
return -> {} unless @use_scope
|
149
|
-
|
150
|
-
# use passed in scope if present
|
151
|
-
# Otherwise default to blank default
|
152
|
-
@scope || -> {}
|
153
|
-
end
|
154
|
-
end
|
155
|
-
```
|
156
|
-
Adding custom methods allows for higher level of complexity. The methods can be used and accessed just as an `attr_accessor` would.
|
54
|
+
For Examples and use cases, check out [Composer Blocking](docs/composer_blocking.md)
|
157
55
|
|
158
56
|
## Development
|
159
57
|
|
160
58
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
161
|
-
`
|
162
|
-
prompt that will allow you to experiment.
|
163
|
-
the gem in this directory, ignoring other installed copies of this gem.
|
164
|
-
|
165
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
166
|
-
|
167
|
-
To release a new version:
|
168
|
-
|
169
|
-
1. Update the version number in [lib/class_composer/version.rb]
|
170
|
-
2. Update [CHANGELOG.md]
|
171
|
-
3. Merge to the main branch. This will trigger an automatic build in CircleCI
|
172
|
-
and push the new gem to the repo.
|
59
|
+
`bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive
|
60
|
+
prompt that will allow you to experiment.
|
173
61
|
|
174
62
|
## Contributing
|
175
63
|
|
data/bin/setup
CHANGED
data/class_composer.gemspec
CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = "https://github.com/matt-taylor/class_composer"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
|
-
spec.required_ruby_version = Gem::Requirement.new(">=
|
16
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.1")
|
17
17
|
|
18
18
|
spec.metadata = {
|
19
19
|
"homepage_uri" => spec.homepage,
|
@@ -25,12 +25,5 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
26
|
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
27
|
end
|
28
|
-
spec.bindir = "exe"
|
29
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
28
|
spec.require_paths = ["lib"]
|
31
|
-
|
32
|
-
spec.add_development_dependency "pry-byebug"
|
33
|
-
spec.add_development_dependency "rake", "~> 12.0"
|
34
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
-
spec.add_development_dependency "simplecov", "~> 0.17.0"
|
36
29
|
end
|
data/docker-compose.yml
CHANGED
data/docs/array_usage.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Usage with Array
|
2
|
+
|
3
|
+
For more details on basic setup, visit [Basic Composer Page](basic_composer.md)
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
Arrays are treated special with the composed methods. `ClassComposer` will inject a custom method `<<` so that it can be treated as a regular array with the added benefit of validation still occurring.
|
8
|
+
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class CustomArrayClass
|
12
|
+
include ClassComposer::Generator
|
13
|
+
|
14
|
+
add_composer :array, allowed: Array, default: [], validator: ->(val) { val.sum < 40 }, invalid_message: ->(val) { "Array sum of [#{val.sum}] must be less than 40" }
|
15
|
+
end
|
16
|
+
|
17
|
+
instance = CustomArrayClass.new
|
18
|
+
instance.array << 1
|
19
|
+
instance.array << 2
|
20
|
+
instance.array
|
21
|
+
=> [1, 2]
|
22
|
+
instance.array << 50
|
23
|
+
ClassComposer::ValidatorError: CustomArrayClass.array failed validation. array is expected to be Array. Array sum of [53] must be less than 40
|
24
|
+
|
25
|
+
```
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# Add Composer
|
2
|
+
|
3
|
+
`ClassComposer` by default is a DRY way to compose configurations for a class. It provides re-usable validations to ensure variables are set correctly. It is lazily loaded by default and has some wicked options available
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
require "class_composer"
|
7
|
+
class MyCoolClass
|
8
|
+
include ClassComposer::Generator
|
9
|
+
|
10
|
+
add_composer :status, allowed: Symbol, default: :in_progress
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
|
15
|
+
## Available Options:
|
16
|
+
|
17
|
+
### Allowed
|
18
|
+
- Required: Yes
|
19
|
+
- Description: This defines allowed types for this composed item. If `===` fails, it will return a runtime error
|
20
|
+
- Type: Type of allowed assignments. Or an Array of allowed assignments --
|
21
|
+
|
22
|
+
Examples:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
add_composer :status, allowed: Symbol, default: :in_progress
|
26
|
+
|
27
|
+
add_composer :status, allowed: [Symbol, String], default: :in_progress
|
28
|
+
```
|
29
|
+
|
30
|
+
### Default
|
31
|
+
- Required: False
|
32
|
+
- Description: This option allows you to set a sane default for configuration while still allowing others to overwrite the value
|
33
|
+
- Type: It _should_ be one of the `allowed` types to ensure it passes validation
|
34
|
+
|
35
|
+
Examples
|
36
|
+
```ruby
|
37
|
+
add_composer :status, allowed: Symbol, default: :different_default
|
38
|
+
|
39
|
+
add_composer :status, allowed: Symbol, default: "This type does not match Symbol. This default value will raise error"
|
40
|
+
```
|
41
|
+
|
42
|
+
### Description
|
43
|
+
- Required: False (Recommend)
|
44
|
+
- Description: This option provides a human readable description of what the current configuration option does. This value is useful when [Generating an Initializer](generating_initializer.md)
|
45
|
+
- Type: String
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
add_composer :status, allowed: Symbol, default: :in_progress, desc: "This config value is the current status for the entity."
|
49
|
+
```
|
50
|
+
|
51
|
+
|
52
|
+
### Default Shown
|
53
|
+
- Required: False (Recommend)
|
54
|
+
- Description: This option is helpful when the `default` value is a class or retrieved from an ENV variable. It will overload the `default` value when [Generating an Initializer](generating_initializer.md)
|
55
|
+
- Type: String
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
add_composer :status, allowed: Symbol, default: :in_progress, desc: "This config value is the current status for the entity.", default_shown: "completed"
|
59
|
+
```
|
60
|
+
|
61
|
+
### Provide a Block
|
62
|
+
- Required: False
|
63
|
+
- Description: The optional block will get executed after all validations have completed successful. This is a valuable option when you need to do a secondary action after the value is assigned/changed.
|
64
|
+
- Type: Proc that accepts (`key`, `value`)
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
BLOCK = Proc.new do |key, value|
|
68
|
+
User.send_mail("Your Status has changed to #{key}")
|
69
|
+
end
|
70
|
+
|
71
|
+
add_composer :status, allowed: Symbol, default: :in_progress, desc: "This config value is the current status for the entity.", default_shown: "completed", &BLOCK
|
72
|
+
```
|
73
|
+
|
74
|
+
### Validator
|
75
|
+
- Required: False
|
76
|
+
- Description: This option provides additional validation to do on the item during assignment.
|
77
|
+
- Type: Proc that returns a truthy or falsey value. Truthy response will pass validation. Falsey response will fail validation and raise a runtime error
|
78
|
+
- Default: `->(_) { true }`
|
79
|
+
|
80
|
+
Examples:
|
81
|
+
```ruby
|
82
|
+
add_composer :status, allowed: Symbol, default: :in_progress, validator: ->(value) { [:backlog,:in_progress, :complete].include?(value) }
|
83
|
+
```
|
84
|
+
|
85
|
+
### Invalid Message
|
86
|
+
- Required: False (Recommended with `validator`)
|
87
|
+
- Description: When provided, you can add a custom validation message to the runtime error. Recommended when `validator` is provided
|
88
|
+
- Type: Proc that returns a string to add to validation message
|
89
|
+
|
90
|
+
Examples:
|
91
|
+
```ruby
|
92
|
+
ALLOWED = [:backlog,:in_progress, :complete]
|
93
|
+
add_composer :status, allowed: Symbol, default: :in_progress, validator: ->(value) { ALLOWED.include?(value) }, invalid_message: ->(value) { "Value must be one of #{ALLOWED}" }
|
94
|
+
```
|
95
|
+
|
96
|
+
### Validation Error Class
|
97
|
+
- Required False
|
98
|
+
- Description: The default error to raise when validation fails.
|
99
|
+
- Default: `ClassComposer::ValidatorError`
|
100
|
+
- Type: Class that has `StandardError` Ancestor
|
101
|
+
|
102
|
+
### Error Class
|
103
|
+
- Required: False
|
104
|
+
- Description: The default error class to raise when errors outside of ClassComposer occur. EG during custom validation
|
105
|
+
- Default: `ClassComposer::Error`
|
106
|
+
- Type: Class that has `StandardError` ancestor
|
107
|
+
|
108
|
+
Examples:
|
109
|
+
```ruby
|
110
|
+
ALLOWED = [:backlog,:in_progress, :complete]
|
111
|
+
add_composer :status, allowed: Symbol, default: :in_progress, validator: ->(value) { UNDEFINED_VARIABLE.include?(value) } validation_error_klass: Exception
|
112
|
+
# Will raise with `Exception`
|
113
|
+
```
|
114
|
+
|
115
|
+
---
|
116
|
+
|
117
|
+
To see some complete Examples, visit [Basic Composer Example](basic_composer_example,md)
|
118
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
```ruby
|
2
|
+
require 'class_composer'
|
3
|
+
|
4
|
+
class MyConfigurableClass
|
5
|
+
include ClassComposer::Generator
|
6
|
+
|
7
|
+
ALLOWED_FIBONACCI = [0, 2, 8, 13, 34]
|
8
|
+
|
9
|
+
add_composer :status, allowed: Integer, default: 35
|
10
|
+
add_composer :number, allowed: Integer, default: 0, validator: -> (val) { val < 50 }
|
11
|
+
# when no default is provided, nil will be returned
|
12
|
+
add_composer :fibbonacci, allowed: Array, validator: ->(val) { val.all? {|i| i.is_a?(Integer) } && val.all? { |i| ALLOWED_FIBONACCI.include?(i) } }, invalid_message: ->(val) { "We only allow #{ALLOWED_FIBONACCI} numbers. Received #{val}" }
|
13
|
+
# Allowed can be passed an array of allowed class types
|
14
|
+
add_composer :type, allowed: [Proc, Integer], default: 35
|
15
|
+
end
|
16
|
+
|
17
|
+
instance = MyConfigurableClass.new
|
18
|
+
instance.type
|
19
|
+
=> 35
|
20
|
+
instance.number = 75
|
21
|
+
ClassComposer::ValidatorError: MyConfigurableClass.number failed validation. number is expected to be Integer.
|
22
|
+
from /gem/lib/class_composer/generator.rb:71:in `block in __composer_assignment__`
|
23
|
+
instance.number = 15
|
24
|
+
=> 15
|
25
|
+
instance.number
|
26
|
+
=> 15
|
27
|
+
instance.fibbonacci
|
28
|
+
=> nil
|
29
|
+
instance.fibbonacci = [1,2,3]
|
30
|
+
ClassComposer::ValidatorError: MyConfigurableClass.fibbonacci failed validation. fibbonacci is expected to be [Array]. We only allow [0, 2, 8, 13, 34] numbers. Received [1, 2, 3]
|
31
|
+
from /gem/lib/class_composer/generator.rb:71:in `block in __composer_assignment__`
|
32
|
+
instance.fibbonacci = [0,13,34]
|
33
|
+
=> [0, 13, 34]
|
34
|
+
instance.fibbonacci
|
35
|
+
=> [0, 13, 34]
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Composer Blocking
|
2
|
+
|
3
|
+
Composer Blocking enhances nested configuration's that utilize `ClassComposer::Generator`. It is a method that provides additional validations and usability.
|
4
|
+
|
5
|
+
Basic Example:
|
6
|
+
```ruby
|
7
|
+
require "class_composer"
|
8
|
+
|
9
|
+
class LoginStrategy
|
10
|
+
include ClassComposer::Generator
|
11
|
+
|
12
|
+
add_composer :password_regex, allowed: Regexp, default: /\A\w{6,20}\z/, desc: "Password must include valid characters between 6 and 20 in length"
|
13
|
+
|
14
|
+
add_composer :username_length, allowed: Integer, default: 10
|
15
|
+
|
16
|
+
add_composer :type, allowed: String, default: "plain_text"
|
17
|
+
end
|
18
|
+
|
19
|
+
class LockableStrategy
|
20
|
+
include ClassComposer::Generator
|
21
|
+
|
22
|
+
add_composer :enable, default: false, allowed: [TrueClass, FalseClass], desc: "By default Lockable Strategy is disabled."
|
23
|
+
add_composer :password_attempts, default: 10, allowed: Integer, desc: "Max password attempts before the account is locked"
|
24
|
+
end
|
25
|
+
|
26
|
+
class AppConfiguration
|
27
|
+
include ClassComposer::Generator
|
28
|
+
|
29
|
+
add_composer_blocking :login, composer_class: LoginStrategy, desc: "Login Strategy for my Application"
|
30
|
+
|
31
|
+
add_composer_blocking :lockable, composer_class: LockableStrategy, enable_attr: :enable, desc: "Lock Strategy for my Application. By default this is disabled"
|
32
|
+
end
|
33
|
+
|
34
|
+
instance = AppConfiguration.new
|
35
|
+
instance.with_login! do |login|
|
36
|
+
login.type = "oauth"
|
37
|
+
login.username_length = 20
|
38
|
+
end
|
39
|
+
instance.login.type
|
40
|
+
=> "oauth"
|
41
|
+
instance.login.type = "plain_text"
|
42
|
+
=> "plain_text"
|
43
|
+
|
44
|
+
instance.lockable?
|
45
|
+
=> false
|
46
|
+
# Calling the block automatically enables the config when passed the `enable_attr`
|
47
|
+
instance.with_lockable! do |lock|
|
48
|
+
lock.password_attempts = 5
|
49
|
+
end
|
50
|
+
instance.lockable?
|
51
|
+
=> true
|
52
|
+
```
|
53
|
+
|
54
|
+
## What does it produce?
|
55
|
+
### Configuration Blocking Method
|
56
|
+
Provides easy invocation for setting a nested composer configuration. By providing a block, you can easily and cleanly set your options for a specific composer item.
|
57
|
+
|
58
|
+
### Check for composer Enable
|
59
|
+
When the `enable_attr` method is provided, you can easily check if the composer instance is enabled by invoking `item_name?`. This convenience method can be used throughout the application to easily check which code paths to go down
|
60
|
+
|
61
|
+
## Allowed Options
|
62
|
+
|
63
|
+
### Composer Class
|
64
|
+
- Required: True
|
65
|
+
- Description: This is the Class object that includes `ClassComposer::Generator`. If the inclusion is missing, the method will raise an runtime error.
|
66
|
+
- Type: Class that includes `ClassComposer::Generator`
|
67
|
+
|
68
|
+
### Description
|
69
|
+
- Required: False (Recommend)
|
70
|
+
- Description: This option provides a human readable description of what the current configuration option does. This value is useful when [Generating an Initializer](generating_initializer.md)
|
71
|
+
- Type: String
|
72
|
+
|
73
|
+
### Block Prepend
|
74
|
+
- Required: False
|
75
|
+
- Description: By default, Composer Blocking prepends blocks with `with_`. This options allows you to set a custom prepended name
|
76
|
+
- Default: `with`
|
77
|
+
- Type: String or Symbol
|
78
|
+
|
79
|
+
### Enable Attr
|
80
|
+
- Required: False
|
81
|
+
- Description: When passed the `enable_attr:`, Class composer will enable the composer instance when called with a block and provide a convenience method `item_name?` to check for if the item is enabled.
|
82
|
+
- Type: String or Symbol
|
83
|
+
|
84
|
+
|
data/docs/freezing.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Freezing ClassComposer
|
2
|
+
|
3
|
+
`ClassComposer` provides a simple way to freeze instances of its classes. Freezing can help ensure that configurations do not change during the life of the script or application.
|
4
|
+
|
5
|
+
Different behaviors are available when a user attempts to change a composed item after the instance has been frozen
|
6
|
+
|
7
|
+
## Allowed Options:
|
8
|
+
### Behavior:
|
9
|
+
- Required: When `&block` is nil, behavior is required
|
10
|
+
- Description: The behavior ClassComposer should enact when a composed item tries to get changed
|
11
|
+
- Type: Symbol [:raise, :log_and_allow, :log_and_skip]
|
12
|
+
|
13
|
+
### Children:
|
14
|
+
- Required: false
|
15
|
+
- Description: Any ClassComposed item that includes `ClassComposer::Generator` is considered a nested Child. When option set to true, We will iterate the tree and set all child instances to the same behavior as the parent. One stop shop to freeze all nested configuration
|
16
|
+
- Type: Boolean
|
17
|
+
|
18
|
+
### Block
|
19
|
+
- Required: When `behavior` is nil, block is required
|
20
|
+
- Description: Custom behavior tailored to your use case. For example, In test, maybe you raise, but production maybe you allow
|
21
|
+
- Type: Passed in block, Return `true` to allow the variable to get set. Return `false` to not allow the variable to get set
|
22
|
+
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
MyCoolEngine.config.class_composer_freeze_objects!(children: true) do |instance, key|
|
26
|
+
if Rails.staging?
|
27
|
+
# allow the variable to get set in staging
|
28
|
+
Rails.logger("Yikes! you are changing a config variable after boot. We will honor this")
|
29
|
+
true
|
30
|
+
elsif Rails.prod?
|
31
|
+
# disallow the variable to get set in prod
|
32
|
+
Rails.logger("Yikes! you are changing a config variable after boot. We will NOT honor this")
|
33
|
+
false
|
34
|
+
else
|
35
|
+
raise Error, "Cant change value on #{instance.class} for key. Please change"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
### Rails Engine
|
43
|
+
When building out a complex nested configuration structure for a Rails Engine, you may want to ensure changes to the configuration do not occur after the Rails App runs its initializers. As example code, this can get added to your `*engine.rb` file
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# MyCoolEngine.config is the location of the config instance
|
47
|
+
# Assign Defaults must get run first otherwise Lazily loaded objects will run into failure
|
48
|
+
|
49
|
+
# Run after Rails loads the initializes and environment files
|
50
|
+
# Ensures User has already set their desired config before we lock this down
|
51
|
+
initializer "my_cool_engine.config.instantiate", after: :load_config_initializers do |_app|
|
52
|
+
# ensure defaults are instantiated and all variables are assigned
|
53
|
+
MyCoolEngine.config.class_composer_assign_defaults!(children: true)
|
54
|
+
|
55
|
+
# Now that we can confirm all variables are defined, freeze all objects an their children
|
56
|
+
MyCoolEngine.config.class_composer_freeze_objects!(behavior: :raise, children: true)
|
57
|
+
end
|
58
|
+
```
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Generating Initializer
|
2
|
+
|
3
|
+
Generating an initializer can help ensure that all users understand all potential configuration options without searching the codebase.
|
4
|
+
|
5
|
+
This generation will add both [Basic Composer Options](basic_composer.md) and [Composer Blocking Options](composer_blocking.md) to a configuration file.
|
6
|
+
|
7
|
+
The file output will show show assignment to all default values. Additionally all lines are commented out so the User can
|
8
|
+
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class LoginStrategy
|
12
|
+
include ClassComposer::Generator
|
13
|
+
|
14
|
+
add_composer :password_regex, allowed: Regexp, default: /\A\w{6,20}\z/, desc: "Password must include valid characters between 6 and 20 in length"
|
15
|
+
|
16
|
+
add_composer :username_length, allowed: Integer, default: 10
|
17
|
+
|
18
|
+
add_composer :type, allowed: String, default: "plain_text"
|
19
|
+
end
|
20
|
+
|
21
|
+
class LockableStrategy
|
22
|
+
include ClassComposer::Generator
|
23
|
+
|
24
|
+
add_composer :enable, default: false, allowed: [TrueClass, FalseClass], desc: "By default Lockable Strategy is disabled."
|
25
|
+
add_composer :password_attempts, default: 10, allowed: Integer, desc: "Max password attempts before the account is locked"
|
26
|
+
end
|
27
|
+
|
28
|
+
class AppConfiguration
|
29
|
+
include ClassComposer::Generator
|
30
|
+
|
31
|
+
add_composer_blocking :login, composer_class: LoginStrategy, desc: "Login Strategy for my Application"
|
32
|
+
|
33
|
+
add_composer_blocking :lockable, composer_class: LockableStrategy, enable_attr: :enable, desc: "Lock Strategy for my Application. By default this is disabled"
|
34
|
+
end
|
35
|
+
|
36
|
+
puts AppConfiguration.composer_generate_config(wrapping: "MyApplication.configure")
|
37
|
+
|
38
|
+
----
|
39
|
+
|
40
|
+
=begin
|
41
|
+
This configuration files lists all the configuration options available.
|
42
|
+
To change the default value, uncomment the line and change the value.
|
43
|
+
Please take note: Values set as `=` to a config variable are the current default values when none is assigned
|
44
|
+
=end
|
45
|
+
|
46
|
+
MyApplication.configure do |config|
|
47
|
+
# ### Block to configure Login ###
|
48
|
+
# Login Strategy for my Application
|
49
|
+
# config.with_login do |login_config|
|
50
|
+
# Password must include valid characters between 6 and 20 in length: [Regexp]
|
51
|
+
# login_config.password_regex = (?-mix:\A\w{6,20}\z)
|
52
|
+
|
53
|
+
# login_config.username_length = 10
|
54
|
+
|
55
|
+
# login_config.type = "plain_text"
|
56
|
+
# end
|
57
|
+
|
58
|
+
# ### Block to configure Lockable ###
|
59
|
+
# Lock Strategy for my Application. By default this is disabled
|
60
|
+
# When using the block, the enable flag will automatically get set to true
|
61
|
+
# config.with_lockable do |lockable_config|
|
62
|
+
# By default Lockable Strategy is disabled.: [TrueClass, FalseClass]
|
63
|
+
# lockable_config.enable = false
|
64
|
+
|
65
|
+
# Max password attempts before the account is locked: [Integer]
|
66
|
+
# lockable_config.password_attempts = 10
|
67
|
+
# end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
## Usage Applications
|
72
|
+
### Rails Generator
|
73
|
+
Are you building an Engine or a Gem that requires custom configuration. This code can easily help downstream users understand exactly what options are available to them to configure your Engine/Gem.
|
74
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClassComposer
|
4
|
+
class GenerateConfig
|
5
|
+
attr_reader :instance
|
6
|
+
NOTICE = <<~HEREDOC
|
7
|
+
=begin
|
8
|
+
This configuration files lists all the configuration options available.
|
9
|
+
To change the default value, uncomment the line and change the value.
|
10
|
+
Please take note: Values set as `=` to a config variable are the current default values when none is assigned
|
11
|
+
=end
|
12
|
+
HEREDOC
|
13
|
+
|
14
|
+
def initialize(instance:)
|
15
|
+
raise ArgumentError, ":instance class (#{instance}) must include ClassComposer::Generator. It does not" unless instance.include?(ClassComposer::Generator)
|
16
|
+
|
17
|
+
@instance = instance
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute(wrapping:, require_file:, space_count: 1, config_name: "config")
|
21
|
+
mapping = instance.composer_mapping
|
22
|
+
generated_config = generate(mapping:, space_count:, demeters_deep:[config_name])
|
23
|
+
|
24
|
+
stringified = ""
|
25
|
+
stringified += "require \"#{require_file}\"\n\n" if require_file
|
26
|
+
stringified += NOTICE
|
27
|
+
stringified += "\n"
|
28
|
+
stringified += "#{wrapping} do |#{config_name}|\n"
|
29
|
+
flattened_config = generated_config.flatten(1).map { _1.join(" ") }
|
30
|
+
flattened_config.pop if flattened_config[-1] == ""
|
31
|
+
|
32
|
+
stringified += flattened_config.join("\n")
|
33
|
+
stringified += "\nend"
|
34
|
+
stringified
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def generate(mapping:, space_count:, demeters_deep:)
|
40
|
+
mapping.map do |key, metadata|
|
41
|
+
if blocking_attributes = metadata[:blocking_attributes]
|
42
|
+
if children = metadata[:children]
|
43
|
+
do_block = "#{key}_config"
|
44
|
+
blocking(key:, do_block:, metadata:, space_count:, demeters_deep:, blocking_attributes:) do
|
45
|
+
generate(mapping: children.first, space_count: space_count + 2, demeters_deep: [do_block])
|
46
|
+
end
|
47
|
+
else
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
elsif children = metadata[:children]
|
51
|
+
config_prepend = demeters_deep + [key]
|
52
|
+
children_config = []
|
53
|
+
if desc = metadata[:desc]
|
54
|
+
children_config << spec_child_description(space_count:, desc:, key:)
|
55
|
+
end
|
56
|
+
|
57
|
+
children.each do |child|
|
58
|
+
children_config += generate(mapping: child, space_count:, demeters_deep: config_prepend)
|
59
|
+
end
|
60
|
+
# binding.pry
|
61
|
+
|
62
|
+
children_config.flatten(1)
|
63
|
+
else
|
64
|
+
spec(key:, metadata:, space_count:, demeters_deep:)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def spec_child_description(space_count:, desc:, key:)
|
70
|
+
base = "#########"
|
71
|
+
length = base.length * 2 + 4 + key.capitalize.length
|
72
|
+
|
73
|
+
[
|
74
|
+
[prepending(space_count),"#" * length],
|
75
|
+
[prepending(space_count),"##{" " * (length - 2)}#" ],
|
76
|
+
[prepending(space_count), "#{base} #{key.capitalize} #{base}"],
|
77
|
+
[prepending(space_count),"##{" " * (length - 2)}#" ],
|
78
|
+
[prepending(space_count),"#" * length],
|
79
|
+
[prepending(space_count), "## #{desc}"],
|
80
|
+
[],
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
def blocking(key:, do_block:, metadata:, space_count:, demeters_deep:, blocking_attributes:)
|
85
|
+
config = concat_demeter_with_key(blocking_attributes[:block_name], demeters_deep)
|
86
|
+
values = [
|
87
|
+
[prepending(space_count), "### Block to configure #{key.to_s.split("_").map {_1.capitalize}.join(" ")} ###"],
|
88
|
+
[prepending(space_count), metadata[:desc]],
|
89
|
+
]
|
90
|
+
values << [prepending(space_count), "When using the block, the #{blocking_attributes[:enable_attr]} flag will automatically get set to true"] if blocking_attributes[:enable_attr]
|
91
|
+
values << [prepending(space_count), config, "do", "|#{do_block}|"]
|
92
|
+
|
93
|
+
values += yield.flatten(1)
|
94
|
+
values.pop if values[-1] == [""]
|
95
|
+
values << [prepending(space_count), "end"]
|
96
|
+
|
97
|
+
values << [""]
|
98
|
+
end
|
99
|
+
|
100
|
+
def spec(key:, metadata:, space_count:, demeters_deep:)
|
101
|
+
config = concat_demeter_with_key(key, demeters_deep)
|
102
|
+
|
103
|
+
if metadata[:allowed].include?(String)
|
104
|
+
default = "\"#{metadata[:default]}\""
|
105
|
+
else
|
106
|
+
default = metadata[:default]
|
107
|
+
end
|
108
|
+
arr = []
|
109
|
+
|
110
|
+
arr << [prepending(space_count), "#{metadata[:desc]}: #{(metadata[:allowed] - [ClassComposer::DefaultObject])}"] if metadata[:desc]
|
111
|
+
arr <<[prepending(space_count), config, "=", default]
|
112
|
+
arr << [""]
|
113
|
+
|
114
|
+
arr
|
115
|
+
end
|
116
|
+
|
117
|
+
def prepending(space_count)
|
118
|
+
"#{" " * space_count}#"
|
119
|
+
end
|
120
|
+
|
121
|
+
def concat_demeter_with_key(key, demeters_deep)
|
122
|
+
(demeters_deep + ["#{key}"]).join(".")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -1,38 +1,170 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "class_composer/default_object"
|
4
|
+
require "class_composer/generate_config"
|
4
5
|
|
5
6
|
module ClassComposer
|
7
|
+
FROZEN_TYPES = [
|
8
|
+
DEFAULT_FROZEN_TYPE = FROZEN_RAISE = :raise,
|
9
|
+
FROZEN_LOG_AND_ALLOW = :log_and_allow,
|
10
|
+
FROZEN_LOG_AND_SKIP = :log_and_skip,
|
11
|
+
]
|
6
12
|
module Generator
|
7
13
|
def self.included(base)
|
8
14
|
base.extend(ClassMethods)
|
15
|
+
base.include(InstanceMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
def class_composer_frozen!(key)
|
20
|
+
# when nil, we allow changes to the instance methods
|
21
|
+
return if @class_composer_frozen.nil?
|
22
|
+
|
23
|
+
# When frozen is a proc, we let the user decide how to handle
|
24
|
+
# The return value decides if the value can be changed or not
|
25
|
+
if Proc === @class_composer_frozen
|
26
|
+
return @class_composer_frozen.(self, key)
|
27
|
+
end
|
28
|
+
|
29
|
+
msg = "#{self.class} instance methods are frozen. Attempted to change variable [#{key}]."
|
30
|
+
case @class_composer_frozen
|
31
|
+
when FROZEN_LOG_AND_ALLOW
|
32
|
+
msg += " This operation will proceed."
|
33
|
+
Kernel.warn(msg)
|
34
|
+
return true
|
35
|
+
when FROZEN_LOG_AND_SKIP
|
36
|
+
msg += " This operation will NOT proceed."
|
37
|
+
Kernel.warn(msg)
|
38
|
+
return false
|
39
|
+
when FROZEN_RAISE
|
40
|
+
raise Error, msg
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def class_composer_assign_defaults!(children: false)
|
45
|
+
self.class.composer_mapping.each do |key, metadata|
|
46
|
+
assigned_value = method(:"#{key}").call
|
47
|
+
method(:"#{key}=").call(assigned_value)
|
48
|
+
|
49
|
+
if children && metadata[:children]
|
50
|
+
method(:"#{key}").call().class_composer_assign_defaults!(children: children)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def class_composer_freeze_objects!(behavior: nil, children: false, &block)
|
58
|
+
if behavior && block
|
59
|
+
raise ArgumentError, "`behavior` and `block` can not both be present. Choose one"
|
60
|
+
end
|
61
|
+
|
62
|
+
if behavior.nil? && block.nil?
|
63
|
+
raise ArgumentError, "`behavior` or `block` must be present."
|
64
|
+
end
|
65
|
+
|
66
|
+
if block
|
67
|
+
@class_composer_frozen = block
|
68
|
+
else
|
69
|
+
if !FROZEN_TYPES.include?(behavior)
|
70
|
+
raise Error, "Unknown behavior [#{behavior}]. Expected one of #{FROZEN_TYPES}."
|
71
|
+
end
|
72
|
+
@class_composer_frozen = behavior
|
73
|
+
end
|
74
|
+
|
75
|
+
# If children is set, iterate the children, otherwise exit early
|
76
|
+
return if children == false
|
77
|
+
|
78
|
+
self.class.composer_mapping.each do |key, metadata|
|
79
|
+
next unless metadata[:children]
|
80
|
+
|
81
|
+
method(:"#{key}").call().class_composer_freeze_objects!(behavior:, children:, &block)
|
82
|
+
end
|
83
|
+
end
|
9
84
|
end
|
10
85
|
|
11
86
|
module ClassMethods
|
12
87
|
COMPOSER_VALIDATE_METHOD_NAME = ->(name) { :"__composer_#{name}_is_valid__?" }
|
13
88
|
COMPOSER_ASSIGNED_ATTR_NAME = ->(name) { :"@__composer_#{name}_value_assigned__" }
|
14
89
|
COMPOSER_ASSIGNED_ARRAY_METHODS = ->(name) { :"@__composer_#{name}_array_methods_set__" }
|
90
|
+
COMPOSER_ALLOWED_FROZEN_TYPE_ARGS = [:raise, :log]
|
91
|
+
|
92
|
+
def add_composer_blocking(name, composer_class:, desc: nil, block_prepend: "with", enable_attr: nil)
|
93
|
+
unless composer_class.include?(ClassComposer::Generator)
|
94
|
+
raise ClassComposer::Error, ".add_composer_blocking passed `composer_class:` that does not include ClassComposer::Generator. Passed argument must include ClassComposer::Generator"
|
95
|
+
end
|
96
|
+
|
97
|
+
blocking_name = "#{block_prepend}_#{name}"
|
98
|
+
blocking_attributes = { block_name: blocking_name, enable_attr: enable_attr }
|
99
|
+
add_composer(name, allowed: composer_class, default: composer_class.new, desc: desc, blocking_attributes: blocking_attributes)
|
15
100
|
|
16
|
-
|
101
|
+
define_method(blocking_name) do |&blk|
|
102
|
+
instance = public_send(:"#{name}")
|
103
|
+
instance.public_send(:"#{enable_attr}=", true) if enable_attr
|
104
|
+
|
105
|
+
blk.(instance) if blk
|
106
|
+
|
107
|
+
method(:"#{name}=").call(instance)
|
108
|
+
end
|
109
|
+
|
110
|
+
if enable_attr
|
111
|
+
define_method("#{name}?") do
|
112
|
+
public_send(:"#{name}").public_send(enable_attr)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_composer(name, allowed:, desc: nil, validator: ->(_) { true }, validation_error_klass: ::ClassComposer::ValidatorError, error_klass: ::ClassComposer::Error, blocking_attributes: nil, default_shown: nil, **params, &blk)
|
17
118
|
default =
|
18
119
|
if params.has_key?(:default)
|
19
120
|
params[:default]
|
20
121
|
else
|
21
|
-
if allowed.is_a?(Array)
|
22
|
-
allowed << ClassComposer::DefaultObject
|
23
|
-
else
|
24
|
-
allowed = [allowed, ClassComposer::DefaultObject]
|
25
|
-
end
|
26
122
|
ClassComposer::DefaultObject
|
27
123
|
end
|
28
124
|
|
29
|
-
allowed.
|
125
|
+
if allowed.is_a?(Array)
|
126
|
+
allowed << ClassComposer::DefaultObject
|
127
|
+
else
|
128
|
+
allowed = [allowed, ClassComposer::DefaultObject]
|
129
|
+
end
|
130
|
+
|
131
|
+
if allowed.select { _1.include?(ClassComposer::Generator) }.count > 1
|
132
|
+
raise Error, "Allowed arguments has multiple classes that include ClassComposer::Generator. Max 1 is allowed"
|
133
|
+
end
|
134
|
+
|
30
135
|
validate_proc = __composer_validator_proc__(validator: validator, allowed: allowed, name: name, error_klass: error_klass)
|
31
136
|
__composer_validate_options__!(name: name, validate_proc: validate_proc, default: default, validation_error_klass: validation_error_klass, error_klass: error_klass)
|
32
137
|
|
33
138
|
array_proc = __composer_array_proc__(name: name, validator: validator, allowed: allowed, params: params)
|
34
|
-
__composer_assignment__(name: name, allowed: allowed, params: params, validator: validate_proc, array_proc: array_proc, validation_error_klass: validation_error_klass, error_klass: error_klass)
|
139
|
+
__composer_assignment__(name: name, allowed: allowed, params: params, validator: validate_proc, array_proc: array_proc, validation_error_klass: validation_error_klass, error_klass: error_klass, &blk)
|
35
140
|
__composer_retrieval__(name: name, default: default, array_proc: array_proc)
|
141
|
+
|
142
|
+
# Add to mapping
|
143
|
+
__add_to_composer_mapping__(name: name, default: default, allowed: allowed, desc: desc, blocking_attributes: blocking_attributes, default_shown: default_shown)
|
144
|
+
end
|
145
|
+
|
146
|
+
def composer_mapping
|
147
|
+
@composer_mapping ||= {}
|
148
|
+
end
|
149
|
+
|
150
|
+
def composer_generate_config(wrapping:, require_file: nil, space_count: 2)
|
151
|
+
@composer_generate_config ||= GenerateConfig.new(instance: self)
|
152
|
+
|
153
|
+
@composer_generate_config.execute(wrapping:, require_file:, space_count:)
|
154
|
+
end
|
155
|
+
|
156
|
+
def __add_to_composer_mapping__(name:, default:, allowed:, desc:, blocking_attributes:, default_shown: nil)
|
157
|
+
children = Array(allowed).select { _1.include?(ClassComposer::Generator) }.map do |allowed_class|
|
158
|
+
allowed_class.composer_mapping
|
159
|
+
end
|
160
|
+
|
161
|
+
composer_mapping[name] = {
|
162
|
+
desc: desc,
|
163
|
+
children: children.empty? ? nil : children,
|
164
|
+
default: default_shown || (default.to_s.start_with?("#<") ? default.class : default),
|
165
|
+
blocking_attributes: blocking_attributes,
|
166
|
+
allowed: allowed,
|
167
|
+
}.compact
|
36
168
|
end
|
37
169
|
|
38
170
|
def __composer_validate_options__!(name:, validate_proc:, default:, params: {}, validation_error_klass:, error_klass:)
|
@@ -52,22 +184,27 @@ module ClassComposer
|
|
52
184
|
end
|
53
185
|
|
54
186
|
# create assignment method for the incoming name
|
55
|
-
def __composer_assignment__(name:, params:, allowed:, validator:, array_proc:, validation_error_klass:, error_klass
|
187
|
+
def __composer_assignment__(name:, params:, allowed:, validator:, array_proc:, validation_error_klass:, error_klass:, &blk)
|
56
188
|
define_method(:"#{name}=") do |value|
|
189
|
+
case class_composer_frozen!(name)
|
190
|
+
when false
|
191
|
+
# false is returned when the instance is frozen AND we do not allow the operation to proceed
|
192
|
+
return
|
193
|
+
when true
|
194
|
+
# true is returned when the instance is frozen AND we allow the operation to proceed
|
195
|
+
when nil
|
196
|
+
# nil is returned when the instance is not frozen
|
197
|
+
end
|
198
|
+
|
57
199
|
is_valid = validator.(value)
|
58
200
|
|
59
201
|
if is_valid
|
60
202
|
instance_variable_set(COMPOSER_ASSIGNED_ATTR_NAME.(name), true)
|
61
203
|
instance_variable_set(:"@#{name}", value)
|
62
204
|
else
|
63
|
-
message = ["#{self.class}.#{name} failed validation. #{name} is expected to be #{allowed}."]
|
205
|
+
message = ["#{self.class}.#{name} failed validation. #{name} is expected to be #{allowed}. Received [#{value}](#{value.class})"]
|
64
206
|
|
65
207
|
message << (params[:invalid_message].is_a?(Proc) ? params[:invalid_message].(value) : params[:invalid_message].to_s)
|
66
|
-
if value.is_a?(Array)
|
67
|
-
# we assigned the array value...pop it from the array
|
68
|
-
# must be done after the message is created so that failing value can get passed appropriately
|
69
|
-
value.pop
|
70
|
-
end
|
71
208
|
raise validation_error_klass, message.compact.join(" ")
|
72
209
|
end
|
73
210
|
|
@@ -79,6 +216,10 @@ module ClassComposer
|
|
79
216
|
value.instance_variable_set(COMPOSER_ASSIGNED_ARRAY_METHODS.(name), true)
|
80
217
|
end
|
81
218
|
|
219
|
+
if blk
|
220
|
+
yield(name, value)
|
221
|
+
end
|
222
|
+
|
82
223
|
value
|
83
224
|
end
|
84
225
|
end
|
@@ -120,7 +261,7 @@ module ClassComposer
|
|
120
261
|
# Default object will likely raise an error if there is a custom validator
|
121
262
|
(allowed.include?(ClassComposer::DefaultObject) && value == ClassComposer::DefaultObject) || (allow && validator.(value))
|
122
263
|
rescue StandardError => e
|
123
|
-
raise error_klass, "#{e}
|
264
|
+
raise error_klass, "#{e} occurred during validation for value [#{value}]. Check custom validator for #{name}"
|
124
265
|
end
|
125
266
|
end
|
126
267
|
end
|
metadata
CHANGED
@@ -1,71 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: class_composer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Taylor
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: pry-byebug
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '12.0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '12.0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rspec
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '3.0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '3.0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: simplecov
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 0.17.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.17.0
|
11
|
+
date: 2024-12-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
69
13
|
description: Compose configurations for any class.
|
70
14
|
email:
|
71
15
|
- mattius.taylor@gmail.com
|
@@ -89,8 +33,15 @@ files:
|
|
89
33
|
- bin/setup
|
90
34
|
- class_composer.gemspec
|
91
35
|
- docker-compose.yml
|
36
|
+
- docs/array_usage.md
|
37
|
+
- docs/basic_composer.md
|
38
|
+
- docs/basic_composer_example.md
|
39
|
+
- docs/composer_blocking.md
|
40
|
+
- docs/freezing.md
|
41
|
+
- docs/generating_initializer.md
|
92
42
|
- lib/class_composer.rb
|
93
43
|
- lib/class_composer/default_object.rb
|
44
|
+
- lib/class_composer/generate_config.rb
|
94
45
|
- lib/class_composer/generator.rb
|
95
46
|
- lib/class_composer/version.rb
|
96
47
|
homepage: https://github.com/matt-taylor/class_composer
|
@@ -107,14 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
58
|
requirements:
|
108
59
|
- - ">="
|
109
60
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
61
|
+
version: '3.1'
|
111
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
63
|
requirements:
|
113
64
|
- - ">="
|
114
65
|
- !ruby/object:Gem::Version
|
115
66
|
version: '0'
|
116
67
|
requirements: []
|
117
|
-
rubygems_version: 3.
|
68
|
+
rubygems_version: 3.5.9
|
118
69
|
signing_key:
|
119
70
|
specification_version: 4
|
120
71
|
summary: Easily compose a class via inline code or passed in YAML config. Add instance
|