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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcc66136d59c7eb3cb37b487a849582ec4f2cd9c322a60dc924402aa380f1d7e
4
- data.tar.gz: ad8bc697727169695feb41cc1dc37efcab5a3401e1190944e831f9612f252c1b
3
+ metadata.gz: a0152f7975b6372edce049e3983a3d488a1405514732dc49d0d18c2f650f5cb5
4
+ data.tar.gz: 9edcdd291201087f68ee11f08ca400d8fdfaac7403bdf8368ebec4dcee18ffa2
5
5
  SHA512:
6
- metadata.gz: 747aeabf61174e81aff1f9792b8b3d4fd2044a201805716c0eac054ee33385bd86552c75c1d98177d61e13f94d55e6af581a84f6033cd6110cf235e7b09a7a0b
7
- data.tar.gz: 6c60682cdcf912430453471cfd9ddba394a06110d9edcb8f44e40fd9e856fb5b0467cafa62792809f8f07035c44e66d13878a697a67f8d86db4bcfe4b80d69d7
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.1.0...HEAD
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module StructKit
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  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.2.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-05 00:00:00.000000000 Z
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: '3.1'
46
+ version: 3.1.0
47
47
  required_rubygems_version: !ruby/object:Gem::Requirement
48
48
  requirements:
49
49
  - - ">="