adaptiveconfiguration 1.0.0.beta01

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ require_relative 'adaptive_configuration/builder'
2
+ require_relative 'adaptive_configuration/configurable'
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: []