adaptiveconfiguration 1.0.0.beta01
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +559 -0
- data/adaptiveconfiguration.gemspec +37 -0
- data/lib/adaptive_configuration/builder.rb +61 -0
- data/lib/adaptive_configuration/configurable.rb +20 -0
- data/lib/adaptive_configuration/context.rb +170 -0
- data/lib/adaptive_configuration/group_builder.rb +42 -0
- data/lib/adaptive_configuration.rb +2 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 40a0689cda0cd8ceb25859afd3c06379667fb296d3a2a90dbf027f00d8fb6454
|
4
|
+
data.tar.gz: cf253ace46d9f2d634a9b578653c16a12e6909c517c53cd19d0b584e17111819
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f2af0a82072944057dca43e2c4a294c42cdb140e1433e2fdedd7d353e92df0d97e6e9736b45e00d7e391d7f7bb40336b6b631626e972f7d1e4d10888b3abf0b9
|
7
|
+
data.tar.gz: 04a3bee03bffff23ab8b13157b2e5c7fd589d435f18870f709b4c176342d402fdf6f9bf16ed4734fc7172e5d07385808fda56fcf36287fd170bf5799d8af6a1a
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Endless International
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,559 @@
|
|
1
|
+
# AdaptiveConfiguration
|
2
|
+
|
3
|
+
**AdaptiveConfiguration** is a powerful and flexible Ruby gem that allows you to define a DSL
|
4
|
+
(Domain-Specific Language) for structured and hierarchical configurations. It is ideal for defining
|
5
|
+
complex configurations for various use cases, such as API clients, application settings, or
|
6
|
+
any scenario where structured configuration is needed.
|
7
|
+
|
8
|
+
In addition AdaptiveConfiguration can be more generally used to transform and validate JSON data
|
9
|
+
from any source such as network request or API reponse.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'adaptiveconfiguration'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
gem install adaptiveconfiguration
|
29
|
+
```
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Requiring the Gem
|
34
|
+
|
35
|
+
To start using the `adaptiveconfiguration` gem, simply require it in your Ruby application:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require 'adaptiveconfiguration'
|
39
|
+
```
|
40
|
+
|
41
|
+
### Defining Configurations with AdaptiveConfiguration
|
42
|
+
|
43
|
+
AdaptiveConfiguration permits the caller to define a domain specific language +Builder+
|
44
|
+
specifying parameters, parameter collections, and related options. This builder can then
|
45
|
+
be used to build or validate the configuration using the domain specific language.
|
46
|
+
|
47
|
+
---
|
48
|
+
|
49
|
+
## Parameters
|
50
|
+
|
51
|
+
**Parameters** are the basic building blocks of your configuration. They represent individual
|
52
|
+
settings or options that you can define with specific types, defaults, and other attributes.
|
53
|
+
|
54
|
+
When defining a parameter, you specify its name and optionally type, default and alias.
|
55
|
+
|
56
|
+
### Example:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
require 'adaptiveconfiguration'
|
60
|
+
|
61
|
+
# define a configuration structure with parameters
|
62
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
63
|
+
parameter :api_key, String
|
64
|
+
parameter :version, String, default: '1.0'
|
65
|
+
end
|
66
|
+
|
67
|
+
# build the configuration and set values
|
68
|
+
result = configuration.build! do
|
69
|
+
api_key 'your-api-key'
|
70
|
+
end
|
71
|
+
|
72
|
+
# access the configuration values
|
73
|
+
puts result[:api_key] # => "your-api-key"
|
74
|
+
puts result[:version] # => "1.0"
|
75
|
+
```
|
76
|
+
|
77
|
+
**Notes:**
|
78
|
+
|
79
|
+
- **Defining Parameters:**
|
80
|
+
- `parameter :api_key, String` defines a parameter named `:api_key` with the type `String`.
|
81
|
+
- `parameter :version, String, default: '1.0'` defines a parameter with a default value.
|
82
|
+
- **Building the Configuration:**
|
83
|
+
- `configuration.build!` creates a context where you can set values for the parameters.
|
84
|
+
- Inside the block, `api_key 'your-api-key'` sets the value of `:api_key`.
|
85
|
+
- **Accessing Values:**
|
86
|
+
- `result[:api_key]` retrieves the value of `:api_key`.
|
87
|
+
- If a parameter has a default and you don't set it, it uses the default value.
|
88
|
+
|
89
|
+
---
|
90
|
+
|
91
|
+
## Groups
|
92
|
+
|
93
|
+
**Groups** allow you to organize related parameters into nested structures. This is useful for
|
94
|
+
logically grouping settings and creating hierarchical configurations.
|
95
|
+
|
96
|
+
### Example:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
require 'adaptiveconfiguration'
|
100
|
+
|
101
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
102
|
+
parameter :api_key, String
|
103
|
+
group :chat_options do
|
104
|
+
parameter :model, String, default: 'claude-3'
|
105
|
+
parameter :max_tokens, Integer, default: 1000
|
106
|
+
parameter :temperature, Float, default: 0.5
|
107
|
+
parameter :stream, TrueClass
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
result = configuration.build! do
|
112
|
+
api_key 'your-api-key'
|
113
|
+
chat_options do
|
114
|
+
temperature 0.8
|
115
|
+
stream true
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Accessing values
|
120
|
+
puts result[:api_key] # => "your-api-key"
|
121
|
+
puts result[:chat_options][:model] # => "claude-3"
|
122
|
+
puts result[:chat_options][:temperature] # => 0.8
|
123
|
+
puts result[:chat_options][:stream] # => true
|
124
|
+
```
|
125
|
+
|
126
|
+
**Notes:**
|
127
|
+
|
128
|
+
- **Defining a Group:**
|
129
|
+
- `group :chat_options do ... end` defines a group named `:chat_options`.
|
130
|
+
- Inside the group, you can define parameters that belong to that group.
|
131
|
+
- **Setting Values in Groups:**
|
132
|
+
- In the build block, you can set values for parameters within groups by nesting blocks.
|
133
|
+
- `chat_options do ... end` allows you to set parameters inside the `:chat_options` group.
|
134
|
+
- **Accessing Values:**
|
135
|
+
- You access group parameters by chaining the keys: `result[:chat_options][:model]`.
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
## Array Parameters
|
140
|
+
|
141
|
+
**Array Parameters** allow you to define parameters that can hold multiple values. This is
|
142
|
+
useful when you need to collect lists of items, such as headers, tags, or multiple
|
143
|
+
configuration entries.
|
144
|
+
|
145
|
+
### Example:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
require 'adaptiveconfiguration'
|
149
|
+
|
150
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
151
|
+
parameter :api_key, String
|
152
|
+
group :request_options do
|
153
|
+
parameter :headers, String, array: true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
result = configuration.build! do
|
158
|
+
api_key 'your-api-key'
|
159
|
+
request_options do
|
160
|
+
headers ['Content-Type: application/json', 'Authorization: Bearer token']
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Accessing array parameter
|
165
|
+
puts result[:request_options][:headers]
|
166
|
+
# => ["Content-Type: application/json", "Authorization: Bearer token"]
|
167
|
+
```
|
168
|
+
|
169
|
+
**Notes:**
|
170
|
+
|
171
|
+
- **Defining an Array Parameter:**
|
172
|
+
- `parameter :headers, String, array: true` defines `:headers` as an array parameter of type `String`.
|
173
|
+
- **Setting Values:**
|
174
|
+
- You can assign an array of values to `headers`.
|
175
|
+
- **Accessing Values:**
|
176
|
+
- The values are stored as an array, and you can access them directly.
|
177
|
+
|
178
|
+
---
|
179
|
+
|
180
|
+
## The `:as` Option
|
181
|
+
|
182
|
+
The `:as` option allows you to map the parameter's name in your DSL to a different key in the
|
183
|
+
resulting configuration. This is useful when you need to conform to specific key names required
|
184
|
+
by APIs or other systems, while still using friendly method names in your configuration blocks.
|
185
|
+
|
186
|
+
### Example:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
require 'adaptiveconfiguration'
|
190
|
+
|
191
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
192
|
+
parameter :api_key, String, as: :apiKey
|
193
|
+
group :user_settings, as: :userSettings do
|
194
|
+
parameter :user_name, String, as: :userName
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
result = configuration.build! do
|
199
|
+
api_key 'your-api-key'
|
200
|
+
user_settings do
|
201
|
+
user_name 'john_doe'
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Accessing values with mapped keys
|
206
|
+
puts result[:apiKey] # => "your-api-key"
|
207
|
+
puts result[:userSettings][:userName] # => "john_doe"
|
208
|
+
```
|
209
|
+
|
210
|
+
**Notes:**
|
211
|
+
|
212
|
+
- **Using the `:as` Option:**
|
213
|
+
- `parameter :apiKey, String, as: :api_key` defines a parameter that you set using `apiKey`,
|
214
|
+
but it's stored as `:api_key` in the result.
|
215
|
+
- Similarly, `group :userSettings, as: :user_settings` maps the group name.
|
216
|
+
- **Setting Values:**
|
217
|
+
- In the build block, you use the original names (`apiKey`, `userSettings`), but the values
|
218
|
+
are stored under the mapped keys.
|
219
|
+
- **Accessing Values:**
|
220
|
+
- You access the values using the mapped keys (`:api_key`, `:user_settings`, `:user_name`).
|
221
|
+
|
222
|
+
---
|
223
|
+
|
224
|
+
## Default Values
|
225
|
+
|
226
|
+
The `default` option allows you to specify a default value for a parameter. If you do not set
|
227
|
+
a value for that parameter during the build phase, it automatically uses the default.
|
228
|
+
|
229
|
+
### Example:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
require 'adaptiveconfiguration'
|
233
|
+
|
234
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
235
|
+
parameter :api_key, String, default: 'default-api-key'
|
236
|
+
group :settings do
|
237
|
+
parameter :timeout, Integer, default: 30
|
238
|
+
parameter :retries, Integer, default: 3
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
result = configuration.build! do
|
243
|
+
# No need to set api_key or settings parameters unless you want to override defaults
|
244
|
+
end
|
245
|
+
|
246
|
+
# Accessing default values
|
247
|
+
puts result[:api_key] # => "default-api-key"
|
248
|
+
puts result[:settings][:timeout] # => 30
|
249
|
+
puts result[:settings][:retries] # => 3
|
250
|
+
```
|
251
|
+
|
252
|
+
**Notes:**
|
253
|
+
|
254
|
+
- **Defining Defaults:**
|
255
|
+
- Parameters like `:api_key` have a default value specified with `default: 'default-api-key'`.
|
256
|
+
- **Building Without Setting Values:**
|
257
|
+
- If you do not provide values during the build phase, the defaults are used.
|
258
|
+
- **Accessing Values:**
|
259
|
+
- The result contains the default values for parameters you didn't set.
|
260
|
+
|
261
|
+
---
|
262
|
+
|
263
|
+
## Type Conversion
|
264
|
+
|
265
|
+
AdaptiveConfiguration automatically handles type conversion based on the parameter's specified
|
266
|
+
type. If you provide a value that can be converted to the specified type, it will do so.
|
267
|
+
If conversion fails, it raises a `TypeError`.
|
268
|
+
|
269
|
+
### Example:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
require 'adaptiveconfiguration'
|
273
|
+
|
274
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
275
|
+
parameter :max_tokens, Integer
|
276
|
+
parameter :temperature, Float
|
277
|
+
parameter :start_date, Date
|
278
|
+
end
|
279
|
+
|
280
|
+
result = configuration.build! do
|
281
|
+
max_tokens '1500' # String that can be converted to Integer
|
282
|
+
temperature '0.75' # String that can be converted to Float
|
283
|
+
start_date '2023-01-01' # String that can be converted to Date
|
284
|
+
end
|
285
|
+
|
286
|
+
# Accessing converted values
|
287
|
+
puts result[:max_tokens] # => 1500 (Integer)
|
288
|
+
puts result[:temperature] # => 0.75 (Float)
|
289
|
+
puts result[:start_date] # => #<Date: 2023-01-01 ...>
|
290
|
+
```
|
291
|
+
|
292
|
+
**Notes:**
|
293
|
+
|
294
|
+
- **Type Conversion:**
|
295
|
+
- AdaptiveConfiguration converts `'1500'` to `1500` (Integer).
|
296
|
+
- Converts `'0.75'` to `0.75` (Float).
|
297
|
+
- Converts `'2023-01-01'` to a `Date` object.
|
298
|
+
- **Error Handling:**
|
299
|
+
- If you provide a value that cannot be converted, it raises a `TypeError`.
|
300
|
+
|
301
|
+
---
|
302
|
+
|
303
|
+
## Custom Converters
|
304
|
+
|
305
|
+
You can define custom converters for your own types, allowing you to extend the framework's capabilities.
|
306
|
+
|
307
|
+
### Example:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
require 'adaptiveconfiguration'
|
311
|
+
|
312
|
+
# define a custom class
|
313
|
+
class UpcaseString < String
|
314
|
+
def initialize( value )
|
315
|
+
super( value.to_s.upcase )
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
320
|
+
convert( UpcaseString ) { | v | UpcaseString.new( v ) }
|
321
|
+
parameter :name, UpcaseString
|
322
|
+
end
|
323
|
+
|
324
|
+
result = configuration.build! do
|
325
|
+
name 'john doe'
|
326
|
+
end
|
327
|
+
|
328
|
+
# Accessing custom converted value
|
329
|
+
puts result[:name] # => "JOHN DOE"
|
330
|
+
puts result[:name].class # => UpcaseString
|
331
|
+
```
|
332
|
+
|
333
|
+
**Notes:**
|
334
|
+
|
335
|
+
- **Defining a Custom Converter:**
|
336
|
+
- `convert( UpcaseString ) { | v | UpcaseString.new( v ) }` tells the builder how to convert
|
337
|
+
values to `UpcaseString`.
|
338
|
+
- **Using Custom Types:**
|
339
|
+
- `parameter :name, UpcaseString` defines a parameter of the custom type.
|
340
|
+
- **Conversion Behavior:**
|
341
|
+
- When you set `name 'john doe'`, it converts it to `'JOHN DOE'` and stores it as an instance
|
342
|
+
of `UpcaseString`.
|
343
|
+
|
344
|
+
---
|
345
|
+
|
346
|
+
Certainly! Let's expand the first paragraph and complete the explanation in the section you added to the README.
|
347
|
+
|
348
|
+
---
|
349
|
+
|
350
|
+
## Transforming and Validating JSON Data
|
351
|
+
|
352
|
+
AdaptiveConfiguration can also be utilized to transform and validate JSON data. By defining parameters and groups
|
353
|
+
that mirror the expected structure of your JSON input, you can map and coerce incoming data into the desired format
|
354
|
+
seamlessly.
|
355
|
+
|
356
|
+
The `:as` option allows you to rename keys during this transformation process, ensuring that your data conforms to
|
357
|
+
specific API requirements or internal data models. Moreover, AdaptiveConfiguration provides built-in validation by
|
358
|
+
raising exceptions when the input data contains unexpected elements or elements of the wrong type, helping you
|
359
|
+
maintain data integrity and catch errors early in your data processing pipeline.
|
360
|
+
|
361
|
+
### Example:
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
require 'adaptiveconfiguration'
|
365
|
+
|
366
|
+
# Define the expected structure of the JSON data
|
367
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
368
|
+
parameter :api_key, String
|
369
|
+
group :user, as: :user_info do
|
370
|
+
parameter :name, String
|
371
|
+
parameter :email, String
|
372
|
+
parameter :age, Integer
|
373
|
+
end
|
374
|
+
group :preferences, as: :user_preferences do
|
375
|
+
parameter :notifications_enabled, TrueClass
|
376
|
+
parameter :theme, String, default: 'light'
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# sample JSON data as a Hash (e.g., parsed from JSON/YAML or API response
|
381
|
+
input_data = {
|
382
|
+
'api_key' => 'your-api-key',
|
383
|
+
'user' => {
|
384
|
+
'name' => 'John Doe',
|
385
|
+
'email' => 'john@example.com',
|
386
|
+
'age' => '30' # age is a String that should be converted to Integer
|
387
|
+
},
|
388
|
+
'preferences' => {
|
389
|
+
'notifications_enabled' => 'true', # Should be converted to TrueClass
|
390
|
+
'theme' => 'dark'
|
391
|
+
},
|
392
|
+
'extra_field' => 'unexpected' # This field is not defined in the configuration
|
393
|
+
}
|
394
|
+
|
395
|
+
# build the configuration context using the input data
|
396
|
+
begin
|
397
|
+
|
398
|
+
result = configuration.build!( input_data )
|
399
|
+
|
400
|
+
# Access transformed and validated data
|
401
|
+
puts result[:api_key] # => "your-api-key"
|
402
|
+
puts result[:user_info][:name] # => "John Doe"
|
403
|
+
puts result[:user_info][:age] # => 30 (Integer)
|
404
|
+
puts result[:user_preferences][:theme] # => "dark"
|
405
|
+
|
406
|
+
rescue TypeError => e
|
407
|
+
puts "Validation Error: #{e.message}"
|
408
|
+
end
|
409
|
+
```
|
410
|
+
|
411
|
+
**Explanation:**
|
412
|
+
|
413
|
+
- **Defining the Structure:**
|
414
|
+
- **Parameters and Groups:**
|
415
|
+
- We define a configuration that reflects the expected structure of the input JSON data.
|
416
|
+
- `parameter :api_key, String` defines the API key parameter.
|
417
|
+
- `group :user, as: :user_info` defines a group for user data, which will be transformed to the key `:user_info` in the result.
|
418
|
+
- Inside the `:user` group, we define parameters for `:name`, `:email`, and `:age`.
|
419
|
+
- `group :preferences, as: :user_preferences` defines a group for user preferences, transformed to `:user_preferences`.
|
420
|
+
- **Using the `:as` Option:**
|
421
|
+
- The `:as` option renames the group keys in the resulting configuration, allowing the internal DSL names to differ from the output keys.
|
422
|
+
|
423
|
+
- **Building the Configuration Context:**
|
424
|
+
- **Using `build!`:**
|
425
|
+
- We use the `build!` method to enforce strict validation. If any type coercion fails or if unexpected elements are present, it raises an exception.
|
426
|
+
- **Setting Values from Input Data:**
|
427
|
+
- We set the values by extracting them from the `input_data` hash.
|
428
|
+
- For example, `api_key input_data['api_key']` sets the `:api_key` parameter.
|
429
|
+
- Within the `user` and `preferences` groups, we set the nested parameters accordingly.
|
430
|
+
|
431
|
+
- **Type Conversion and Coercion:**
|
432
|
+
- **Automatic Conversion:**
|
433
|
+
- The gem attempts to coerce input values to the specified types.
|
434
|
+
- `'30'` (String) is converted to `30` (Integer) for the `:age` parameter.
|
435
|
+
- `'true'` (String) is converted to `true` (TrueClass) for `:notifications_enabled`.
|
436
|
+
- **Error Handling:**
|
437
|
+
- If the input value cannot be coerced to the specified type, a `TypeError` is raised.
|
438
|
+
- For instance, if `'thirty'` were provided for `:age`, it would raise a `TypeError` because it cannot be converted to an Integer.
|
439
|
+
|
440
|
+
- **Validation:**
|
441
|
+
- **Unexpected Elements:**
|
442
|
+
- The `build!` method currently does not raise an exception for unexpected keys in the input data (like `'extra_field'`), but these keys are ignored.
|
443
|
+
- If you require strict validation against unexpected keys, additional validation logic would need to be implemented.
|
444
|
+
- **Type Enforcement:**
|
445
|
+
- The gem enforces that the values match the expected types, ensuring data integrity.
|
446
|
+
- This helps catch errors early, preventing invalid data from propagating through your application.
|
447
|
+
|
448
|
+
- **Transformation:**
|
449
|
+
- **Key Renaming:**
|
450
|
+
- The use of the `:as` option transforms the internal parameter and group names to match the desired output keys.
|
451
|
+
- This is particularly useful when the input data keys do not align with the output format required by your application or when interfacing with external APIs.
|
452
|
+
- **Structuring Data:**
|
453
|
+
- By defining the configuration structure, you effectively map and reorganize the input data into a format that suits your needs.
|
454
|
+
|
455
|
+
- **Accessing Transformed Data:**
|
456
|
+
- **Resulting Configuration:**
|
457
|
+
- The `result` object contains the transformed and validated data.
|
458
|
+
- You can access the data using the mapped keys, such as `result[:user_info][:name]`.
|
459
|
+
- **Usage in Application:**
|
460
|
+
- The validated and transformed data is now ready for use within your application, confident that it adheres to the expected structure and types.
|
461
|
+
|
462
|
+
**Note:**
|
463
|
+
|
464
|
+
- **Extending Validation:**
|
465
|
+
- If you need to validate against unexpected keys (e.g., to ensure there are no extra fields in the input data), you can extend the gem's functionality.
|
466
|
+
- This could involve comparing the keys in the input data with the defined parameters and raising an error if discrepancies are found.
|
467
|
+
- **Flexible Handling:**
|
468
|
+
- For scenarios where you prefer not to raise exceptions on type coercion failures, you can use the `build` method instead of `build!`.
|
469
|
+
- The `build` method will attempt type coercion but will retain the original value if coercion fails, without raising an exception.
|
470
|
+
|
471
|
+
|
472
|
+
By leveraging AdaptiveConfiguration in this way, you can create robust data transformation and validation pipelines that simplify handling complex JSON data structures, ensuring your application receives data that is correctly typed and structured.
|
473
|
+
|
474
|
+
---
|
475
|
+
|
476
|
+
## Complex Example with Nested Groups and Arrays
|
477
|
+
|
478
|
+
Here's a comprehensive example that combines parameters, groups, array parameters, and defaults.
|
479
|
+
|
480
|
+
### Example:
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
require 'adaptiveconfiguration'
|
484
|
+
|
485
|
+
configuration = AdaptiveConfiguration::Builder.new do
|
486
|
+
parameter :api_key, String
|
487
|
+
group :chat_options do
|
488
|
+
parameter :model, String, default: 'claude-3-5'
|
489
|
+
parameter :max_tokens, Integer, default: 2000
|
490
|
+
parameter :temperature, Float, default: 0.7
|
491
|
+
parameter :stream, TrueClass
|
492
|
+
parameter :stop_sequences, String, array: true
|
493
|
+
|
494
|
+
group :metadata do
|
495
|
+
parameter :user_id, String
|
496
|
+
parameter :session_id, String
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
group :message, as: :messages, array: true do
|
501
|
+
parameter :role, String
|
502
|
+
parameter :content, String
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
result = configuration.build! do
|
507
|
+
api_key 'your-api-key'
|
508
|
+
|
509
|
+
chat_options do
|
510
|
+
temperature 0.5
|
511
|
+
stop_sequences ['end', 'stop']
|
512
|
+
metadata do
|
513
|
+
user_id 'user-123'
|
514
|
+
session_id 'session-456'
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
message do
|
519
|
+
role 'system'
|
520
|
+
content 'You are a helpful assistant.'
|
521
|
+
end
|
522
|
+
|
523
|
+
message do
|
524
|
+
role 'user'
|
525
|
+
content 'Hello!'
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Accessing values
|
530
|
+
puts result[:api_key] # => "your-api-key"
|
531
|
+
puts result[:chat_options][:model] # => "claude-3-5"
|
532
|
+
puts result[:chat_options][:temperature] # => 0.5
|
533
|
+
puts result[:chat_options][:metadata][:user_id] # => "user-123"
|
534
|
+
puts result[:messages].map { | msg | msg[:content] }
|
535
|
+
# => ["You are a helpful assistant.", "Hello!"]
|
536
|
+
```
|
537
|
+
|
538
|
+
**Notes:**
|
539
|
+
|
540
|
+
- **Combining Concepts:**
|
541
|
+
- Parameters with defaults (`:model`, `:max_tokens`).
|
542
|
+
- Nested groups (`:chat_options`, `:metadata`).
|
543
|
+
- Array parameters (`:stop_sequences`, `:messages`).
|
544
|
+
- Using the `:as` option to map `:message` to `:messages`.
|
545
|
+
- **Setting Values:**
|
546
|
+
- Override default values by providing new ones.
|
547
|
+
- Add multiple messages to the `:messages` array.
|
548
|
+
- **Accessing Values:**
|
549
|
+
- Use nested keys to access deeply nested values.
|
550
|
+
|
551
|
+
---
|
552
|
+
|
553
|
+
## Contributing
|
554
|
+
|
555
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/EndlessInternational/adaptive-configuration](https://github.com/EndlessInternational/adaptive-configuration).
|
556
|
+
|
557
|
+
## License
|
558
|
+
|
559
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Gem::Specification.new do | spec |
|
2
|
+
|
3
|
+
spec.name = 'adaptiveconfiguration'
|
4
|
+
spec.version = '1.0.0.beta01'
|
5
|
+
spec.authors = [ 'Kristoph Cichocki-Romanov' ]
|
6
|
+
spec.email = [ 'rubygems.org@kristoph.net' ]
|
7
|
+
|
8
|
+
spec.summary = <<~TEXT.gsub( "\n", " " ).strip
|
9
|
+
An elegant, lightweight and simple yet powerful gem for building structured configuration
|
10
|
+
definitions.
|
11
|
+
TEXT
|
12
|
+
spec.description = <<~TEXT.gsub( "\n", " " ).strip
|
13
|
+
AdaptiveConfiguration is an elegant, lightweight and simple, yet powerful Ruby gem that allows
|
14
|
+
you to define a DSL (Domain-Specific Language) for structured and hierarchical configurations.
|
15
|
+
It is ideal for defining complex configurations for various use cases, such as API clients,
|
16
|
+
application settings, or any scenario where structured configuration is needed.
|
17
|
+
|
18
|
+
In addition AdaptiveConfiguration can be more generally used to transform and validate JSON
|
19
|
+
data from any source such as from a network request or API reponse.
|
20
|
+
TEXT
|
21
|
+
|
22
|
+
spec.license = 'MIT'
|
23
|
+
spec.homepage = 'https://github.com/EndlessInternational/adaptive_configuration'
|
24
|
+
spec.metadata = {
|
25
|
+
'source_code_uri' => 'https://github.com/EndlessInternational/adaptive_configuration',
|
26
|
+
'bug_tracker_uri' => 'https://github.com/EndlessInternational/adaptive_configuration/issues',
|
27
|
+
# 'documentation_uri' => 'https://github.com/EndlessInternational/adaptive_configuration'
|
28
|
+
}
|
29
|
+
|
30
|
+
spec.required_ruby_version = '>= 3.0'
|
31
|
+
spec.files = Dir[ "lib/**/*.rb", "LICENSE", "README.md", "adaptiveconfiguration.gemspec" ]
|
32
|
+
spec.require_paths = [ "lib" ]
|
33
|
+
|
34
|
+
spec.add_development_dependency 'rspec', '~> 3.13'
|
35
|
+
spec.add_development_dependency 'debug', '~> 1.9'
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'group_builder'
|
2
|
+
require_relative 'context'
|
3
|
+
|
4
|
+
# types must be included to support conversation
|
5
|
+
require 'time'
|
6
|
+
require 'date'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
module AdaptiveConfiguration
|
10
|
+
class Builder < GroupBuilder
|
11
|
+
|
12
|
+
DEFAULT_CONVERTERS = {
|
13
|
+
|
14
|
+
Date => ->( v ) { v.respond_to?( :to_date ) ? v.to_date : Date.parse( v.to_s ) },
|
15
|
+
Time => ->( v ) { v.respond_to?( :to_time ) ? v.to_time : Time.parse( v.to_s ) },
|
16
|
+
URI => ->( v ) { URI.parse( v.to_s ) },
|
17
|
+
String => ->( v ) { String( v ) },
|
18
|
+
Integer => ->( v ) { Integer( v ) },
|
19
|
+
Float => ->( v ) { Float( v ) },
|
20
|
+
Rational => ->( v ) { Rational( v ) },
|
21
|
+
Array => ->( v ) { Array( v ) },
|
22
|
+
TrueClass => ->( v ) {
|
23
|
+
case v
|
24
|
+
when Numeric
|
25
|
+
v.nonzero? ? true : nil
|
26
|
+
else
|
27
|
+
v.to_s.match( /\A\s*(true|yes)\s*\z/i ) ? true : nil
|
28
|
+
end
|
29
|
+
},
|
30
|
+
FalseClass => ->( v ) {
|
31
|
+
case v
|
32
|
+
when Numeric
|
33
|
+
v.zero? ? false : nil
|
34
|
+
else
|
35
|
+
v.to_s.match( /\A\s*(false|no)\s*\z/i ) ? false : nil
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
}
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
super
|
43
|
+
@converters = DEFAULT_CONVERTERS.dup
|
44
|
+
end
|
45
|
+
|
46
|
+
def convert( klass, &block )
|
47
|
+
@converters[ klass ] = block
|
48
|
+
end
|
49
|
+
|
50
|
+
def build!( values = nil, &block )
|
51
|
+
context = AdaptiveConfiguration::Context.new(
|
52
|
+
values,
|
53
|
+
converters: @converters,
|
54
|
+
definitions: @definitions
|
55
|
+
)
|
56
|
+
context.instance_eval( &block ) if block
|
57
|
+
context
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'builder'
|
2
|
+
|
3
|
+
module AdaptiveConfiguration
|
4
|
+
module Configurable
|
5
|
+
|
6
|
+
def configuration( &block )
|
7
|
+
@configuration_builder ||= AdaptiveConfiguration::Builder.new
|
8
|
+
@configuration_builder.instance_eval( &block )
|
9
|
+
@configuration_builder
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure( attributes = nil, &block )
|
13
|
+
raise RuntimeError, "The adapter configuration has not been defined." \
|
14
|
+
if @configuration_builder.nil?
|
15
|
+
configuration = @configuration_builder.build!( attributes, &block )
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module AdaptiveConfiguration
|
2
|
+
class Context < BasicObject
|
3
|
+
|
4
|
+
def initialize( values = nil, definitions:, converters: )
|
5
|
+
|
6
|
+
values = values ? values.transform_keys( &:to_sym ) : {}
|
7
|
+
|
8
|
+
@converters = converters&.dup
|
9
|
+
@definitions = definitions&.dup
|
10
|
+
@values = {}
|
11
|
+
|
12
|
+
@definitions.each do | key, definition |
|
13
|
+
name = definition[ :as ] || key
|
14
|
+
if definition[ :type ] == :group
|
15
|
+
context = Context.new(
|
16
|
+
values[ key ] || {},
|
17
|
+
converters: @converters, definitions: definition[ :definitions ],
|
18
|
+
)
|
19
|
+
@values[ name ] = context unless context.empty?
|
20
|
+
elsif definition.key?( :default )
|
21
|
+
@values[ name ] = definition[ :array ] ?
|
22
|
+
::Kernel.method( :Array ).call( definition[ :default ] ) :
|
23
|
+
definition[ :default ]
|
24
|
+
# note: this is needed to know when an array paramter which was initially assigned
|
25
|
+
# to a default should be replaced rather than appended
|
26
|
+
definition[ :default_assigned ] = true
|
27
|
+
end
|
28
|
+
self.__send__( key, values[ key ] ) if values[ key ]
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def []( key )
|
34
|
+
@values[ key ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def []=( key, value )
|
38
|
+
@values[ key ] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def each( &block )
|
42
|
+
@values.each( &block )
|
43
|
+
end
|
44
|
+
|
45
|
+
def merge( hash )
|
46
|
+
self.to_h.merge( hash )
|
47
|
+
end
|
48
|
+
|
49
|
+
def empty?
|
50
|
+
@values.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_h
|
54
|
+
recursive_to_h = ->( object ) do
|
55
|
+
case object
|
56
|
+
when ::AdaptiveConfiguration::Context
|
57
|
+
recursive_to_h.call( object.to_h )
|
58
|
+
when ::Hash
|
59
|
+
object.transform_values { | value | recursive_to_h.call( value ) }
|
60
|
+
when ::Array
|
61
|
+
object.map { | element | recursive_to_h.call( element ) }
|
62
|
+
else
|
63
|
+
object.respond_to?( :to_h ) ? object.to_h : object
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
recursive_to_h.call( @values )
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
inspect
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_yaml
|
75
|
+
self.to_h.to_yaml
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect
|
79
|
+
@values.inspect
|
80
|
+
end
|
81
|
+
|
82
|
+
def class
|
83
|
+
::AdaptiveConfiguration::Context
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_a?( klass )
|
87
|
+
klass == ::AdaptiveConfiguration::Context || klass == ::BasicObject
|
88
|
+
end
|
89
|
+
|
90
|
+
alias :kind_of? :is_a?
|
91
|
+
|
92
|
+
def method_missing( method, *args, &block )
|
93
|
+
|
94
|
+
if @definitions.key?( method )
|
95
|
+
|
96
|
+
definition = @definitions[ method ]
|
97
|
+
name = definition[ :as ] || method
|
98
|
+
|
99
|
+
unless definition[ :array ]
|
100
|
+
if definition[ :type ] == :group
|
101
|
+
context =
|
102
|
+
@values[ name ] ||
|
103
|
+
Context.new( converters: @converters, definitions: definition[ :definitions ] )
|
104
|
+
context.instance_eval( &block ) if block
|
105
|
+
@values[ name ] = context
|
106
|
+
else
|
107
|
+
value = args.first
|
108
|
+
value = _convert_value!( definition[ :type ], method, value ) if definition[ :type ]
|
109
|
+
@values[ name ] = value
|
110
|
+
end
|
111
|
+
else
|
112
|
+
@values[ name ] = definition[ :default_assigned ] ?
|
113
|
+
::Array.new :
|
114
|
+
@values[ name ] || ::Array.new
|
115
|
+
if definition[ :type ] == :group
|
116
|
+
context = Context.new( converters: @converters, definitions: definition[ :definitions ] )
|
117
|
+
context.instance_eval( &block ) if block
|
118
|
+
@values[ name ] << context
|
119
|
+
else
|
120
|
+
values = ::Kernel.method( :Array ).call( args.first )
|
121
|
+
if type = definition[ :type ]
|
122
|
+
values = values.map { | v | _convert_value!( type, method, v ) }
|
123
|
+
end
|
124
|
+
@values[ name ].concat( values )
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
definition[ :default_assigned ] = false
|
129
|
+
@values[ name ]
|
130
|
+
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
def respond_to?( method, include_private = false )
|
138
|
+
@definitions.key?( method ) || self.class.instance_methods.include?( method )
|
139
|
+
end
|
140
|
+
|
141
|
+
def respond_to_missing?( method, include_private = false )
|
142
|
+
@definitions.key?( method ) || self.class.instance_methods.include?( method )
|
143
|
+
end
|
144
|
+
|
145
|
+
private; def _convert_value!( klass, key, value )
|
146
|
+
return value unless klass && value
|
147
|
+
|
148
|
+
types =::Kernel.method( :Array ).call( klass )
|
149
|
+
result = nil
|
150
|
+
|
151
|
+
types.each do | type |
|
152
|
+
result =
|
153
|
+
( ( value.respond_to?( :is_a? ) && value.is_a?( type ) ) ? value : nil ) ||
|
154
|
+
@converters[ klass ].call( value ) rescue nil
|
155
|
+
break if result
|
156
|
+
end
|
157
|
+
|
158
|
+
if result.nil?
|
159
|
+
types_names = types.map( &:name ).join( ', ' )
|
160
|
+
::Kernel.raise ::TypeError, <<~TEXT.gsub( /\s+/, ' ' ).strip
|
161
|
+
The key #{key} expects a value of type #{types_names} but received an
|
162
|
+
incompatible #{value.class.name}.
|
163
|
+
TEXT
|
164
|
+
end
|
165
|
+
|
166
|
+
result
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'context'
|
2
|
+
|
3
|
+
module AdaptiveConfiguration
|
4
|
+
class GroupBuilder
|
5
|
+
|
6
|
+
attr_reader :definitions
|
7
|
+
|
8
|
+
def initialize( &block )
|
9
|
+
@definitions = {}
|
10
|
+
self.instance_eval( &block ) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def parameter( name, *args )
|
14
|
+
name = name.to_sym
|
15
|
+
options = nil
|
16
|
+
|
17
|
+
raise NameError, "The parameter #{name} cannot be used." \
|
18
|
+
if AdaptiveConfiguration::Context.instance_methods.include?( name )
|
19
|
+
|
20
|
+
if args.first.is_a?( ::Hash )
|
21
|
+
# when called without type: parameter :stream, as: :streams
|
22
|
+
options = args.first
|
23
|
+
else
|
24
|
+
# when called with type: parameter :stream, Boolean, as: :streams
|
25
|
+
options = args[ 1 ] || {}
|
26
|
+
options[ :type ] = args.first
|
27
|
+
end
|
28
|
+
|
29
|
+
@definitions[ name ] = options
|
30
|
+
end
|
31
|
+
|
32
|
+
def group( name, options = {}, &block )
|
33
|
+
builder = GroupBuilder.new
|
34
|
+
builder.instance_eval( &block ) if block
|
35
|
+
@definitions[ name ] = options.merge( {
|
36
|
+
type: :group,
|
37
|
+
definitions: builder.definitions
|
38
|
+
} )
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adaptiveconfiguration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.beta01
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kristoph Cichocki-Romanov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.13'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.13'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: debug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
description: AdaptiveConfiguration is an elegant, lightweight and simple, yet powerful
|
42
|
+
Ruby gem that allows you to define a DSL (Domain-Specific Language) for structured
|
43
|
+
and hierarchical configurations. It is ideal for defining complex configurations
|
44
|
+
for various use cases, such as API clients, application settings, or any scenario
|
45
|
+
where structured configuration is needed. In addition AdaptiveConfiguration can
|
46
|
+
be more generally used to transform and validate JSON data from any source such
|
47
|
+
as from a network request or API reponse.
|
48
|
+
email:
|
49
|
+
- rubygems.org@kristoph.net
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- LICENSE
|
55
|
+
- README.md
|
56
|
+
- adaptiveconfiguration.gemspec
|
57
|
+
- lib/adaptive_configuration.rb
|
58
|
+
- lib/adaptive_configuration/builder.rb
|
59
|
+
- lib/adaptive_configuration/configurable.rb
|
60
|
+
- lib/adaptive_configuration/context.rb
|
61
|
+
- lib/adaptive_configuration/group_builder.rb
|
62
|
+
homepage: https://github.com/EndlessInternational/adaptive_configuration
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata:
|
66
|
+
source_code_uri: https://github.com/EndlessInternational/adaptive_configuration
|
67
|
+
bug_tracker_uri: https://github.com/EndlessInternational/adaptive_configuration/issues
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '3.0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.5.19
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: An elegant, lightweight and simple yet powerful gem for building structured
|
87
|
+
configuration definitions.
|
88
|
+
test_files: []
|