philiprehberger-struct_kit 0.2.0 → 0.3.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/CHANGELOG.md +15 -1
- data/README.md +47 -1
- data/lib/philiprehberger/struct_kit/definition.rb +18 -2
- data/lib/philiprehberger/struct_kit/field.rb +10 -0
- data/lib/philiprehberger/struct_kit/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0152f7975b6372edce049e3983a3d488a1405514732dc49d0d18c2f650f5cb5
|
|
4
|
+
data.tar.gz: 9edcdd291201087f68ee11f08ca400d8fdfaac7403bdf8368ebec4dcee18ffa2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 923c971ee52f8e015b593e54f0cbb091522b05f2cd9ab4ef96a8ec980626c4785bddaf1d5ac5bed199d3f6ae2b25d792ed12319eb63cf7f9a0456724a07827c8
|
|
7
|
+
data.tar.gz: a24215634ee0dd5302f446821d2142a1e426a81a31c7597936f305a44b59f0a00c25b5481dd3b01ad43e1362f95d477231683e2bc385f00271a1ee2e363f610b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `#with(**changes)` instance method for non-destructive updates
|
|
14
|
+
- `#to_a` instance method returning field values in declaration order
|
|
15
|
+
- `.field_names` class method for introspecting declared fields
|
|
16
|
+
- `presence:` option on `validate` DSL to reject nil/empty values
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `spec.files` glob narrowed to `lib/**/*.rb` to match gemspec template
|
|
20
|
+
- `required_ruby_version` normalized to `>= 3.1.0` to match gemspec template
|
|
21
|
+
|
|
10
22
|
## [0.2.0] - 2026-04-04
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -93,5 +105,7 @@ and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
93
105
|
- Value equality via `#==`
|
|
94
106
|
- Keyword-only constructor
|
|
95
107
|
|
|
96
|
-
[Unreleased]: https://github.com/philiprehberger/rb-struct-kit/compare/v0.
|
|
108
|
+
[Unreleased]: https://github.com/philiprehberger/rb-struct-kit/compare/v0.3.0...HEAD
|
|
109
|
+
[0.3.0]: https://github.com/philiprehberger/rb-struct-kit/compare/v0.2.0...v0.3.0
|
|
110
|
+
[0.2.0]: https://github.com/philiprehberger/rb-struct-kit/compare/v0.1.0...v0.2.0
|
|
97
111
|
[0.1.0]: https://github.com/philiprehberger/rb-struct-kit/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -124,6 +124,49 @@ in { role: :user }
|
|
|
124
124
|
end
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Non-destructive Updates
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
require "philiprehberger/struct_kit"
|
|
131
|
+
|
|
132
|
+
User = Philiprehberger::StructKit.define do
|
|
133
|
+
field :name, String
|
|
134
|
+
field :age, Integer, default: 0
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
alice = User.new(name: 'Alice', age: 30)
|
|
138
|
+
older = alice.with(age: 31)
|
|
139
|
+
|
|
140
|
+
alice.age # => 30 (unchanged)
|
|
141
|
+
older.age # => 31
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Presence Validation
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
Account = Philiprehberger::StructKit.define do
|
|
148
|
+
field :email, String
|
|
149
|
+
field :tags, Array, default: -> { [] }
|
|
150
|
+
validate :email, presence: true
|
|
151
|
+
validate :tags, presence: true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Account.new(email: '', tags: ['a']) # ArgumentError: email must be present
|
|
155
|
+
Account.new(email: 'a@b', tags: []) # ArgumentError: tags must be present
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Introspection
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
User = Philiprehberger::StructKit.define do
|
|
162
|
+
field :name, String
|
|
163
|
+
field :age, Integer, default: 0
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
User.field_names # => [:name, :age]
|
|
167
|
+
User.new(name: 'Alice', age: 30).to_a # => ["Alice", 30]
|
|
168
|
+
```
|
|
169
|
+
|
|
127
170
|
## API
|
|
128
171
|
|
|
129
172
|
### `Philiprehberger::StructKit.define(mutable: false, &block)`
|
|
@@ -135,14 +178,16 @@ Define a new struct class. Evaluates the block in DSL context.
|
|
|
135
178
|
| Method | Description |
|
|
136
179
|
|--------|-------------|
|
|
137
180
|
| `field(name, type = nil, default: UNSET, coerce: nil)` | Declare a typed field with optional default and coercion |
|
|
138
|
-
| `validate(name, range: nil, format: nil, &block)` | Add validation rule to a field |
|
|
181
|
+
| `validate(name, range: nil, format: nil, presence: nil, &block)` | Add validation rule to a field |
|
|
139
182
|
|
|
140
183
|
### Instance Methods
|
|
141
184
|
|
|
142
185
|
| Method | Description |
|
|
143
186
|
|--------|-------------|
|
|
144
187
|
| `#to_h` | Convert to a plain hash |
|
|
188
|
+
| `#to_a` | Convert to an array of values in field-declaration order |
|
|
145
189
|
| `#to_json` | Convert to JSON string |
|
|
190
|
+
| `#with(**changes)` | Return a new instance with the given fields changed |
|
|
146
191
|
| `#deconstruct_keys(keys)` | Pattern matching support |
|
|
147
192
|
| `#==` | Value equality |
|
|
148
193
|
| `#inspect` | Human-readable string representation |
|
|
@@ -152,6 +197,7 @@ Define a new struct class. Evaluates the block in DSL context.
|
|
|
152
197
|
| Method | Description |
|
|
153
198
|
|--------|-------------|
|
|
154
199
|
| `.from_h(hash)` | Construct from hash (string or symbol keys) |
|
|
200
|
+
| `.field_names` | Return the declared field names in order |
|
|
155
201
|
|
|
156
202
|
## Development
|
|
157
203
|
|
|
@@ -15,16 +15,17 @@ module Philiprehberger
|
|
|
15
15
|
@fields[name] = Field.new(name, type, default: default, coerce: coerce)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def validate(field_name, range: nil, format: nil, &block)
|
|
18
|
+
def validate(field_name, range: nil, format: nil, presence: nil, &block)
|
|
19
19
|
@validations[field_name] ||= []
|
|
20
20
|
rules = {}
|
|
21
21
|
rules[:range] = range if range
|
|
22
22
|
rules[:format] = format if format
|
|
23
|
+
rules[:presence] = presence unless presence.nil?
|
|
23
24
|
@validations[field_name] << rules unless rules.empty?
|
|
24
25
|
@validations[field_name] << block if block
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def build
|
|
28
|
+
def build # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
28
29
|
fields = @fields.dup
|
|
29
30
|
mutable = @mutable
|
|
30
31
|
|
|
@@ -83,16 +84,31 @@ module Philiprehberger
|
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
define_method(:to_a) do
|
|
88
|
+
self.class._fields.keys.map { |fname| instance_variable_get(:"@#{fname}") }
|
|
89
|
+
end
|
|
90
|
+
|
|
86
91
|
define_method(:to_json) do |*args|
|
|
87
92
|
require 'json'
|
|
88
93
|
to_h.to_json(*args)
|
|
89
94
|
end
|
|
90
95
|
|
|
96
|
+
define_method(:with) do |**changes|
|
|
97
|
+
unknown = changes.keys - self.class._fields.keys
|
|
98
|
+
raise ArgumentError, "unknown keyword: #{unknown.first}" unless unknown.empty?
|
|
99
|
+
|
|
100
|
+
self.class.new(**to_h, **changes)
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
define_singleton_method(:from_h) do |hash|
|
|
92
104
|
sym_hash = hash.transform_keys(&:to_sym)
|
|
93
105
|
new(**sym_hash)
|
|
94
106
|
end
|
|
95
107
|
|
|
108
|
+
define_singleton_method(:field_names) do
|
|
109
|
+
_fields.keys
|
|
110
|
+
end
|
|
111
|
+
|
|
96
112
|
define_method(:deconstruct_keys) do |keys|
|
|
97
113
|
h = to_h
|
|
98
114
|
keys ? h.slice(*keys) : h
|
|
@@ -52,6 +52,7 @@ module Philiprehberger
|
|
|
52
52
|
when Hash
|
|
53
53
|
errors << "#{@name} must be in range #{rule[:range]}" if rule[:range] && !rule[:range].include?(value)
|
|
54
54
|
errors << "#{@name} does not match required format" if rule[:format] && !rule[:format].match?(value.to_s)
|
|
55
|
+
errors << "#{@name} must be present" if rule[:presence] && blank?(value)
|
|
55
56
|
when Proc
|
|
56
57
|
msg = rule.call(value)
|
|
57
58
|
errors << msg if msg.is_a?(String)
|
|
@@ -60,6 +61,15 @@ module Philiprehberger
|
|
|
60
61
|
|
|
61
62
|
errors
|
|
62
63
|
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def blank?(value)
|
|
68
|
+
return true if value.nil?
|
|
69
|
+
return true if value.respond_to?(:empty?) && value.empty?
|
|
70
|
+
|
|
71
|
+
false
|
|
72
|
+
end
|
|
63
73
|
end
|
|
64
74
|
end
|
|
65
75
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-struct_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Define data classes with typed fields, default values, validation rules,
|
|
14
14
|
and pattern matching support. Immutable by default with keyword-only construction,
|
|
@@ -43,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version:
|
|
46
|
+
version: 3.1.0
|
|
47
47
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
48
|
requirements:
|
|
49
49
|
- - ">="
|