etcher 1.6.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +137 -53
- data/etcher.gemspec +3 -3
- data/lib/etcher/builder.rb +19 -12
- data/lib/etcher/loaders/environment.rb +5 -5
- data/lib/etcher/loaders/hash.rb +22 -0
- data/lib/etcher/loaders/json.rb +19 -8
- data/lib/etcher/loaders/yaml.rb +44 -10
- data/lib/etcher/registry.rb +18 -13
- data/lib/etcher/resolver.rb +13 -15
- data/lib/etcher/transformers/basename.rb +29 -0
- data/lib/etcher/transformers/format.rb +35 -0
- data/lib/etcher/transformers/root.rb +29 -0
- data/lib/etcher/transformers/time.rb +3 -3
- data.tar.gz.sig +0 -0
- metadata +11 -8
- metadata.gz.sig +0 -0
- data/lib/etcher/transformers/string.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18c97ac57c0742fedf8872da7aa401ceae29552ccc9e78ce0d8df72fc8fff08c
|
4
|
+
data.tar.gz: feb2442ddc16419bc49dc97ba1e449e226713d57a2c1fd6714e8592479faf636
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd2eb81d5e6a950c8d2cc7205fa09d41608bf2a835e25aa13dc5bbbb9bf4f6a60eea36716d5a132047aba29290a3706c8b3b6b54c175df9f95d0e25943719d25
|
7
|
+
data.tar.gz: af20a0db0eaefd28a6a69c1bf0193084ec841a1042cc6f842667c3cc2da803d064e244670429c30a621905615b4a6aee963cab081028b6d10d7b563444fa6969
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -15,6 +15,7 @@
|
|
15
15
|
:pipeable_link: link:https://alchemists.io/projects/pipeable[Pipeable]
|
16
16
|
:runcom_link: link:https://alchemists.io/projects/runcom[Runcom]
|
17
17
|
:sod_link: link:https://alchemists.io/projects/sod[Sod]
|
18
|
+
:string_formats_link: link:https://docs.ruby-lang.org/en/3.3/format_specifications_rdoc.html[String Formats]
|
18
19
|
:struct_link: link:https://alchemists.io/articles/ruby_structs[Struct]
|
19
20
|
:versionaire_link: link:https://alchemists.io/projects/versionaire[Versionaire]
|
20
21
|
:xdg_link: link:https://alchemists.io/projects/xdg[XDG]
|
@@ -28,7 +29,7 @@ ____
|
|
28
29
|
[Use] strong acid or mordant to cut into the unprotected parts of a metal surface to create a design in intaglio (incised) in the metal.
|
29
30
|
____
|
30
31
|
|
31
|
-
By using Etcher, you have a reliable way to load default configurations (i.e. {environment_link}, {json_link}, {yaml_link}) which can be validated and etched into _frozen_ records (i.e. {hash_link}, {data_link}, {struct_link}) for consumption within your application which doesn't violate the {demeter_link}. This comes complete with transformations and validations all via a simple Object API
|
32
|
+
By using Etcher, you have a reliable way to load default configurations (i.e. {hash_link}, {environment_link}, {json_link}, {yaml_link}) which can be validated and etched into _frozen_ records (i.e. {hash_link}, {data_link}, {struct_link}) for consumption within your application which doesn't violate the {demeter_link}. This comes complete with loaders, transformations, and validations all via a simple Object API that pairs well with the {xdg_link}, {runcom_link}, and {sod_link} gems.
|
32
33
|
|
33
34
|
toc::[]
|
34
35
|
|
@@ -36,7 +37,7 @@ toc::[]
|
|
36
37
|
|
37
38
|
* Supports contracts which respond to `#call` to validate a {hash_link} before building the final record. Pairs well with the {dry_schema_link} and {dry_validation_link} gems.
|
38
39
|
* Supports models which respond to `.[]` for consuming a splatted {hash_link} to instantiate new records. Pairs well with primitives such as: {hash_link}, {data_link}, and {struct_link}.
|
39
|
-
* Supports loading of default configurations from the {environment_link}, a {json_link} configuration, a {yaml_link} configuration, or anything that can answer a hash.
|
40
|
+
* Supports loading of default configurations from a {hash_link}, the {environment_link}, a {json_link} configuration, a {yaml_link} configuration, or anything that can answer a hash.
|
40
41
|
* Supports multiple transformations which can process loaded configuration hashes and answer a transformed hash.
|
41
42
|
* Supports {hash_link} overrides as a final customization which is handy for Command Line Interfaces (CLIs), as aided by {sod_link}, or anything that might require user input at runtime.
|
42
43
|
|
@@ -133,16 +134,16 @@ While this is a more advanced use case, you'll usually only need to register a c
|
|
133
134
|
As hinted at above, the complete sequence of steps are performed in the order listed:
|
134
135
|
|
135
136
|
. *Load*: Each loader, if any, is called and merged with the previous loader to build initial attributes.
|
136
|
-
. *Override*: Any overrides are merged with the result of the last loader to produce updated attributes. ⚠️ _In Version 2.0.0, this step will happen after the Transform step._
|
137
137
|
. *Transform*: Each transformer, if any, is called to transform and manipulate the attributes.
|
138
|
+
. *Override*: Overrides, if any, are merged with the result of the last transformer so you can fine tune the data as desired.
|
138
139
|
. *Validate*: The contract is called to validate the attributes as previously loaded, overwritten, and transformed.
|
139
140
|
. *Model*: The model consumes the attributes of the validated contract and creates a new record for you to use as needed.
|
140
141
|
|
141
|
-
You can use the above steps as a reference when using this gem. Each step is explained in greater
|
142
|
+
Each step _mutates_ the attributes of the previous step in order to produce a record (success) or error (failure). You can use the above steps as a reference when using this gem. Each step is explained in greater below.
|
142
143
|
|
143
144
|
=== Registry
|
144
145
|
|
145
|
-
The registry
|
146
|
+
The registry provides a way to register any/all behavior for before creating a new Etcher instance. Here's what you get by default:
|
146
147
|
|
147
148
|
[source,ruby]
|
148
149
|
----
|
@@ -179,7 +180,7 @@ Contracts are a critical piece of this workflow as they provide a way to validat
|
|
179
180
|
|
180
181
|
* `#call`: Must be able to consume a {hash_link} and answer an object which can respond to `#to_monad`.
|
181
182
|
|
182
|
-
|
183
|
+
Both {dry_schema_link} and {dry_validation_link} respond to the `#to_monad` message. Ensure the {dry_monads_link} extensions are loaded too, as briefly shown earlier, so the result will respond to the `#to_monad` message. Here's how to enable monad support if using both gems:
|
183
184
|
|
184
185
|
[source,ruby]
|
185
186
|
----
|
@@ -209,7 +210,7 @@ etcher.call from: "Mork", to: "Mindy"
|
|
209
210
|
# Success({:from=>"Mork", :to=>"Mindy"})
|
210
211
|
----
|
211
212
|
|
212
|
-
Here you can see the power of using a contract to validate your data both as a failure and a success. Unfortunately, with the success, we only get a {hash_link} as a record
|
213
|
+
Here you can see the power of using a contract to validate your data both as a failure and a success. Unfortunately, with the success, we only get a {hash_link} as a record but it would be better to have a data structure which will be explained shortly.
|
213
214
|
|
214
215
|
=== Types
|
215
216
|
|
@@ -279,7 +280,7 @@ Notice we get an failure if all attributes are not provided but if we supply the
|
|
279
280
|
|
280
281
|
=== Loaders
|
281
282
|
|
282
|
-
Loaders are a great way to load a _default_ configuration for your application which can be in multiple formats. Loaders can either be defined when creating a new registry instance or added after the fact. Here are a
|
283
|
+
Loaders are a great way to load a _default_ configuration for your application which can be in multiple formats. Loaders can either be defined when creating a new registry instance or added after the fact. Here are a couple examples:
|
283
284
|
|
284
285
|
[source,ruby]
|
285
286
|
----
|
@@ -292,8 +293,9 @@ registry = Etcher::Registry.new.add_loader MyLoader.new
|
|
292
293
|
|
293
294
|
There are a few guidelines to using them:
|
294
295
|
|
295
|
-
*
|
296
|
-
* All
|
296
|
+
* All loaders must respond to `#call` with no arguments.
|
297
|
+
* All loaders must answer either a success with attributes (i.e. `Success attributes`) or a failure with details about the failure (i.e. `Failure step: :load, constant: MyLoader, payload: "My error message.`)
|
298
|
+
* All keys are symbolized after the loader is called which helps streamline merging and overriding values from the same keys across multiple configurations.
|
297
299
|
* All nested keys will be flattened after being loaded. This means a key structure of `{demo: {one: "test"}}` will be flattened to `demo_one: "test"` which adheres to the {demeter_link} when a new recored is _etched_ for you.
|
298
300
|
* The order in which you define your loaders matters. This means the first loader defined will be processed first, then the second, and so forth. Loaders defined last take precedence over previously defined loaders when overriding the same keys.
|
299
301
|
|
@@ -341,6 +343,25 @@ loader.call
|
|
341
343
|
|
342
344
|
This loader is great for pulling from environment variables as a fallback configuration for your application.
|
343
345
|
|
346
|
+
==== Hash
|
347
|
+
|
348
|
+
Use `:hash` or `Etcher::Loaders::Hash` to load in-memory attributes. By default, this loader will answer an empty hash if not supplied with any attributes. Here's a few examples:
|
349
|
+
|
350
|
+
[source,ruby]
|
351
|
+
----
|
352
|
+
# Default behavior.
|
353
|
+
loader = Etcher::Loaders::Hash.new
|
354
|
+
loader.call
|
355
|
+
# Success({})
|
356
|
+
|
357
|
+
# With custom attributes
|
358
|
+
loader = Etcher::Loaders::Hash.new one: 1, two: 2
|
359
|
+
loader.call
|
360
|
+
# Success({:one=>1, :two=>2})
|
361
|
+
----
|
362
|
+
|
363
|
+
This loader is great for adding custom attributes, overriding/adjusting attributes from a previous loader, or customizing attributes for testing purposes within a test suite.
|
364
|
+
|
344
365
|
==== JSON
|
345
366
|
|
346
367
|
Use `Etcher::Loaders::JSON` to load configuration information from a {json_link} file. Here's how to use this loader (using a file that doesn't exist):
|
@@ -362,9 +383,12 @@ loader = Etcher::Loaders::JSON.new "your/path/to/configuration.json",
|
|
362
383
|
loader.call # Success({})
|
363
384
|
----
|
364
385
|
|
365
|
-
|
386
|
+
If the file exists with _valid_ content, you'll get a `Hash` wrapped as a `Success`. In situations in which the file doesn't exist, you'll get a `Success` with an empty hash and debug information logged instead. Any failures will be provided with step, constant, and payload details. Example:
|
366
387
|
|
367
|
-
|
388
|
+
[source,ruby]
|
389
|
+
----
|
390
|
+
Failure step: :load, constant: Etcher::Loaders::JSON, payload: "Danger!"
|
391
|
+
----
|
368
392
|
|
369
393
|
==== YAML
|
370
394
|
|
@@ -387,9 +411,12 @@ loader = Etcher::Loaders::YAML.new "your/path/to/configuration.yml",
|
|
387
411
|
loader.call # Success({})
|
388
412
|
----
|
389
413
|
|
390
|
-
|
414
|
+
If the file exists with _valid_ content, you'll get a `Hash` wrapped as a `Success`. In situations in which the file doesn't exist, you'll get a `Success` with an empty hash and debug information logged instead. Any failures will be provided with step, constant, and payload details. Example:
|
391
415
|
|
392
|
-
|
416
|
+
[source,ruby]
|
417
|
+
----
|
418
|
+
Failure step: :load, constant: Etcher::Loaders::YAML, payload: "Danger!"
|
419
|
+
----
|
393
420
|
|
394
421
|
==== Custom
|
395
422
|
|
@@ -402,26 +429,31 @@ require "dry/monads"
|
|
402
429
|
class Demo
|
403
430
|
include Dry::Monads[:result]
|
404
431
|
|
405
|
-
def initialize
|
406
|
-
@
|
432
|
+
def initialize processor: Processor.new
|
433
|
+
@processor = processor
|
407
434
|
end
|
408
435
|
|
409
|
-
def call
|
436
|
+
def call
|
437
|
+
Success processor.call
|
438
|
+
rescue ProcessorError => error
|
439
|
+
Failure step: :load, constant: self.class, payload: error.message
|
440
|
+
end
|
410
441
|
|
411
442
|
private
|
412
443
|
|
413
|
-
attr_reader :
|
444
|
+
attr_reader :processor
|
414
445
|
end
|
415
446
|
|
416
|
-
|
417
|
-
|
447
|
+
registry = Etcher::Registry[loaders: [Demo.new]]
|
448
|
+
|
449
|
+
Etcher.new(registry).call
|
418
450
|
----
|
419
451
|
|
420
|
-
While the above
|
452
|
+
While the above assumes you have some kind of `Processor` for loading attributes, you can see there is little effort required to implement and customize as desired.
|
421
453
|
|
422
454
|
=== Transformers
|
423
455
|
|
424
|
-
Transformers are great for
|
456
|
+
Transformers are great for _mutating_ specific keys and values. They give you fine grained customization over your configuration. Transformers can either be defined when creating a new registry instance or added after the fact. Here are a couple examples:
|
425
457
|
|
426
458
|
[source,ruby]
|
427
459
|
----
|
@@ -436,8 +468,9 @@ The guidelines for using transformers are:
|
|
436
468
|
|
437
469
|
* They can be initialized with whatever requirements you need.
|
438
470
|
* They must respond to `#call` which takes a required `attributes` positional argument and answers a modified version of these attributes (`Hash`) wrapped as a monad.
|
439
|
-
*
|
440
|
-
* When using a
|
471
|
+
* They must answer either a success with attributes (i.e. `Success attributes`) or a failure with details about the failure (i.e. `Failure step: :transform, constant: MyTransformer, payload: "My error message.`)
|
472
|
+
* When using a proc/lambda, the first, _required_, parameter should be the `attributes` parameter followed by a second positional `key` parameter.
|
473
|
+
* When using a class, the `key` should be your first positional parameter. Additional parameters can be supplied after if desired.
|
441
474
|
* The `attributes` passed to your transformer will have symbolized keys so you don't need to transform them further.
|
442
475
|
|
443
476
|
For example, the following capitalizes all values (which may or may not be good depending on your data structure):
|
@@ -492,8 +525,8 @@ For convenience, all transformers -- only packaged with this gem -- can be regis
|
|
492
525
|
----
|
493
526
|
registry = Etcher::Registry.new
|
494
527
|
|
495
|
-
#
|
496
|
-
registry.add_transformer :
|
528
|
+
# Format
|
529
|
+
registry.add_transformer :format, :project_uri
|
497
530
|
|
498
531
|
# Time
|
499
532
|
registry.add_transformer :time
|
@@ -501,9 +534,47 @@ registry.add_transformer :time
|
|
501
534
|
|
502
535
|
Any positional or keyword arguments will be passed to the transformers's constructor. _This only works when using `Registry#add_transformer`, though._ The following sections provide more details on each.
|
503
536
|
|
504
|
-
====
|
537
|
+
==== Basename
|
538
|
+
|
539
|
+
Use `Etcher::Transformers::Basename` to dynamically obtain the name of the current directory as a value for a key. This is handy for scripting or CLI purposes when needing to know the name of the current project you are working in. Example:
|
540
|
+
|
541
|
+
[source,ruby]
|
542
|
+
----
|
543
|
+
transformer = Etcher::Transformers::Basename.new :demo
|
544
|
+
transformer.call({})
|
545
|
+
# Success({:demo=>"scratch"})
|
546
|
+
|
547
|
+
transformer = Etcher::Transformers::Basename.new :demo, fallback: "undefined"
|
548
|
+
transformer.call({})
|
549
|
+
# Success({:demo=>"undefined"})
|
550
|
+
|
551
|
+
transformer = Etcher::Transformers::Basename.new :demo
|
552
|
+
transformer.call({demo: "defined"})
|
553
|
+
# Success({:demo=>"defined"})
|
554
|
+
----
|
555
|
+
|
556
|
+
==== Root
|
505
557
|
|
506
|
-
Use `Etcher::Transformers::
|
558
|
+
Use `Etcher::Transformers::Root` to dynamically obtain the current path as a value for a key. This is handy for obtaining the absolute path to a new or existing directory. Example:
|
559
|
+
|
560
|
+
[source,ruby]
|
561
|
+
----
|
562
|
+
transformer = Etcher::Transformers::Root.new :demo
|
563
|
+
transformer.call({})
|
564
|
+
# Success({:demo=>#<Pathname:/Users/demo/Engineering/OSS/scratch>})
|
565
|
+
|
566
|
+
transformer = Etcher::Transformers::Root.new :demo, fallback: "undefined"
|
567
|
+
transformer.call({})
|
568
|
+
# Success({:demo=>#<Pathname:/Users/demo/Engineering/undefined>})
|
569
|
+
|
570
|
+
transformer = Etcher::Transformers::Root.new :demo
|
571
|
+
transformer.call({demo: "defined"})
|
572
|
+
# Success({:demo=>#<Pathname:/Users/demo/Engineering/defined>})
|
573
|
+
----
|
574
|
+
|
575
|
+
==== Format
|
576
|
+
|
577
|
+
Use `Etcher::Transformers::Format` to transform any key's value by using the configuration's existing attributes to format the value of a specific key using the {string_formats_link} Specification. Example:
|
507
578
|
|
508
579
|
[source,ruby]
|
509
580
|
----
|
@@ -513,9 +584,7 @@ attributes = {
|
|
513
584
|
project_uri: "%<organization_uri>s/projects/%<project_name>s"
|
514
585
|
}
|
515
586
|
|
516
|
-
|
517
|
-
|
518
|
-
transformer.call attributes
|
587
|
+
Etcher::Transformers::Format.new(:project_uri).call attributes
|
519
588
|
# Success(
|
520
589
|
{
|
521
590
|
organization_uri: "https://acme.io",
|
@@ -526,53 +595,69 @@ transformer.call attributes
|
|
526
595
|
|
527
596
|
attributes.delete :project_name
|
528
597
|
transformer.call attributes
|
529
|
-
|
598
|
+
|
599
|
+
# Failure(
|
600
|
+
# {
|
601
|
+
# step: :transform,
|
602
|
+
# constant: Etcher::Transformers::Format,
|
603
|
+
# payload: "Unable to transform :project_uri, missing specifier: \"<project_name>\"."
|
604
|
+
# }
|
605
|
+
# )
|
530
606
|
----
|
531
607
|
|
608
|
+
You can also, safely, transform a value which _doesn't_ have string specifiers:
|
609
|
+
|
610
|
+
[source,ruby]
|
611
|
+
----
|
612
|
+
Etcher::Transformers::Format.new(:version).call(version: "1.2.3")
|
613
|
+
# Success({:version=>"1.2.3"})
|
614
|
+
----
|
615
|
+
|
616
|
+
Normally, you'd get a "too many arguments for format string" warning but this transformer detects and immediate skips formatting when no string specifiers are detected. This is handy for situations where your configuration supports values which may or may not need formatting.
|
617
|
+
|
532
618
|
==== Time
|
533
619
|
|
534
|
-
Use `Etcher::Transformers::Time` to transform the
|
620
|
+
Use `Etcher::Transformers::Time` to transform the any key in your configuration when you want to know the current time at which the configuration was loaded. Handy for situations where you need to calculate relative time or format time based on when your configuration was loaded.
|
535
621
|
|
536
|
-
|
622
|
+
You must supply a key and `Time.now.utc` is the default fallback. You can customize as desired. Example:
|
537
623
|
|
538
624
|
[source,ruby]
|
539
625
|
----
|
540
|
-
transformer = Etcher::Transformers::Time.new
|
541
|
-
transformer.call({})
|
542
|
-
# Success({:loaded_at=>2024-05-23 22:18:27.92767 UTC})
|
543
|
-
|
544
626
|
transformer = Etcher::Transformers::Time.new :now
|
545
627
|
transformer.call({})
|
546
|
-
# Success({:now=>2024-
|
628
|
+
# Success({:now=>2024-06-15 22:43:29.178488 UTC})
|
547
629
|
|
548
630
|
transformer = Etcher::Transformers::Time.new :now, fallback: Time.utc(2000, 1, 1)
|
549
631
|
transformer.call({})
|
550
632
|
# Success({:now=>2000-01-01 00:00:00 UTC})
|
551
633
|
|
552
|
-
transformer = Etcher::Transformers::Time.new
|
553
|
-
transformer.call({
|
554
|
-
# Success({:
|
634
|
+
transformer = Etcher::Transformers::Time.new :now
|
635
|
+
transformer.call({now: Time.utc(2000, 1, 1)})
|
636
|
+
# Success({:now=>2000-01-01 00:00:00 UTC})
|
555
637
|
----
|
556
638
|
|
557
639
|
=== Overrides
|
558
640
|
|
559
|
-
Overrides are what you pass to the Etcher instance when called. Example:
|
641
|
+
Overrides are what you pass to the Etcher instance when called. They allow you to override any values that were loaded and/or transformed. Example:
|
560
642
|
|
561
643
|
[source,ruby]
|
562
644
|
----
|
563
645
|
etcher = Etcher.new
|
646
|
+
|
647
|
+
# With symbol keys.
|
564
648
|
etcher.call name: "test", label: "Test"
|
649
|
+
# Success({:name=>"test", :label=>"Test"})
|
565
650
|
|
651
|
+
# With string keys.
|
652
|
+
etcher.call "name" => "test", "label" => "Test"
|
566
653
|
# Success({:name=>"test", :label=>"Test"})
|
567
654
|
----
|
568
655
|
|
569
|
-
Overrides are applied _after_ any
|
570
|
-
|
571
|
-
⚠️ In Version 2.0.0, this step will be changed to occur _after_ the Transform step for maximum flexibility.
|
656
|
+
Overrides are applied _after_ any transforms and _before_ validations. They are a nice way to deal with user input during runtime or provide additional attributes not supplied by the loading and/or transforming of your default configuration while ensuring they are validated properly. Any string keys will be transformed to symbol keys to ensure consistency and reduce issues when merged.
|
572
657
|
|
573
658
|
=== Resolver
|
574
659
|
|
575
|
-
In situations where you'd like Etcher to handle the complete load, transform, validate, and
|
660
|
+
In situations where you'd like Etcher to handle the complete load, transform, override, validate, and model steps for you, then you can use the resolver. This is provided for use cases where you'd like Etcher to handle everything for you and abort if otherwise. Example:
|
576
661
|
|
577
662
|
[source,ruby]
|
578
663
|
----
|
@@ -599,23 +684,22 @@ registry = Etcher::Registry.new(contract:, model:)
|
|
599
684
|
|
600
685
|
Etcher.call registry
|
601
686
|
|
602
|
-
#
|
687
|
+
# 🛑 Etcher validate failure (Etcher::Builder). Unable to load configuration:
|
603
688
|
# - to is missing
|
604
689
|
# - from is missing
|
605
690
|
|
606
691
|
Etcher.call registry, to: "Mindy"
|
607
692
|
|
608
|
-
#
|
693
|
+
# 🛑 Etcher validate failure (Etcher::Builder). Unable to load configuration:
|
609
694
|
# - from is missing
|
610
695
|
|
611
|
-
|
612
696
|
registry = Etcher::Registry.new(model: Data.define(:name, :label))
|
613
697
|
Etcher.call registry, to: "Mindy"
|
614
698
|
|
615
|
-
#
|
699
|
+
# 🛑 Etcher model failure (Etcher::Builder). Missing keywords: :name, :label.
|
616
700
|
----
|
617
701
|
|
618
|
-
💡 When using a custom registry, make sure it's the first argument.
|
702
|
+
💡 When using a custom registry, make sure it's the first argument. Additional arguments can be supplied afterwards and they can be any number of key/value overrides which is similar to how `Etcher.new` works.
|
619
703
|
|
620
704
|
== Development
|
621
705
|
|
@@ -639,7 +723,7 @@ bin/console
|
|
639
723
|
|
640
724
|
The following illustrates the full sequences of events when _etching_ new records:
|
641
725
|
|
642
|
-
image::https://alchemists.io/images/projects/etcher/
|
726
|
+
image::https://alchemists.io/images/projects/etcher/architecture.png[Architecture Diagram,1250,1071,role=focal_point]
|
643
727
|
|
644
728
|
== Tests
|
645
729
|
|
data/etcher.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = "etcher"
|
5
|
-
spec.version = "
|
5
|
+
spec.version = "2.0.0"
|
6
6
|
spec.authors = ["Brooke Kuhlmann"]
|
7
7
|
spec.email = ["brooke@alchemists.io"]
|
8
8
|
spec.homepage = "https://alchemists.io/projects/etcher"
|
@@ -23,11 +23,11 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.cert_chain = [Gem.default_cert_path]
|
24
24
|
|
25
25
|
spec.required_ruby_version = "~> 3.3"
|
26
|
-
spec.add_dependency "cogger", "~> 0.
|
26
|
+
spec.add_dependency "cogger", "~> 0.21"
|
27
27
|
spec.add_dependency "core", "~> 1.0"
|
28
28
|
spec.add_dependency "dry-monads", "~> 1.6"
|
29
29
|
spec.add_dependency "dry-types", "~> 1.7"
|
30
|
-
spec.add_dependency "refinements", "~> 12.
|
30
|
+
spec.add_dependency "refinements", "~> 12.5"
|
31
31
|
spec.add_dependency "versionaire", "~> 13.0"
|
32
32
|
spec.add_dependency "zeitwerk", "~> 2.6"
|
33
33
|
|
data/lib/etcher/builder.rb
CHANGED
@@ -16,27 +16,25 @@ module Etcher
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def call(**overrides)
|
19
|
-
load
|
20
|
-
|
21
|
-
|
19
|
+
load.bind { |attributes| transform attributes }
|
20
|
+
.fmap { |attributes| attributes.merge! overrides.symbolize_keys! }
|
21
|
+
.bind { |attributes| validate attributes }
|
22
|
+
.bind { |attributes| model attributes }
|
22
23
|
end
|
23
24
|
|
24
25
|
private
|
25
26
|
|
26
27
|
attr_reader :registry
|
27
28
|
|
28
|
-
def load
|
29
|
+
def load
|
29
30
|
registry.loaders
|
30
31
|
.map { |loader| loader.call.fmap { |pairs| pairs.flatten_keys.symbolize_keys! } }
|
31
|
-
.
|
32
|
-
.with_object({}) { |attributes, all| attributes.bind { |body| all.merge! body } }
|
33
|
-
.merge!(overrides.flatten_keys)
|
34
|
-
.then { |attributes| Success attributes }
|
32
|
+
.reduce(Success({})) { |all, result| merge all, result }
|
35
33
|
end
|
36
34
|
|
37
35
|
def transform attributes
|
38
|
-
registry.transformers.reduce attributes do |all, transformer|
|
39
|
-
all
|
36
|
+
registry.transformers.reduce Success(attributes) do |all, transformer|
|
37
|
+
merge all, transformer.call(attributes)
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
@@ -44,13 +42,22 @@ module Etcher
|
|
44
42
|
registry.contract
|
45
43
|
.call(attributes)
|
46
44
|
.to_monad
|
47
|
-
.or
|
45
|
+
.or do |result|
|
46
|
+
Failure step: __method__, constant: self.class, payload: result.errors.to_h
|
47
|
+
end
|
48
48
|
end
|
49
49
|
|
50
50
|
def model attributes
|
51
51
|
Success registry.model[**attributes.to_h].freeze
|
52
52
|
rescue ArgumentError => error
|
53
|
-
Failure step: __method__, payload: "#{error.message.capitalize}."
|
53
|
+
Failure step: __method__, constant: self.class, payload: "#{error.message.capitalize}."
|
54
|
+
end
|
55
|
+
|
56
|
+
def merge(*items)
|
57
|
+
case items
|
58
|
+
in Success(all), Success(subset) then Success(all.merge!(subset))
|
59
|
+
else items.last
|
60
|
+
end
|
54
61
|
end
|
55
62
|
end
|
56
63
|
end
|
@@ -9,16 +9,16 @@ module Etcher
|
|
9
9
|
class Environment
|
10
10
|
include Dry::Monads[:result]
|
11
11
|
|
12
|
-
def initialize
|
13
|
-
@
|
14
|
-
@
|
12
|
+
def initialize attributes = ENV, only: Core::EMPTY_ARRAY
|
13
|
+
@attributes = attributes
|
14
|
+
@only = Array only
|
15
15
|
end
|
16
16
|
|
17
|
-
def call = Success
|
17
|
+
def call = Success attributes.slice(*only).transform_keys(&:downcase)
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
attr_reader :
|
21
|
+
attr_reader :attributes, :only
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/monads"
|
4
|
+
|
5
|
+
module Etcher
|
6
|
+
module Loaders
|
7
|
+
# Loads (wraps) raw attributes.
|
8
|
+
class Hash
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
def initialize(**attributes)
|
12
|
+
@attributes = attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
def call = Success attributes
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :attributes
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/etcher/loaders/json.rb
CHANGED
@@ -18,22 +18,33 @@ module Etcher
|
|
18
18
|
|
19
19
|
def call
|
20
20
|
Success ::JSON.load_file(path)
|
21
|
-
rescue
|
22
|
-
|
23
|
-
rescue ::JSON::ParserError => error
|
24
|
-
debug_and_fallback "#{error.message}. Path: #{path_info}. Using fallback."
|
21
|
+
rescue Errno::ENOENT, TypeError then debug_invalid_path
|
22
|
+
rescue ::JSON::ParserError => error then content_failure error
|
25
23
|
end
|
26
24
|
|
27
25
|
private
|
28
26
|
|
29
27
|
attr_reader :path, :fallback, :logger
|
30
28
|
|
31
|
-
def
|
32
|
-
|
33
|
-
def debug_and_fallback message
|
34
|
-
logger.debug { message }
|
29
|
+
def debug_invalid_path
|
30
|
+
logger.debug { "Invalid path: #{path_info}. Using fallback." }
|
35
31
|
Success fallback
|
36
32
|
end
|
33
|
+
|
34
|
+
def content_failure error
|
35
|
+
constant = self.class
|
36
|
+
token = error.message[/(?<token>'.+?')/, :token].to_s.tr "'", ""
|
37
|
+
|
38
|
+
if token.empty?
|
39
|
+
Failure step: :load, constant:, payload: "File is empty: #{path_info}."
|
40
|
+
else
|
41
|
+
Failure step: :load,
|
42
|
+
constant:,
|
43
|
+
payload: "Invalid content: #{token.inspect}. Path: #{path_info}."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def path_info = path.to_s.inspect
|
37
48
|
end
|
38
49
|
end
|
39
50
|
end
|
data/lib/etcher/loaders/yaml.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "core"
|
4
4
|
require "dry/monads"
|
5
|
+
require "refinements/string"
|
5
6
|
require "yaml"
|
6
7
|
|
7
8
|
module Etcher
|
@@ -10,6 +11,8 @@ module Etcher
|
|
10
11
|
class YAML
|
11
12
|
include Dry::Monads[:result]
|
12
13
|
|
14
|
+
using Refinements::String
|
15
|
+
|
13
16
|
def initialize path, fallback: Core::EMPTY_HASH, logger: LOGGER
|
14
17
|
@path = path
|
15
18
|
@fallback = fallback
|
@@ -18,10 +21,10 @@ module Etcher
|
|
18
21
|
|
19
22
|
def call
|
20
23
|
load
|
21
|
-
rescue
|
22
|
-
|
23
|
-
rescue Psych::
|
24
|
-
|
24
|
+
rescue Errno::ENOENT, TypeError then debug_invalid_path
|
25
|
+
rescue Psych::AliasesNotEnabled then alias_failure
|
26
|
+
rescue Psych::DisallowedClass => error then disallowed_failure error
|
27
|
+
rescue Psych::SyntaxError => error then syntax_failure error
|
25
28
|
end
|
26
29
|
|
27
30
|
private
|
@@ -31,17 +34,48 @@ module Etcher
|
|
31
34
|
def load
|
32
35
|
content = ::YAML.safe_load_file path
|
33
36
|
|
34
|
-
|
37
|
+
case content
|
38
|
+
in ::Hash then Success content
|
39
|
+
in nil then empty_failure
|
40
|
+
else invalid_failure content
|
41
|
+
end
|
42
|
+
end
|
35
43
|
|
36
|
-
|
44
|
+
def debug_invalid_path
|
45
|
+
logger.debug { "Invalid path: #{path_info}. Using fallback." }
|
46
|
+
Success fallback
|
37
47
|
end
|
38
48
|
|
39
|
-
def
|
49
|
+
def empty_failure
|
50
|
+
Failure step: :load, constant: self.class, payload: "File is empty: #{path_info}."
|
51
|
+
end
|
40
52
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
53
|
+
def invalid_failure content
|
54
|
+
Failure step: :load,
|
55
|
+
constant: self.class,
|
56
|
+
payload: "Invalid content: #{content.inspect}. Path: #{path_info}."
|
57
|
+
end
|
58
|
+
|
59
|
+
def alias_failure
|
60
|
+
Failure step: :load,
|
61
|
+
constant: self.class,
|
62
|
+
payload: "Aliases are disabled, please remove. Path: #{path_info}."
|
63
|
+
end
|
64
|
+
|
65
|
+
def disallowed_failure error
|
66
|
+
Failure step: :load,
|
67
|
+
constant: self.class,
|
68
|
+
payload: "Invalid type, #{error.message.down}. Path: #{path_info}."
|
44
69
|
end
|
70
|
+
|
71
|
+
def syntax_failure error
|
72
|
+
Failure step: :load,
|
73
|
+
constant: self.class,
|
74
|
+
payload: "Invalid syntax, #{error.message[/found.+/]}. " \
|
75
|
+
"Path: #{path_info}."
|
76
|
+
end
|
77
|
+
|
78
|
+
def path_info = path.to_s.inspect
|
45
79
|
end
|
46
80
|
end
|
47
81
|
end
|
data/lib/etcher/registry.rb
CHANGED
@@ -15,25 +15,30 @@ module Etcher
|
|
15
15
|
super
|
16
16
|
end
|
17
17
|
|
18
|
-
def add_loader(loader,
|
19
|
-
|
20
|
-
|
18
|
+
def add_loader(loader, ...) = add(loader, :Loaders, ...)
|
19
|
+
|
20
|
+
def remove_loader(index) = remove index, loaders
|
21
|
+
|
22
|
+
def add_transformer(transformer, ...) = add(transformer, :Transformers, ...)
|
23
|
+
|
24
|
+
def remove_transformer(index) = remove index, transformers
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def add(item, namespace, ...)
|
29
|
+
collection = __send__ namespace.downcase
|
30
|
+
|
31
|
+
if item.is_a? Symbol
|
32
|
+
self.class.find(namespace, item).then { |kind| collection.append kind.new(...) }
|
21
33
|
else
|
22
|
-
|
34
|
+
collection.append item
|
23
35
|
end
|
24
36
|
|
25
37
|
self
|
26
38
|
end
|
27
39
|
|
28
|
-
def
|
29
|
-
|
30
|
-
self.class.find(:Transformers, transformer).then do |constant|
|
31
|
-
transformers.append constant.new(*, **)
|
32
|
-
end
|
33
|
-
else
|
34
|
-
transformers.append transformer
|
35
|
-
end
|
36
|
-
|
40
|
+
def remove index, collection
|
41
|
+
collection.delete_at index
|
37
42
|
self
|
38
43
|
end
|
39
44
|
end
|
data/lib/etcher/resolver.rb
CHANGED
@@ -10,35 +10,33 @@ module Etcher
|
|
10
10
|
|
11
11
|
using Refinements::Array
|
12
12
|
|
13
|
-
def initialize registry = Registry.new,
|
13
|
+
def initialize registry = Registry.new, logger: LOGGER
|
14
14
|
@builder = Builder.new registry
|
15
|
-
@kernel = kernel
|
16
15
|
@logger = logger
|
17
16
|
end
|
18
17
|
|
19
18
|
def call(**overrides)
|
20
19
|
case builder.call(**overrides)
|
21
20
|
in Success(attributes) then attributes
|
22
|
-
in Failure(step:, payload: String => payload)
|
23
|
-
logger.
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
in Failure(step:, constant:, payload: String => payload)
|
22
|
+
logger.abort "#{step.capitalize} failure (#{constant}). #{payload}"
|
23
|
+
in Failure(step:, constant:, payload: Hash => payload)
|
24
|
+
log_and_abort step, constant, payload
|
25
|
+
in Failure(String => message) then logger.abort message
|
26
|
+
else logger.abort "Unable to parse failure."
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
31
31
|
|
32
|
-
attr_reader :builder, :
|
32
|
+
attr_reader :builder, :logger
|
33
33
|
|
34
|
-
def log_and_abort errors
|
35
|
-
|
36
|
-
|
37
|
-
.join
|
38
|
-
"Unable to load configuration due to the following issues:\n#{details}"
|
39
|
-
end
|
34
|
+
def log_and_abort step, constant, errors
|
35
|
+
details = errors.map { |key, message| " - #{key} #{message.to_sentence}\n" }
|
36
|
+
.join
|
40
37
|
|
41
|
-
|
38
|
+
logger.abort "#{step.capitalize} failure (#{constant}). " \
|
39
|
+
"Unable to load configuration:\n#{details}"
|
42
40
|
end
|
43
41
|
end
|
44
42
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/monads"
|
4
|
+
require "refinements/hash"
|
5
|
+
|
6
|
+
module Etcher
|
7
|
+
module Transformers
|
8
|
+
# Conditionally updates value based on path.
|
9
|
+
class Basename
|
10
|
+
include Dry::Monads[:result]
|
11
|
+
|
12
|
+
using Refinements::Hash
|
13
|
+
|
14
|
+
def initialize key, fallback: Pathname.pwd.basename.to_s
|
15
|
+
@key = key
|
16
|
+
@fallback = fallback
|
17
|
+
end
|
18
|
+
|
19
|
+
def call attributes
|
20
|
+
attributes.fetch_value(key) { attributes.merge! key => fallback }
|
21
|
+
Success attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :key, :fallback
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/monads"
|
4
|
+
|
5
|
+
module Etcher
|
6
|
+
module Transformers
|
7
|
+
# Formats given key using existing and/or placeholder attributes.
|
8
|
+
class Format
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
def initialize key, **pass_throughs
|
12
|
+
@key = key
|
13
|
+
@pass_throughs = pass_throughs
|
14
|
+
@pattern = /%<.+>s/o
|
15
|
+
end
|
16
|
+
|
17
|
+
def call attributes
|
18
|
+
value = attributes[key]
|
19
|
+
|
20
|
+
return Success attributes unless value && value.match?(pattern)
|
21
|
+
|
22
|
+
Success attributes.merge!(key => format(value, **attributes, **pass_throughs))
|
23
|
+
rescue KeyError => error
|
24
|
+
Failure step: :transform,
|
25
|
+
constant: self.class,
|
26
|
+
payload: "Unable to transform #{key.inspect}, missing specifier: " \
|
27
|
+
"\"#{error.message[/<.+>/]}\"."
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :key, :pass_throughs, :pattern
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/monads"
|
4
|
+
require "refinements/hash"
|
5
|
+
|
6
|
+
module Etcher
|
7
|
+
module Transformers
|
8
|
+
# Conditionally updates value based on path.
|
9
|
+
class Root
|
10
|
+
include Dry::Monads[:result]
|
11
|
+
|
12
|
+
using Refinements::Hash
|
13
|
+
|
14
|
+
def initialize key, fallback: Pathname.pwd
|
15
|
+
@key = key
|
16
|
+
@fallback = fallback
|
17
|
+
end
|
18
|
+
|
19
|
+
def call attributes
|
20
|
+
value = attributes.fetch_value(key) { fallback }
|
21
|
+
Success attributes.merge!(key => Pathname(value).expand_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :key, :fallback
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -8,14 +8,14 @@ module Etcher
|
|
8
8
|
class Time
|
9
9
|
include Dry::Monads[:result]
|
10
10
|
|
11
|
-
def initialize key
|
11
|
+
def initialize key, fallback: ::Time.now.utc
|
12
12
|
@key = key
|
13
13
|
@fallback = fallback
|
14
14
|
end
|
15
15
|
|
16
16
|
def call attributes
|
17
|
-
attributes.fetch(key) { fallback }
|
18
|
-
|
17
|
+
attributes.fetch(key) { attributes.merge! key => fallback }
|
18
|
+
Success attributes
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: etcher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brooke Kuhlmann
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
|
36
36
|
gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2024-05
|
38
|
+
date: 2024-07-05 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: cogger
|
@@ -43,14 +43,14 @@ dependencies:
|
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '0.
|
46
|
+
version: '0.21'
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '0.
|
53
|
+
version: '0.21'
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: core
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,14 +99,14 @@ dependencies:
|
|
99
99
|
requirements:
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version: '12.
|
102
|
+
version: '12.5'
|
103
103
|
type: :runtime
|
104
104
|
prerelease: false
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version: '12.
|
109
|
+
version: '12.5'
|
110
110
|
- !ruby/object:Gem::Dependency
|
111
111
|
name: versionaire
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -152,11 +152,14 @@ files:
|
|
152
152
|
- lib/etcher/contract.rb
|
153
153
|
- lib/etcher/finder.rb
|
154
154
|
- lib/etcher/loaders/environment.rb
|
155
|
+
- lib/etcher/loaders/hash.rb
|
155
156
|
- lib/etcher/loaders/json.rb
|
156
157
|
- lib/etcher/loaders/yaml.rb
|
157
158
|
- lib/etcher/registry.rb
|
158
159
|
- lib/etcher/resolver.rb
|
159
|
-
- lib/etcher/transformers/
|
160
|
+
- lib/etcher/transformers/basename.rb
|
161
|
+
- lib/etcher/transformers/format.rb
|
162
|
+
- lib/etcher/transformers/root.rb
|
160
163
|
- lib/etcher/transformers/time.rb
|
161
164
|
- lib/etcher/types.rb
|
162
165
|
homepage: https://alchemists.io/projects/etcher
|
@@ -185,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
185
188
|
- !ruby/object:Gem::Version
|
186
189
|
version: '0'
|
187
190
|
requirements: []
|
188
|
-
rubygems_version: 3.5.
|
191
|
+
rubygems_version: 3.5.14
|
189
192
|
signing_key:
|
190
193
|
specification_version: 4
|
191
194
|
summary: A monadic configuration loader, transformer, and validator.
|
metadata.gz.sig
CHANGED
Binary file
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "dry/monads"
|
4
|
-
|
5
|
-
module Etcher
|
6
|
-
module Transformers
|
7
|
-
# Formats given key using existing and/or ancillary attributes.
|
8
|
-
class String
|
9
|
-
include Dry::Monads[:result]
|
10
|
-
|
11
|
-
def initialize key, **ancillary
|
12
|
-
@key = key
|
13
|
-
@ancillary = ancillary
|
14
|
-
end
|
15
|
-
|
16
|
-
def call attributes
|
17
|
-
value = attributes[key]
|
18
|
-
|
19
|
-
return Success attributes unless value
|
20
|
-
|
21
|
-
Success attributes.merge(key => format(value, **attributes, **ancillary))
|
22
|
-
rescue KeyError => error
|
23
|
-
Failure "Unable to transform #{key.inspect}, missing specifier: " \
|
24
|
-
"\"#{error.message[/<.+>/]}\"."
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
attr_reader :key, :ancillary
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|