declarative_initialization 0.1.1 → 0.2.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 +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +27 -3
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +133 -64
- data/Rakefile +4 -0
- data/lib/declarative_initialization/class_methods.rb +29 -46
- data/lib/declarative_initialization/internal.rb +88 -0
- data/lib/declarative_initialization/version.rb +1 -1
- data/lib/declarative_initialization.rb +4 -13
- metadata +2 -2
- data/lib/declarative_initialization/instance_methods.rb +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e2dbea81aa3125522d94fde6f67206de5d3c828fefd7c1001a4b5b37a77cdae
|
|
4
|
+
data.tar.gz: 39f96acbc627c8da767dc864a4bc983795788bc3e27a67a375270252d81936dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 379472ad15222200b17f1550501d2b2e2db1807d3c76e73da59e789014582b9609bed23f1699b77bb2a8c41e5d673c9dd055c3f528130559a34599a5dffa1287
|
|
7
|
+
data.tar.gz: acafc11dd1d73ab41c2401de250c185dc2a71c0826c8a262f0692fa8f0b4d376264efae117cbddf216c23e4b3d29b0b1f9b37cf255e6460ad6ed561fa6feeee0
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
* N/A
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-02-19
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- `initialize_with` readers are now always defined, even if a method with the same name already exists. This makes `foo` consistently return the init-arg value.
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
- Previously, if a method `#foo` existed (on the class or an ancestor), the gem skipped defining the reader and logged a warning; callers had to use `@foo` to access the init-arg. Now the reader is defined and overrides the existing method.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Optional override warnings in Rails development/test, or when logger level is `DEBUG`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- No warning on Rails reload when the existing method was originally defined by this gem.
|
|
18
|
+
- No warning when a subclass re-declares an attribute already declared by an ancestor’s `initialize_with`.
|
|
19
|
+
- Duplicate common mutable default values (`Array`, `Hash`, `Set`, `String`) per instance when the caller omits that keyword, preventing accidental cross-instance mutation. Copy is shallow; caller-provided values are not duplicated.
|
|
20
|
+
|
|
21
|
+
|
|
3
22
|
## [0.1.1] - 2025-05-02
|
|
4
|
-
- Refactor internals
|
|
5
|
-
- [BUGFIX] Only trigger `attr_reader` creation on initial class load (vs on every call to `#new`)
|
|
6
23
|
|
|
24
|
+
### Changed
|
|
25
|
+
- Refactor internals.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Only define `attr_reader`s on initial class setup (avoid re-defining on each call to `.new`).
|
|
7
29
|
|
|
8
30
|
## [0.1.0] - 2025-03-05
|
|
9
|
-
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Initial release.
|
data/CODE_OF_CONDUCT.md
CHANGED
|
@@ -39,7 +39,7 @@ This Code of Conduct applies within all community spaces, and also applies when
|
|
|
39
39
|
|
|
40
40
|
## Enforcement
|
|
41
41
|
|
|
42
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at
|
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at oss@teamshares.com. All complaints will be reviewed and investigated promptly and fairly.
|
|
43
43
|
|
|
44
44
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
|
45
45
|
|
data/README.md
CHANGED
|
@@ -1,106 +1,175 @@
|
|
|
1
1
|
# DeclarativeInitialization
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Declare a class’s keyword inputs once and get a keyword-only `initialize` with assignments, readers, and helpful argument errors.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **Keyword-only initializer**: rejects positional args and unknown keywords
|
|
6
|
+
- **Assignments**: sets `@keyword` instance variables from declared inputs
|
|
7
|
+
- **Readers**: defines `attr_reader` for each input (and a `block` reader)
|
|
8
|
+
- **Defaults**: supports optional keywords with default values
|
|
9
|
+
- **No dependencies**: plain Ruby (\(>= 3.0\))
|
|
6
10
|
|
|
7
|
-
##
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
Use this when you have small POROs that take keyword inputs and you’re tired of repeating the same initializer boilerplate:
|
|
14
|
+
|
|
15
|
+
- **Service / command objects** that take dependencies and parameters
|
|
16
|
+
- **Value objects** with a fixed set of attributes
|
|
17
|
+
- **Configuration objects** with a handful of optional flags
|
|
18
|
+
- **Components / presenters** that accept a stable set of inputs
|
|
19
|
+
|
|
20
|
+
If you need complex inheritance initialization, multiple initializer “shapes”, or highly dynamic defaults, a handwritten `initialize` may be clearer.
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add to your Gemfile:
|
|
10
25
|
|
|
11
26
|
```ruby
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
gem "declarative_initialization"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then install:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
In non-Bundler contexts, require it directly:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "declarative_initialization"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
18
43
|
|
|
19
|
-
|
|
44
|
+
```ruby
|
|
45
|
+
class UserGreeter
|
|
46
|
+
include DeclarativeInitialization
|
|
47
|
+
|
|
48
|
+
initialize_with :user
|
|
49
|
+
|
|
50
|
+
def call
|
|
51
|
+
"Hello, #{user.name}!"
|
|
52
|
+
end
|
|
20
53
|
end
|
|
54
|
+
|
|
55
|
+
UserGreeter.new(user: current_user).call
|
|
56
|
+
# => "Hello, Alice!"
|
|
57
|
+
|
|
58
|
+
UserGreeter.new
|
|
59
|
+
# ArgumentError: [UserGreeter] Missing keyword argument(s): user
|
|
60
|
+
|
|
61
|
+
UserGreeter.new(user: current_user, extra: true)
|
|
62
|
+
# ArgumentError: [UserGreeter] Unknown keyword argument(s): extra
|
|
21
63
|
```
|
|
22
64
|
|
|
23
|
-
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Required vs optional keywords (defaults)
|
|
68
|
+
|
|
69
|
+
Declare required keywords as symbols, and optional keywords as keyword arguments:
|
|
24
70
|
|
|
25
71
|
```ruby
|
|
26
|
-
class
|
|
72
|
+
class Search
|
|
27
73
|
include DeclarativeInitialization
|
|
28
74
|
|
|
29
|
-
initialize_with :
|
|
75
|
+
initialize_with :query, limit: 10, order: :desc
|
|
76
|
+
|
|
77
|
+
def call
|
|
78
|
+
results = perform_search(query).take(limit)
|
|
79
|
+
order == :desc ? results.reverse : results
|
|
80
|
+
end
|
|
30
81
|
end
|
|
82
|
+
|
|
83
|
+
Search.new(query: "ruby").call
|
|
84
|
+
Search.new(query: "ruby", limit: 50).call
|
|
31
85
|
```
|
|
32
|
-
## Quick note on naming
|
|
33
86
|
|
|
34
|
-
|
|
87
|
+
### Post-initialize hook
|
|
35
88
|
|
|
36
|
-
|
|
89
|
+
Pass a block to `initialize_with` to run code after assignments. The block runs in the instance context.
|
|
37
90
|
|
|
38
|
-
|
|
91
|
+
```ruby
|
|
92
|
+
class Rectangle
|
|
93
|
+
include DeclarativeInitialization
|
|
94
|
+
|
|
95
|
+
initialize_with :width, :height do
|
|
96
|
+
raise ArgumentError, "Dimensions must be positive" if width <= 0 || height <= 0
|
|
97
|
+
@area = width * height
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
attr_reader :area
|
|
101
|
+
end
|
|
102
|
+
```
|
|
39
103
|
|
|
40
|
-
###
|
|
104
|
+
### Capturing a block passed to `.new`
|
|
41
105
|
|
|
42
|
-
|
|
106
|
+
If the caller passes a block to `.new`, it’s stored in `@block` and available via the `block` reader.
|
|
43
107
|
|
|
44
|
-
|
|
108
|
+
```ruby
|
|
109
|
+
class Wrapper
|
|
110
|
+
include DeclarativeInitialization
|
|
45
111
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
112
|
+
initialize_with :tag
|
|
113
|
+
|
|
114
|
+
def render
|
|
115
|
+
"<#{tag}>#{block&.call}</#{tag}>"
|
|
49
116
|
end
|
|
50
|
-
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Wrapper.new(tag: "div") { "Content" }.render
|
|
120
|
+
# => "<div>Content</div>"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Behavior notes / gotchas
|
|
51
124
|
|
|
52
|
-
###
|
|
125
|
+
### Only keyword arguments are accepted
|
|
53
126
|
|
|
54
|
-
|
|
127
|
+
The generated initializer is keyword-only. Passing positional arguments raises an `ArgumentError`.
|
|
55
128
|
|
|
56
|
-
|
|
129
|
+
### Readers are public by default
|
|
57
130
|
|
|
58
|
-
|
|
131
|
+
Inputs are exposed with `attr_reader`. If you prefer private readers, make them private after the declaration:
|
|
59
132
|
|
|
60
|
-
|
|
133
|
+
```ruby
|
|
134
|
+
class Example
|
|
135
|
+
include DeclarativeInitialization
|
|
136
|
+
initialize_with :user, admin: false
|
|
61
137
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
* Workaround:
|
|
67
|
-
```ruby
|
|
68
|
-
initialize_with :user, company: nil do
|
|
69
|
-
@company ||= @user.employer
|
|
70
|
-
end
|
|
71
|
-
```
|
|
138
|
+
private :user, :admin
|
|
139
|
+
end
|
|
140
|
+
```
|
|
72
141
|
|
|
73
|
-
|
|
74
|
-
> implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly.
|
|
142
|
+
### Defaults are literal values
|
|
75
143
|
|
|
76
|
-
|
|
144
|
+
Defaults are applied when the caller omits that keyword. For common mutable defaults (`Array`, `Hash`, `Set`, `String`), the value is duplicated per instance (shallow). If you need deeper setup (or derived values), use the post-initialize block.
|
|
77
145
|
|
|
78
|
-
|
|
79
|
-
```ruby
|
|
80
|
-
initialize_with :foo do
|
|
81
|
-
super(bar: 123)
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
* Workaround _possible_ (but really, probably more understandable to just fall back to manually writing `def initialize`):
|
|
85
|
-
```ruby
|
|
86
|
-
initialize_with :foo do
|
|
87
|
-
parent_initialize = method(:initialize).super_method
|
|
88
|
-
parent_initialize.call(bar: 123)
|
|
89
|
-
end
|
|
90
|
-
```
|
|
146
|
+
### Method name conflicts
|
|
91
147
|
|
|
92
|
-
|
|
148
|
+
`initialize_with` defines readers for each declared input (and `block`). If a method with the same name already exists, it will be overridden.
|
|
93
149
|
|
|
94
|
-
|
|
150
|
+
In Rails development/test (or when your logger level allows it), the gem logs a warning when it overrides an existing method.
|
|
95
151
|
|
|
96
|
-
|
|
152
|
+
### Referencing other inputs in defaults
|
|
97
153
|
|
|
98
|
-
|
|
154
|
+
You can’t reference one declared input from another input’s default at declaration time:
|
|
99
155
|
|
|
100
|
-
|
|
156
|
+
```ruby
|
|
157
|
+
initialize_with :user, account: user.account # user is not available here
|
|
158
|
+
```
|
|
101
159
|
|
|
102
|
-
|
|
160
|
+
Use the post-initialize block instead:
|
|
103
161
|
|
|
104
|
-
|
|
162
|
+
```ruby
|
|
163
|
+
initialize_with :user, account: nil do
|
|
164
|
+
@account ||= user.account
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Inheritance and `super`
|
|
169
|
+
|
|
170
|
+
`initialize_with` generates an `initialize` method. If a subclass calls `initialize_with`, it replaces the parent initializer and **does not call `super`**. Prefer a single initializer per hierarchy, or avoid this gem for complex inheritance chains.
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
105
173
|
|
|
106
|
-
|
|
174
|
+
- **Source**: [teamshares/declarative_initialization](https://github.com/teamshares/declarative_initialization)
|
|
175
|
+
- **Code of conduct**: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
|
data/Rakefile
CHANGED
|
@@ -10,3 +10,7 @@ require "rubocop/rake_task"
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
12
12
|
task default: %i[spec rubocop]
|
|
13
|
+
|
|
14
|
+
# Require default to pass before release. This relies on the default gem release task
|
|
15
|
+
# (from bundler/gem_tasks) depending on "build"; default runs before build, so before push.
|
|
16
|
+
Rake::Task["build"].enhance([:default])
|
|
@@ -1,74 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "set"
|
|
4
4
|
|
|
5
5
|
module DeclarativeInitialization
|
|
6
6
|
module ClassMethods
|
|
7
7
|
# Defines an initializer expecting the specified keyword arguments.
|
|
8
8
|
# @param args [Array<Symbol>] Required keyword arguments
|
|
9
|
-
# @param kwargs [Hash<Symbol, Object>] Optional keyword arguments
|
|
10
|
-
# @param
|
|
11
|
-
def initialize_with(*args, **kwargs, &
|
|
9
|
+
# @param kwargs [Hash<Symbol, Object>] Optional keyword arguments with default values
|
|
10
|
+
# @param post_initialize [Proc] Block to execute after initialization (optional)
|
|
11
|
+
def initialize_with(*args, **kwargs, &post_initialize)
|
|
12
12
|
declared = args + kwargs.keys
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
_define_initializer(declared, kwargs, post_initialize_block)
|
|
13
|
+
Internal.validate_arguments!(self, declared)
|
|
14
|
+
declared.each { |key| _define_reader(key) }
|
|
15
|
+
_define_reader(:block, block_reader: true)
|
|
16
|
+
_define_generated_initializer(declared, kwargs, post_initialize)
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
private
|
|
21
20
|
|
|
22
|
-
def
|
|
23
|
-
|
|
21
|
+
def _declared_readers
|
|
22
|
+
@_declared_readers ||= Set.new
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
logger = Logger.new($stdout)
|
|
31
|
-
logger.level = Logger::WARN
|
|
32
|
-
logger
|
|
33
|
-
end
|
|
25
|
+
def _ancestor_declared_reader?(key)
|
|
26
|
+
ancestors.drop(1).any? do |ancestor|
|
|
27
|
+
ancestor.instance_variable_get(:@_declared_readers)&.include?(key)
|
|
28
|
+
end
|
|
34
29
|
end
|
|
35
30
|
|
|
36
|
-
def
|
|
37
|
-
return if
|
|
31
|
+
def _define_reader(key, block_reader: false)
|
|
32
|
+
return if _declared_readers.include?(key)
|
|
33
|
+
return if _ancestor_declared_reader?(key)
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def _set_up_attribute_readers(declared)
|
|
43
|
-
declared.each do |key|
|
|
44
|
-
if method_defined?(key)
|
|
45
|
-
_logger.warn "[#{_class_name}] Method ##{key} already exists -- skipping attr_reader generation"
|
|
46
|
-
else
|
|
47
|
-
attr_reader key
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
35
|
+
Internal.warn_override(self, key, block_reader: block_reader) if method_defined?(key)
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_logger.warn "[#{_class_name}] Method #block already exists -- may NOT be able to reference a block " \
|
|
55
|
-
"passed to #new as #block (use @block instead)"
|
|
56
|
-
else
|
|
57
|
-
attr_reader :block
|
|
58
|
-
end
|
|
37
|
+
_declared_readers.add(key)
|
|
38
|
+
attr_reader key
|
|
59
39
|
end
|
|
60
40
|
|
|
61
|
-
def
|
|
41
|
+
def _define_generated_initializer(declared, defaults, post_initialize)
|
|
62
42
|
define_method(:initialize) do |*given_args, **given_kwargs, &given_block|
|
|
63
|
-
|
|
64
|
-
_validate_initialization_arguments!(class_name, given_args, given_kwargs, declared, defaults)
|
|
43
|
+
Internal.validate_initialization_arguments!(self.class, given_args, given_kwargs, declared, defaults)
|
|
65
44
|
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
defaults.each do |key, value|
|
|
46
|
+
next if given_kwargs.key?(key)
|
|
47
|
+
|
|
48
|
+
instance_variable_set(:"@#{key}", Internal.copy_default(value))
|
|
68
49
|
end
|
|
69
50
|
|
|
51
|
+
given_kwargs.each { |key, value| instance_variable_set(:"@#{key}", value) }
|
|
52
|
+
|
|
70
53
|
instance_variable_set(:@block, given_block) if given_block
|
|
71
|
-
instance_exec(&
|
|
54
|
+
instance_exec(&post_initialize) if post_initialize
|
|
72
55
|
end
|
|
73
56
|
end
|
|
74
57
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module DeclarativeInitialization
|
|
7
|
+
# Internal helpers that don't need to be injected into user classes.
|
|
8
|
+
# All methods are module functions - stateless and callable as Internal.method_name
|
|
9
|
+
module Internal
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def logger
|
|
13
|
+
@logger ||= if defined?(Rails) && Rails.respond_to?(:logger)
|
|
14
|
+
Rails.logger
|
|
15
|
+
else
|
|
16
|
+
Logger.new($stdout).tap { |l| l.level = Logger::WARN }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def display_name(klass)
|
|
21
|
+
klass.name || "Anonymous Class"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def format_message(klass, message)
|
|
25
|
+
"[#{display_name(klass)}] #{message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate_arguments!(klass, declared)
|
|
29
|
+
return if declared.all?(Symbol)
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, format_message(klass, "All arguments to #initialize_with must be symbols")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_initialization_arguments!(klass, given_args, given_kwargs, declared, defaults)
|
|
35
|
+
raise ArgumentError, format_message(klass, "Only keyword arguments are accepted") unless given_args.empty?
|
|
36
|
+
|
|
37
|
+
missing = declared - given_kwargs.keys - defaults.keys
|
|
38
|
+
unless missing.empty?
|
|
39
|
+
raise ArgumentError, format_message(klass, "Missing keyword argument(s): #{missing.join(", ")}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
extra = given_kwargs.keys - declared
|
|
43
|
+
return if extra.empty?
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, format_message(klass, "Unknown keyword argument(s): #{extra.join(", ")}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warn_override(klass, key, block_reader:)
|
|
49
|
+
return unless warn_override?
|
|
50
|
+
|
|
51
|
+
location = override_location(klass, key)
|
|
52
|
+
reader_type = block_reader ? "block" : "init-arg"
|
|
53
|
+
logger.warn format_message(klass,
|
|
54
|
+
"Method ##{key} already exists #{location} -- overriding with #{reader_type} reader")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def warn_override?
|
|
58
|
+
return true if defined?(Rails) && Rails.respond_to?(:env) && (Rails.env.development? || Rails.env.test?)
|
|
59
|
+
|
|
60
|
+
logger.level <= Logger::DEBUG
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def override_location(klass, key)
|
|
64
|
+
return "on this class" if klass.method_defined?(key, false)
|
|
65
|
+
|
|
66
|
+
owner = klass.instance_method(key).owner
|
|
67
|
+
"in #{owner.name || "an anonymous ancestor"}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Defensive copy for common mutable default values.
|
|
71
|
+
#
|
|
72
|
+
# Defaults passed to `initialize_with` are created once at class definition
|
|
73
|
+
# time. Without copying, `[]` / `{}` / `Set.new` defaults can be shared across
|
|
74
|
+
# instances and accidentally mutated.
|
|
75
|
+
#
|
|
76
|
+
# This is intentionally shallow, and only for common core mutable types.
|
|
77
|
+
def copy_default(value)
|
|
78
|
+
return value if value.nil? || value.frozen?
|
|
79
|
+
|
|
80
|
+
case value
|
|
81
|
+
when Array, Hash, Set, String
|
|
82
|
+
value.dup
|
|
83
|
+
else
|
|
84
|
+
value
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "declarative_initialization/version"
|
|
4
|
+
require_relative "declarative_initialization/internal"
|
|
4
5
|
require_relative "declarative_initialization/class_methods"
|
|
5
|
-
require_relative "declarative_initialization/instance_methods"
|
|
6
6
|
|
|
7
7
|
module DeclarativeInitialization
|
|
8
8
|
def self.included(base)
|
|
9
|
-
base.
|
|
10
|
-
include InstanceMethods
|
|
11
|
-
extend ClassMethods
|
|
12
|
-
end
|
|
9
|
+
base.extend ClassMethods
|
|
13
10
|
end
|
|
14
11
|
end
|
|
15
12
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
def self.included(base)
|
|
19
|
-
base.class_eval do
|
|
20
|
-
include DeclarativeInitialization
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
13
|
+
# Alias so you can also do `include InitializeWith`
|
|
14
|
+
InitializeWith = DeclarativeInitialization
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: declarative_initialization
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kali Donovan
|
|
@@ -26,7 +26,7 @@ files:
|
|
|
26
26
|
- Rakefile
|
|
27
27
|
- lib/declarative_initialization.rb
|
|
28
28
|
- lib/declarative_initialization/class_methods.rb
|
|
29
|
-
- lib/declarative_initialization/
|
|
29
|
+
- lib/declarative_initialization/internal.rb
|
|
30
30
|
- lib/declarative_initialization/version.rb
|
|
31
31
|
homepage: https://github.com/teamshares/declarative_initialization
|
|
32
32
|
licenses: []
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module DeclarativeInitialization
|
|
4
|
-
module InstanceMethods
|
|
5
|
-
private
|
|
6
|
-
|
|
7
|
-
def _validate_initialization_arguments!(class_name, given_args, given_kwargs, declared, defaults)
|
|
8
|
-
raise ArgumentError, "[#{class_name}] Only keyword arguments are accepted" unless given_args.empty?
|
|
9
|
-
|
|
10
|
-
missing = declared - given_kwargs.keys - defaults.keys
|
|
11
|
-
extra = given_kwargs.keys - declared
|
|
12
|
-
|
|
13
|
-
raise ArgumentError, "[#{class_name}] Missing keyword argument(s): #{missing.join(", ")}" unless missing.empty?
|
|
14
|
-
raise ArgumentError, "[#{class_name}] Unknown keyword argument(s): #{extra.join(", ")}" unless extra.empty?
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|