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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e462682ab4cb1a7149595a36e1d2893f329db37813209655c53a6fd767e9751
4
- data.tar.gz: 2f1a5bd597dfa8590d6ad48f9bd92de9688023b14a40c3856842d4539c9e3ca7
3
+ metadata.gz: 18c97ac57c0742fedf8872da7aa401ceae29552ccc9e78ce0d8df72fc8fff08c
4
+ data.tar.gz: feb2442ddc16419bc49dc97ba1e449e226713d57a2c1fd6714e8592479faf636
5
5
  SHA512:
6
- metadata.gz: 37067ae8933e3512d3be4a0e10147cad3813a76c969c414801b915b31c164bd1154b11dff58bb1196857e7f30b5fc9455ade7f305824ddf2a0c61c7149eec610
7
- data.tar.gz: d37ae593f29bc0a8c2402dc39bc4d6df37478baec964480b76bbd94a23b00ff17d4236d21deff7e57b92cd18ed1d0f5cc29f00f7b7c907c0c6158c7ecf8543a7
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 and pairs well with the {xdg_link}, {runcom_link}, and {sod_link} gems.
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 detail below.
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 is provided as a way to register any/all complexity for before creating a new Etcher instance. Here's what you get by default:
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
- The best gems which adhere to this interface are: {dry_schema_link} and {dry_validation_link}. You'll also want to make sure the {dry_monads_link} extensions are loaded, as briefly shown earlier, so the result will respond to `#to_monad`. Here's how to enable monad support if using both gems:
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 and it would be nice to structured record which will be explained shortly.
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 few examples:
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
- * They must respond to `#call` with no arguments.
296
- * All keys are symbolized which helps streamline merging and overriding values from the same keys across multiple configurations.
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
- Otherwise, if the file exists with content, you'll get a `Hash` wrapped as a `Success`.
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
- ℹ️ The logger is only used to log debug information when issues are encountered when reading from the file. This is done to reduce noise in your console when a configuration might have issues and can safely revert to the fallback in order to load the rest of the configuration.
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
- Otherwise, if the file exists with content, you'll get a `Hash` wrapped as a `Success`.
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
- ℹ️ The logger is only used to log debug information when issues are encountered when reading from the file. This is done to reduce noise in your console when a configuration might have issues and can safely revert to the fallback in order to load the rest of the configuration.
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 fallback: {}
406
- @fallback = fallback
432
+ def initialize processor: Processor.new
433
+ @processor = processor
407
434
  end
408
435
 
409
- def call = Success fallback
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 :fallback
444
+ attr_reader :processor
414
445
  end
415
446
 
416
- etcher = Etcher::Registry[loaders: [Demo.new]].then { |registry| Etcher.new registry }
417
- etcher.call # Success({})
447
+ registry = Etcher::Registry[loaders: [Demo.new]]
448
+
449
+ Etcher.new(registry).call
418
450
  ----
419
451
 
420
- While the above isn't super useful since it only answers whatever you provide as fallback information, you can see there is little effort required to implement and customize as desired.
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 modifying specific keys and values. They give you finer grained control over your configuration and are the last step before validating and creating an associated record of your configuration. Transformers can either be defined when creating a new registry instance or added after the fact. Here are a few examples:
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
- * When using a proc/lambda, the first, _required_, parameter should be the `attributes` parameter followed by an _optional_ positional `key` parameter with a default value. This allows you to quickly refactor the key later while also reducing key duplication throughout your implementation.
440
- * When using a class, the `key` should be your first positional parameter with a default value. Additional parameters can be supplied after if desired.
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
- # String
496
- registry.add_transformer :string, :project_uri
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
- ==== String
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::String` to transform any key in your configuration by using the configuration's existing keys. Example:
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
- transformer = Etcher::Transformers::String.new :project_uri
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
- # Failure("Unable to transform :project_uri, missing specifier: \"<project_name>\".")
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 `loaded_at` 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.
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
- Even though `loaded_at` is the default key and `Time.now.utc` is the default fallback, you're not limited to using different keys and fallbacks. Example:
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-05-23 22:18:49.93189 UTC})
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({loaded_at: Time.utc(2000, 1, 1)})
554
- # Success({:loaded_at=>2000-01-01 00:00:00 UTC})
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 loaders are processed and _before_ any transformations. They are a nice way to deal with user input during runtime or provide any additional configuration not supplied by the loading of your default configuration while still allowing you to transform them if desired.
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 build 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:
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
- # 🔥 Unable to load configuration due to the following issues:
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
- # 🔥 Unable to load configuration due to the following issues:
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
- # 🔥 Build failure: :record. Missing keywords: :name, :label.
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. All arguments afterwards can be any number of key/values overrides which is similar to how `Etcher.new` works.
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/doc/architecture.svg[Architecture Diagram]
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 = "1.6.0"
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.15"
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.1"
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
 
@@ -16,27 +16,25 @@ module Etcher
16
16
  end
17
17
 
18
18
  def call(**overrides)
19
- load(overrides.symbolize_keys!).then { |attributes| transform attributes }
20
- .bind { |attributes| validate attributes }
21
- .bind { |attributes| model attributes }
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 overrides
29
+ def load
29
30
  registry.loaders
30
31
  .map { |loader| loader.call.fmap { |pairs| pairs.flatten_keys.symbolize_keys! } }
31
- .each
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.bind { |body| transformer.call body }
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 { |result| Failure step: __method__, payload: result.errors.to_h }
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 includes = Core::EMPTY_ARRAY, source: ENV
13
- @includes = Array includes
14
- @source = source
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 source.slice(*includes).transform_keys(&:downcase)
17
+ def call = Success attributes.slice(*only).transform_keys(&:downcase)
18
18
 
19
19
  private
20
20
 
21
- attr_reader :includes, :source
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
@@ -18,22 +18,33 @@ module Etcher
18
18
 
19
19
  def call
20
20
  Success ::JSON.load_file(path)
21
- rescue TypeError, Errno::ENOENT
22
- debug_and_fallback "Invalid path: #{path_info}. Using fallback."
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 path_info = path.to_s.inspect
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
@@ -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 TypeError, Errno::ENOENT
22
- debug_and_fallback "Invalid path: #{path_info}. Using fallback."
23
- rescue Psych::Exception => error
24
- debug_and_fallback "#{error.message}. Path: #{path_info}. Using fallback."
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
- return Success content if content.is_a? Hash
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
- debug_and_fallback "Invalid content: #{content.inspect}. Using fallback."
44
+ def debug_invalid_path
45
+ logger.debug { "Invalid path: #{path_info}. Using fallback." }
46
+ Success fallback
37
47
  end
38
48
 
39
- def path_info = path.to_s.inspect
49
+ def empty_failure
50
+ Failure step: :load, constant: self.class, payload: "File is empty: #{path_info}."
51
+ end
40
52
 
41
- def debug_and_fallback message
42
- logger.debug { message }
43
- Success fallback
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
@@ -15,25 +15,30 @@ module Etcher
15
15
  super
16
16
  end
17
17
 
18
- def add_loader(loader, *, **)
19
- if loader.is_a? Symbol
20
- self.class.find(:Loaders, loader).then { |constant| loaders.append constant.new(*, **) }
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
- loaders.append loader
34
+ collection.append item
23
35
  end
24
36
 
25
37
  self
26
38
  end
27
39
 
28
- def add_transformer(transformer, *, **)
29
- if transformer.is_a? Symbol
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
@@ -10,35 +10,33 @@ module Etcher
10
10
 
11
11
  using Refinements::Array
12
12
 
13
- def initialize registry = Registry.new, kernel: Kernel, logger: LOGGER
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.fatal { "Build failure: #{step.inspect}. #{payload}" }
24
- kernel.abort
25
- in Failure(step:, payload: Hash => payload) then log_and_abort payload
26
- else fail StandardError, "Unable to parse configuration."
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, :kernel, :logger
32
+ attr_reader :builder, :logger
33
33
 
34
- def log_and_abort errors
35
- logger.fatal do
36
- details = errors.map { |key, message| " - #{key} #{message.to_sentence}\n" }
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
- kernel.abort
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 = :loaded_at, fallback: ::Time.now.utc
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
- .then { |value| Success attributes.merge!(key => value) }
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: 1.6.0
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-31 00:00:00.000000000 Z
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.15'
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.15'
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.1'
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.1'
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/string.rb
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.11
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