foobara 0.0.37 → 0.0.39
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +654 -202
- data/projects/builtin_types/src/builtin_types.rb +31 -9
- data/projects/command/src/concerns/domain_mappers.rb +109 -19
- data/projects/common/src/error.rb +4 -1
- data/projects/detached_entity/src/extensions/type_declarations/handlers/extend_detached_entity_type_declaration/to_type_transformer.rb +1 -0
- data/projects/domain_mapper/src/domain_mapper_lookups.rb +7 -3
- data/projects/entity/src/extensions/builtin_types/entity/casters/primary_key.rb +39 -0
- data/projects/entity/src/extensions/type_declarations/handlers/extend_entity_type_declaration/to_type_transformer.rb +0 -31
- data/projects/model/src/extensions/builtin_types/model/{transformers → supported_transformers}/mutable.rb +5 -1
- data/projects/model/src/extensions/builtin_types/model/validators/attributes_declaration.rb +4 -0
- data/projects/model/src/extensions/type_declarations/handlers/extend_model_type_declaration/to_type_transformer.rb +1 -1
- data/projects/model/src/extensions/type_declarations/handlers/extend_registered_model_type_declaration/to_type_transformer.rb +0 -4
- data/projects/types/src/type.rb +48 -1
- metadata +6 -7
data/README.md
CHANGED
@@ -1,13 +1,20 @@
|
|
1
|
+
Foobara is a software framework with a focus on projects that have
|
2
|
+
a complicated business domain. It accomplishes this by helping to
|
3
|
+
build projects that are command-centric and discoverable, as well as some other features that aid in the mission.
|
4
|
+
|
5
|
+
You can watch a video that gives a good overview of what Foobara is and its goals here:
|
6
|
+
[Introduction to the Foobara software framework](https://youtu.be/SSOmQqjNSVY)
|
7
|
+
|
1
8
|
<!-- TOC -->
|
2
|
-
* [
|
3
|
-
* [
|
9
|
+
* [Overview of Features/Concepts/Goals](#overview-of-featuresconceptsgoals)
|
10
|
+
* [Command-centric](#command-centric)
|
4
11
|
* [Discoverability](#discoverability)
|
5
12
|
* [Implications of command-centric + discoverability](#implications-of-command-centric--discoverability)
|
6
13
|
* [Other features for helping with Domain complexity](#other-features-for-helping-with-domain-complexity)
|
7
14
|
* [Installation](#installation)
|
8
15
|
* [Usage/Tutorial](#usagetutorial)
|
9
16
|
* [Foobara 101](#foobara-101)
|
10
|
-
* [Commands](#commands
|
17
|
+
* [Commands](#commands)
|
11
18
|
* [Organizations and Domains](#organizations-and-domains)
|
12
19
|
* [Types](#types)
|
13
20
|
* [Models](#models)
|
@@ -15,6 +22,8 @@
|
|
15
22
|
* [Command connectors](#command-connectors)
|
16
23
|
* [Command-line connectors](#command-line-connectors)
|
17
24
|
* [HTTP Command Connectors](#http-command-connectors)
|
25
|
+
* [Rack Connector](#rack-connector)
|
26
|
+
* [Rails Connector](#rails-connector)
|
18
27
|
* [Async Command Connectors](#async-command-connectors)
|
19
28
|
* [Scheduler Command Connectors](#scheduler-command-connectors)
|
20
29
|
* [Intermediate Foobara](#intermediate-foobara)
|
@@ -26,20 +35,21 @@
|
|
26
35
|
* [Runtime Errors](#runtime-errors)
|
27
36
|
* [Advanced Foobara](#advanced-foobara)
|
28
37
|
* [Domain Mappers](#domain-mappers)
|
38
|
+
* [Types](#types-1)
|
39
|
+
* [Builtin types](#builtin-types)
|
40
|
+
* [Custom types](#custom-types)
|
29
41
|
* [Code Generators](#code-generators)
|
30
42
|
* [Generating a new Foobara Ruby project](#generating-a-new-foobara-ruby-project)
|
31
43
|
* [Generating a new Foobara Typescript/React project](#generating-a-new-foobara-typescriptreact-project)
|
32
|
-
* [Geerating commands, models, entities, types, domains, organizations, etc...](#geerating-commands-models-entities-types-domains-organizations-etc)
|
33
|
-
* [Custom types](#custom-types)
|
34
44
|
* [Expert Foobara](#expert-foobara)
|
35
45
|
* [Callbacks](#callbacks)
|
36
46
|
* [Transactions in Commands](#transactions-in-commands)
|
37
47
|
* [Transactions in tests/console](#transactions-in-testsconsole)
|
38
48
|
* [Custom crud drivers](#custom-crud-drivers)
|
39
49
|
* [Custom command connectors](#custom-command-connectors)
|
40
|
-
* [Value processors](#value-processors)
|
41
50
|
* [Custom types from scratch](#custom-types-from-scratch)
|
42
51
|
* [Namespaces](#namespaces)
|
52
|
+
* [Value processors](#value-processors)
|
43
53
|
* [Additional learning materials/Documentation](#additional-learning-materialsdocumentation)
|
44
54
|
* [Contributing](#contributing)
|
45
55
|
* [Developing locally](#developing-locally)
|
@@ -47,13 +57,9 @@
|
|
47
57
|
* [Licensing](#licensing)
|
48
58
|
<!-- TOC -->
|
49
59
|
|
50
|
-
#
|
60
|
+
# Overview of Features/Concepts/Goals
|
51
61
|
|
52
|
-
|
53
|
-
a complicated business domain. It accomplishes this by helping to
|
54
|
-
build projects that are command-centric and discoverable, as well as some other features.
|
55
|
-
|
56
|
-
## Commands
|
62
|
+
## Command-centric
|
57
63
|
|
58
64
|
* Foobara commands are meant to encapsulate high-level domain operations.
|
59
65
|
* They serve as the public interface to Foobara systems/subsystems.
|
@@ -122,6 +128,9 @@ Let's explore various Foobara concepts with some code examples!
|
|
122
128
|
|
123
129
|
## Foobara 101
|
124
130
|
|
131
|
+
NOTE: We will create various scripts for the first parts of these tutorials but normally we'd generate a project, which
|
132
|
+
will be covered later in the tutorial.
|
133
|
+
|
125
134
|
### Commands
|
126
135
|
|
127
136
|
Foobara commands are meant to encapsulate high-level domain operations and are meant
|
@@ -176,7 +185,7 @@ Let's play with it!
|
|
176
185
|
|
177
186
|
We can run our Add command several ways. First, let's create an instance of it and call the #run method:
|
178
187
|
|
179
|
-
```
|
188
|
+
```
|
180
189
|
$ ./add.rb
|
181
190
|
> command = Add.new(operand1: 2, operand2: 5)
|
182
191
|
==> #<Add:0xad20 @raw_inputs={:operand1=>2, :operand2=>5}, @error_collectio...
|
@@ -193,7 +202,7 @@ we can also get the result with #result and errors with #errors and other helper
|
|
193
202
|
|
194
203
|
We can also just run it with .run without creating an instance:
|
195
204
|
|
196
|
-
```
|
205
|
+
```
|
197
206
|
> outcome = Add.run(operand1: 2, operand2: 5)
|
198
207
|
==> #<Foobara::Outcome:0x00007ffbcc641318...
|
199
208
|
> outcome.success?
|
@@ -204,14 +213,14 @@ We can also just run it with .run without creating an instance:
|
|
204
213
|
|
205
214
|
And we can use .run! if we want just the result or an exception raised:
|
206
215
|
|
207
|
-
```
|
216
|
+
```
|
208
217
|
> Add.run!(operand1: 2, operand2: 5)
|
209
218
|
==> 7
|
210
219
|
```
|
211
220
|
|
212
221
|
Let's cause some errors!
|
213
222
|
|
214
|
-
```
|
223
|
+
```
|
215
224
|
> outcome = Add.run(operand1: "foo", operand2: 5)
|
216
225
|
==> #<Foobara::Outcome:0x00007ffbcc60aea8...
|
217
226
|
> outcome.success?
|
@@ -222,7 +231,7 @@ At operand1: Cannot cast "foo" to an integer. Expected it to be a Integer, or be
|
|
222
231
|
|
223
232
|
Here we used something that wasn't castable to an integer.
|
224
233
|
|
225
|
-
```
|
234
|
+
```
|
226
235
|
> outcome = Add.run
|
227
236
|
==> #<Foobara::Outcome:0x00007ffbcb9d97b0...
|
228
237
|
> outcome.success?
|
@@ -276,7 +285,7 @@ The typical way of putting commands and other Foobara concepts into a domain is
|
|
276
285
|
|
277
286
|
We can play a bit with our new domain:
|
278
287
|
|
279
|
-
```
|
288
|
+
```
|
280
289
|
> IntegerMath.foobara_command_classes
|
281
290
|
==> [IntegerMath::Add]
|
282
291
|
> IntegerMath.foobara_lookup(:Add)
|
@@ -321,7 +330,7 @@ end
|
|
321
330
|
|
322
331
|
And we can play with our Organization:
|
323
332
|
|
324
|
-
```
|
333
|
+
```
|
325
334
|
> FoobaraExamples.foobara_domains
|
326
335
|
==> [FoobaraExamples::IntegerMath]
|
327
336
|
```
|
@@ -409,7 +418,7 @@ end
|
|
409
418
|
|
410
419
|
Let's increment some ages!
|
411
420
|
|
412
|
-
```
|
421
|
+
```
|
413
422
|
> barbara = Capybara.new(name: "Barbara", age: 200, nickname: "bar")
|
414
423
|
==> #<Capybara:0x00007f0ac121dbf8 @attributes={:name=>"Barbara", :age=>200, :nickname=>"bar"}, @mutable=true>
|
415
424
|
> barbara.age
|
@@ -424,7 +433,7 @@ Here we incremented Barbara's age.
|
|
424
433
|
|
425
434
|
Check this out though...
|
426
435
|
|
427
|
-
```
|
436
|
+
```
|
428
437
|
> basil = IncrementAge.run!(capybara: { name: "Basil", age: 300, nickname: "baz" })
|
429
438
|
==> #<Capybara:0x00007f0ac1295f40 @attributes={:name=>"Basil", :age=>301, :nickname=>"baz"}, @mutable=true>
|
430
439
|
> basil.age
|
@@ -458,73 +467,7 @@ Here, we added an InMemory CRUD driver and set it as the default. This lets us w
|
|
458
467
|
|
459
468
|
An entity is like a model except it has a primary key and can be written/read to/from a data store using a CRUD driver.
|
460
469
|
|
461
|
-
|
462
|
-
|
463
|
-
```ruby
|
464
|
-
def print_type_inheritance(type)
|
465
|
-
types = Enumerator.produce(type, &:base_type).take_while { |t| !t.nil? }
|
466
|
-
Foobara::Util.print_tree(types, to_parent: :base_type, to_name: :name)
|
467
|
-
end
|
468
|
-
|
469
|
-
capybara_type = Foobara.foobara_lookup(:Capybara)
|
470
|
-
print_type_inheritance(capybara_type)
|
471
|
-
```
|
472
|
-
|
473
|
-
Which gives us:
|
474
|
-
|
475
|
-
```irb
|
476
|
-
|
477
|
-
* def print_type_inheritance(type)
|
478
|
-
* types = Enumerator.produce(type, &:base_type).take_while { |t| !t.nil? }
|
479
|
-
* Foobara::Util.print_tree(types, to_parent: :base_type, to_name: :name)
|
480
|
-
> end
|
481
|
-
==> :print_type_inheritance
|
482
|
-
> capybara_type = Foobara.foobara_lookup(:Capybara)
|
483
|
-
==> #<Type:Capybara:0x88b8 {:type=>:model, :name=>"Capybara", :model_class=>"Capybara", :model_base_class=>"Foobara::Model", :attributes_declaration=>{:typ...
|
484
|
-
> print_type_inheritance(capybara_type)
|
485
|
-
> print_type_inheritance(capybara_type)
|
486
|
-
╭──────╮
|
487
|
-
│ duck │
|
488
|
-
╰──┬───╯
|
489
|
-
│ ╭─────────────╮
|
490
|
-
└─┤ atomic_duck │
|
491
|
-
╰──────┬──────╯
|
492
|
-
│ ╭───────╮
|
493
|
-
└─┤ model │
|
494
|
-
╰───┬───╯
|
495
|
-
│ ╭────────╮
|
496
|
-
└─┤ entity │
|
497
|
-
╰───┬────╯
|
498
|
-
│ ╭──────────╮
|
499
|
-
└─┤ Capybara │
|
500
|
-
╰──────────╯
|
501
|
-
```
|
502
|
-
|
503
|
-
While we're in here we could look at another type, like Capybara's attributes type
|
504
|
-
|
505
|
-
```irb
|
506
|
-
> print_type_inheritance(Capybara.attributes_type)
|
507
|
-
╭──────╮
|
508
|
-
│ duck │
|
509
|
-
╰──┬───╯
|
510
|
-
│ ╭──────────╮
|
511
|
-
└─┤ duckture │
|
512
|
-
╰────┬─────╯
|
513
|
-
│ ╭───────────────────╮
|
514
|
-
└─┤ associative_array │
|
515
|
-
╰─────────┬─────────╯
|
516
|
-
│ ╭────────────╮
|
517
|
-
└─┤ attributes │
|
518
|
-
╰─────┬──────╯
|
519
|
-
│ ╭────────────────────────────────╮
|
520
|
-
└─┤ Anonymous attributes extension │
|
521
|
-
╰────────────────────────────────╯
|
522
|
-
|
523
|
-
```
|
524
|
-
|
525
|
-
Whoa... this is supposed to be Foobara 101... let's get back to basics.
|
526
|
-
|
527
|
-
Let's make a basic CreateCapybara command:
|
470
|
+
Let's make a CreateCapybara command that creates a Capybara record for us:
|
528
471
|
|
529
472
|
```ruby
|
530
473
|
class CreateCapybara < Foobara::Command
|
@@ -573,7 +516,7 @@ end
|
|
573
516
|
|
574
517
|
And now let's create some Capybara records and manipulate them:
|
575
518
|
|
576
|
-
```
|
519
|
+
```
|
577
520
|
> fumiko = CreateCapybara.run!(name: "Fumiko", nickname: "foo", age: 100)
|
578
521
|
==> <Capybara:1>
|
579
522
|
> barbara = CreateCapybara.run!(name: "Barbara", nickname: "bar", age: 200)
|
@@ -597,8 +540,8 @@ We were able to increment Basil's age using his primary key and we were also abl
|
|
597
540
|
But there is a problem... Basil's record won't be persisted across runs of our script. That's because it is stored in
|
598
541
|
ephemeral memory. Let's instead persist it to a file. Let's install a file crud driver:
|
599
542
|
|
600
|
-
```
|
601
|
-
|
543
|
+
```
|
544
|
+
$ gem install foobara-local-files-crud-driver
|
602
545
|
```
|
603
546
|
|
604
547
|
And now let's swap out the InMemory crud driver with our file crud driver:
|
@@ -612,7 +555,7 @@ Foobara::Persistence.default_crud_driver = crud_driver
|
|
612
555
|
|
613
556
|
Now let's create our records again and look at them on disk:
|
614
557
|
|
615
|
-
```
|
558
|
+
```
|
616
559
|
> CreateCapybara.run!(name: "Fumiko", nickname: "foo", age: 100)
|
617
560
|
==> <Capybara:1>
|
618
561
|
> CreateCapybara.run!(name: "Barbara", nickname: "bar", age: 200)
|
@@ -643,7 +586,7 @@ capybara:
|
|
643
586
|
|
644
587
|
Great! Now let's re-run our script and manipulate some data:
|
645
588
|
|
646
|
-
```
|
589
|
+
```
|
647
590
|
> basil = FindCapybara.run!(id: 3)
|
648
591
|
==> <Capybara:3>
|
649
592
|
> basil.age
|
@@ -658,7 +601,7 @@ We were able to find Basil in a fresh run of our script!
|
|
658
601
|
|
659
602
|
Let's find Basil again in another fresh run:
|
660
603
|
|
661
|
-
```
|
604
|
+
```
|
662
605
|
> basil = FindCapybara.run!(id: 3)
|
663
606
|
==> <Capybara:3>
|
664
607
|
> basil.age
|
@@ -675,7 +618,7 @@ Command connectors allow us to expose our commands to the outside world using va
|
|
675
618
|
|
676
619
|
Let's install a command-line connector for bash:
|
677
620
|
|
678
|
-
```
|
621
|
+
```
|
679
622
|
gem install foobara-sh-cli-connector
|
680
623
|
```
|
681
624
|
|
@@ -698,7 +641,7 @@ And either rename the script to capy-cafe or symlink it.
|
|
698
641
|
|
699
642
|
Now let's run our script again:
|
700
643
|
|
701
|
-
```
|
644
|
+
```
|
702
645
|
$ ./capy-cafe
|
703
646
|
Usage: capy-cafe [GLOBAL_OPTIONS] [ACTION] [COMMAND_OR_TYPE] [COMMAND_INPUTS]
|
704
647
|
|
@@ -760,6 +703,8 @@ Yay! Now Basil is an even more respectable 302 years old!
|
|
760
703
|
|
761
704
|
#### HTTP Command Connectors
|
762
705
|
|
706
|
+
##### Rack Connector
|
707
|
+
|
763
708
|
Let's now replace our command-line connector with an HTTP connector:
|
764
709
|
|
765
710
|
We'll choose a Rack connector for now:
|
@@ -816,7 +761,7 @@ Yay! We found Fumiko!
|
|
816
761
|
|
817
762
|
Let's celebrate her birthday:
|
818
763
|
|
819
|
-
```
|
764
|
+
```
|
820
765
|
$ curl http://localhost:9292/run/IncrementAge?capybara=1
|
821
766
|
{"id":1,"name":"Fumiko","nickname":"foo","age":101}
|
822
767
|
$ curl http://localhost:9292/run/FindCapybara?id=1
|
@@ -825,11 +770,13 @@ $ curl http://localhost:9292/run/FindCapybara?id=1
|
|
825
770
|
|
826
771
|
And now she is 101 as expected.
|
827
772
|
|
773
|
+
##### Rails Connector
|
774
|
+
|
828
775
|
Let's try exposing our commands through the Rails router.
|
829
776
|
|
830
777
|
We'll create an a test rails app with (you can just do --api if you are too lazy to skip all the other stuff):
|
831
778
|
|
832
|
-
```
|
779
|
+
```
|
833
780
|
gem install rails
|
834
781
|
rails rails new --api --skip-docker --skip-asset-pipeline --skip-javascript --skip-hotwire --skip-jbuilder --skip-test --skip-brakeman --skip-kamal --skip-solid rails_test_app
|
835
782
|
```
|
@@ -883,11 +830,163 @@ This has the same effect as the previous code and is just a stylistic alternativ
|
|
883
830
|
|
884
831
|
#### Async Command Connectors
|
885
832
|
|
886
|
-
|
833
|
+
Let's connect a command to some sort of async job solution. We'll connect our IncrementAge command to Resque:
|
834
|
+
|
835
|
+
```ruby
|
836
|
+
require "foobara/resque_connector"
|
837
|
+
|
838
|
+
async_connector = Foobara::CommandConnectors::ResqueConnector.new
|
839
|
+
|
840
|
+
async_connector.connect(IncrementAge)
|
841
|
+
```
|
842
|
+
|
843
|
+
This gives us a new command called IncrementAgeAsync. Let's expose this new command to our CLI connector and try
|
844
|
+
it out on the command line:
|
845
|
+
|
846
|
+
```ruby
|
847
|
+
|
848
|
+
require "foobara/resque_connector"
|
849
|
+
|
850
|
+
async_connector = Foobara::CommandConnectors::ResqueConnector.new
|
851
|
+
|
852
|
+
async_connector.connect(IncrementAge)
|
853
|
+
|
854
|
+
require "foobara/sh_cli_connector"
|
855
|
+
|
856
|
+
cli_connector = Foobara::CommandConnectors::ShCliConnector.new
|
857
|
+
|
858
|
+
cli_connector.connect(IncrementAge)
|
859
|
+
cli_connector.connect(IncrementAgeAsync)
|
860
|
+
cli_connector.connect(FindCapybara)
|
861
|
+
|
862
|
+
cli_connector.run(ARGV)
|
863
|
+
```
|
864
|
+
|
865
|
+
And now let's call it:
|
866
|
+
|
867
|
+
```
|
868
|
+
$ ./part_5b_async_command_connector.rb FindCapybara --id 1 | grep age
|
869
|
+
age: 100
|
870
|
+
$ ./part_5a_async_command_connector.rb IncrementAgeAsync --capybara 1
|
871
|
+
true
|
872
|
+
$ /part_5b_async_command_connector.rb FindCapybara --id 1 | grep age
|
873
|
+
age: 100
|
874
|
+
```
|
875
|
+
|
876
|
+
So we can see that we only got back "true" when we ran IncrementAgeAsync. But the age still hasn't gone up.
|
877
|
+
This is because we're not running a worker. Normally, one would fire up a worker on the command line with
|
878
|
+
`rake resque:work`, however, we will just hack something up so we can stay within one script. We normally wouldn't
|
879
|
+
make such a hack in a real project with multiple files. But here's our hack:
|
880
|
+
|
881
|
+
```ruby
|
882
|
+
...
|
883
|
+
|
884
|
+
require "foobara/resque_connector"
|
885
|
+
|
886
|
+
async_connector = Foobara::CommandConnectors::ResqueConnector.new
|
887
|
+
|
888
|
+
async_connector.connect(IncrementAge)
|
889
|
+
|
890
|
+
require "foobara/sh_cli_connector"
|
891
|
+
|
892
|
+
cli_connector = Foobara::CommandConnectors::ShCliConnector.new
|
893
|
+
|
894
|
+
cli_connector.connect(IncrementAge)
|
895
|
+
cli_connector.connect(IncrementAgeAsync)
|
896
|
+
cli_connector.connect(FindCapybara)
|
897
|
+
|
898
|
+
if ARGV == ["work"]
|
899
|
+
worker = Resque::Worker.new("*")
|
900
|
+
worker.verbose = true
|
901
|
+
worker.work(1)
|
902
|
+
else
|
903
|
+
cli_connector.run(ARGV)
|
904
|
+
end
|
905
|
+
```
|
906
|
+
|
907
|
+
So now we can run it with just "work" to fire up a worker and process our async jobs:
|
908
|
+
|
909
|
+
```
|
910
|
+
$ ./part_5b_async_command_connector.rb work
|
911
|
+
*** got: (Job{general} | Foobara::CommandConnectors::ResqueConnector::CommandJob | [{"command_name"=>"IncrementAge", "inputs"=>{"capybara"=>"1"}}])
|
912
|
+
*** done: (Job{general} | Foobara::CommandConnectors::ResqueConnector::CommandJob | [{"command_name"=>"IncrementAge", "inputs"=>{"capybara"=>"1"}}])
|
913
|
+
```
|
914
|
+
|
915
|
+
And now let's check Fumiko's age:
|
916
|
+
|
917
|
+
```
|
918
|
+
$ /part_5b_async_command_connector.rb FindCapybara --id 1 | grep age
|
919
|
+
age: 101
|
920
|
+
```
|
921
|
+
|
922
|
+
Cool! We asynchronously ran our IncrementAge command and we did it without writing a Resque job! That's cool
|
923
|
+
because it means we can't accidentally place domain logic in our job code because there is no job code.
|
887
924
|
|
888
925
|
#### Scheduler Command Connectors
|
889
926
|
|
890
|
-
|
927
|
+
Let's connect a command to a scheduler now. We will use resque-scheduler in this example:
|
928
|
+
|
929
|
+
```ruby
|
930
|
+
require "foobara/resque_scheduler_connector"
|
931
|
+
|
932
|
+
cron_connector = Foobara::CommandConnectors::ResqueSchedulerConnector.new
|
933
|
+
|
934
|
+
cron_connector.cron(
|
935
|
+
[
|
936
|
+
# ╭─Second (0-59)
|
937
|
+
# │ ╭─Minute (0-59)
|
938
|
+
# │ │ ╭─Hour (0-23)
|
939
|
+
# │ │ │ ╭─Day-of-Month (1-31)
|
940
|
+
# │ │ │ │ ╭─Month (1-12)
|
941
|
+
# │ │ │ │ │ ╭─Day-of-Week (0-6)
|
942
|
+
# │ │ │ │ │ │ ╭─Timezone
|
943
|
+
# │ │ │ │ │ │ │ ╭─Command, ╭─Inputs
|
944
|
+
["*/5 * * * * * ", IncrementAge, { capybara: 1 }]
|
945
|
+
]
|
946
|
+
)
|
947
|
+
```
|
948
|
+
|
949
|
+
We could connect IncrementAge to our connector and that would give us a IncrementAgeAsyncAt command which takes a time
|
950
|
+
when we want the command to be ran. But in this example we will make a recurring job. We will increment Fumiko's age
|
951
|
+
every 5 seconds. That's 6307200 birthdays a year for the curious (moar in leap years.)
|
952
|
+
|
953
|
+
We will expand our hack to fire up a scheduler in a separate thread:
|
954
|
+
|
955
|
+
```ruby
|
956
|
+
...
|
957
|
+
|
958
|
+
if ARGV == ["work"]
|
959
|
+
Thread.new do
|
960
|
+
Resque::Scheduler.verbose = true
|
961
|
+
Resque::Scheduler.run
|
962
|
+
end
|
963
|
+
|
964
|
+
worker = Resque::Worker.new("*")
|
965
|
+
worker.verbose = true
|
966
|
+
worker.work(1)
|
967
|
+
else
|
968
|
+
cli_connector.run(ARGV)
|
969
|
+
end
|
970
|
+
```
|
971
|
+
|
972
|
+
Now when we run it with just "work" we see the scheduler start:
|
973
|
+
|
974
|
+
```
|
975
|
+
$ ./part_6_scheduler_command_connector.rb work
|
976
|
+
resque-scheduler: [INFO] 2024-12-12T14:47:41-08:00: Starting
|
977
|
+
resque-scheduler: [DEBUG] 2024-12-12T14:47:41-08:00: Setting procline "resque-scheduler-4.10.2: Starting"
|
978
|
+
resque-scheduler: [DEBUG] 2024-12-12T14:47:41-08:00: Setting procline "resque-scheduler-4.10.2: Schedules Loaded"
|
979
|
+
```
|
980
|
+
|
981
|
+
And every 5 seconds we see it outputting:
|
982
|
+
|
983
|
+
```
|
984
|
+
resque-scheduler: [INFO] 2024-12-12T14:47:45-08:00: queueing Foobara::CommandConnectors::ResqueConnector::CommandJob (IncrementAge)
|
985
|
+
*** got: (Job{general} | Foobara::CommandConnectors::ResqueConnector::CommandJob | [{"command_name"=>"IncrementAge", "inputs"=>{"capybara"=>1}}])
|
986
|
+
*** done: (Job{general} | Foobara::CommandConnectors::ResqueConnector::CommandJob | [{"command_name"=>"IncrementAge", "inputs"=>{"capybara"=>1}}])
|
987
|
+
```
|
988
|
+
|
989
|
+
And if we check Fumiko's age like before we see it going up every 5 seconds.
|
891
990
|
|
892
991
|
## Intermediate Foobara
|
893
992
|
|
@@ -900,7 +999,7 @@ Let's take a quick look at some metadata in our existing systems.
|
|
900
999
|
|
901
1000
|
Let's for example ask our Capybara entity for its manifest:
|
902
1001
|
|
903
|
-
```
|
1002
|
+
```
|
904
1003
|
> Capybara.foobara_manifest
|
905
1004
|
==>
|
906
1005
|
{:attributes_type=>
|
@@ -922,14 +1021,14 @@ Let's for example ask our Capybara entity for its manifest:
|
|
922
1021
|
|
923
1022
|
Let's ask our Rack connector for a list of commands it exposes:
|
924
1023
|
|
925
|
-
```
|
1024
|
+
```
|
926
1025
|
> command_connector.foobara_manifest[:command].keys
|
927
1026
|
==> [:CreateCapybara, :FindCapybara, :IncrementAge]
|
928
1027
|
```
|
929
1028
|
|
930
1029
|
We can see all the different categories of concepts available by looking at the top-level keys:
|
931
1030
|
|
932
|
-
```
|
1031
|
+
```
|
933
1032
|
> puts command_connector.foobara_manifest.keys.sort
|
934
1033
|
command
|
935
1034
|
domain
|
@@ -993,7 +1092,6 @@ Let's create a command that calls another. Remember our `Add` command from earli
|
|
993
1092
|
Let's implement a contrived Subtract command that is implemented using Add:
|
994
1093
|
|
995
1094
|
```ruby
|
996
|
-
|
997
1095
|
class Subtract < Foobara::Command
|
998
1096
|
inputs do
|
999
1097
|
operand1 :integer, :required
|
@@ -1028,7 +1126,7 @@ dependency graph of commands.
|
|
1028
1126
|
|
1029
1127
|
Let's play with it:
|
1030
1128
|
|
1031
|
-
```
|
1129
|
+
```
|
1032
1130
|
> Subtract.run!(operand1: 5, operand2: 2)
|
1033
1131
|
==> 3
|
1034
1132
|
```
|
@@ -1037,7 +1135,7 @@ We get the answer we expected!
|
|
1037
1135
|
|
1038
1136
|
A little bit advanced but let's look at the possible errors for Subtract:
|
1039
1137
|
|
1040
|
-
```
|
1138
|
+
```
|
1041
1139
|
> Subtract.possible_errors.map(&:key).map(&:to_s).sort
|
1042
1140
|
==>
|
1043
1141
|
["add>data.cannot_cast",
|
@@ -1154,7 +1252,7 @@ operation entails at a high-level.
|
|
1154
1252
|
|
1155
1253
|
Let's play with it:
|
1156
1254
|
|
1157
|
-
```
|
1255
|
+
```
|
1158
1256
|
> Divide.run!(dividend: 6, divisor: 7)
|
1159
1257
|
==> 0
|
1160
1258
|
> Divide.run!(dividend: 8, divisor: 7)
|
@@ -1179,29 +1277,29 @@ This is one way we can express a custom error for associated with a specific inp
|
|
1179
1277
|
|
1180
1278
|
Let's try it out!
|
1181
1279
|
|
1182
|
-
```
|
1280
|
+
```
|
1183
1281
|
> outcome = Divide.run(dividend: 49, divisor: 0)
|
1184
1282
|
==> #<Foobara::Outcome:0x00007f504d178e38...
|
1185
1283
|
> outcome.success?
|
1186
1284
|
==> false
|
1187
1285
|
> outcome.errors_hash
|
1188
1286
|
==>
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1287
|
+
{"data.divisor.divide_by_zero"=>
|
1288
|
+
{:key=>"data.divisor.divide_by_zero",
|
1289
|
+
:path=>[:divisor],
|
1290
|
+
:runtime_path=>[],
|
1291
|
+
:category=>:data,
|
1292
|
+
:symbol=>:divide_by_zero,
|
1293
|
+
:message=>"Cannot divide by zero",
|
1294
|
+
:context=>{},
|
1295
|
+
:is_fatal=>false}}
|
1198
1296
|
> outcome.errors_sentence
|
1199
1297
|
==> "Cannot divide by zero"
|
1200
1298
|
```
|
1201
1299
|
|
1202
1300
|
And we can see the error in the command's list of possible errors:
|
1203
1301
|
|
1204
|
-
```
|
1302
|
+
```
|
1205
1303
|
> Divide.possible_errors.map(&:key).map(&:to_s).grep /zero/
|
1206
1304
|
==> ["data.divisor.divide_by_zero"]
|
1207
1305
|
```
|
@@ -1209,7 +1307,7 @@ And we can see the error in the command's list of possible errors:
|
|
1209
1307
|
And of course, as expected, tooling has access to information about this error and the command's possible error through manifest
|
1210
1308
|
metadata:
|
1211
1309
|
|
1212
|
-
```
|
1310
|
+
```
|
1213
1311
|
> Foobara.manifest[:command][:Divide][:possible_errors]["data.divisor.divide_by_zero"][:error]
|
1214
1312
|
==> "Divide::DivideByZeroError"
|
1215
1313
|
> Foobara.manifest[:error][:"Divide::DivideByZeroError"][:parent]
|
@@ -1225,7 +1323,7 @@ class Divide < Foobara::Command
|
|
1225
1323
|
"Cannot divide by zero"
|
1226
1324
|
end
|
1227
1325
|
end
|
1228
|
-
|
1326
|
+
|
1229
1327
|
possible_input_error :divisor, DivideByZeroError
|
1230
1328
|
```
|
1231
1329
|
|
@@ -1257,7 +1355,7 @@ class Divide < Foobara::Command
|
|
1257
1355
|
|
1258
1356
|
And let's try it out:
|
1259
1357
|
|
1260
|
-
```
|
1358
|
+
```
|
1261
1359
|
> outcome = Divide.run(dividend: 49, divisor: 0)
|
1262
1360
|
==> #<Foobara::Outcome:0x00007f030fe3b8b8...
|
1263
1361
|
> outcome.success?
|
@@ -1386,25 +1484,29 @@ module FoobaraDemo
|
|
1386
1484
|
module CapyCafe
|
1387
1485
|
foobara_depends_on AnimalHouse
|
1388
1486
|
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1487
|
+
module DomainMappers
|
1488
|
+
class MapAnimalToCapybara < Foobara::DomainMapper
|
1489
|
+
from AnimalHouse::Animal
|
1490
|
+
to CreateCapybara
|
1392
1491
|
|
1393
|
-
|
1394
|
-
|
1492
|
+
def map
|
1493
|
+
{
|
1494
|
+
name: "#{first_name} #{last_name}",
|
1495
|
+
age: birthday_to_age
|
1496
|
+
}
|
1497
|
+
end
|
1395
1498
|
|
1396
|
-
|
1397
|
-
name: "#{animal.first_name} #{animal.last_name}",
|
1398
|
-
age:
|
1399
|
-
}
|
1400
|
-
end
|
1499
|
+
alias animal from
|
1401
1500
|
|
1402
|
-
|
1403
|
-
today = Date.today
|
1404
|
-
age = today.year - birthday.year
|
1405
|
-
birthday_this_year = Date.new(birthday.year + age, birthday.month, birthday.day)
|
1501
|
+
foobara_delegate :first_name, :last_name, :birthday, to: :animal
|
1406
1502
|
|
1407
|
-
|
1503
|
+
def birthday_to_age
|
1504
|
+
today = Date.today
|
1505
|
+
age = today.year - birthday.year
|
1506
|
+
birthday_this_year = Date.new(birthday.year + age, birthday.month, birthday.day)
|
1507
|
+
|
1508
|
+
today < birthday_this_year ? age - 1 : age
|
1509
|
+
end
|
1408
1510
|
end
|
1409
1511
|
end
|
1410
1512
|
end
|
@@ -1418,9 +1520,9 @@ distribution changes.
|
|
1418
1520
|
Normally, we wouldn't make use of a domain mapper in isolation. Like everything else, it should be used in the context
|
1419
1521
|
of a command. But we can play with it directly:
|
1420
1522
|
|
1421
|
-
```
|
1523
|
+
```
|
1422
1524
|
$ ./animal_house_import.rb
|
1423
|
-
> create_capybara_inputs = FoobaraDemo::CapyCafe::
|
1525
|
+
> create_capybara_inputs = FoobaraDemo::CapyCafe::DomainMappers::MapAnimalToCapybara.map!(species: :capybara, first_name: "Barbara", last_name: "Doe", birthday: "1000-01-01")
|
1424
1526
|
==> {:name=>"Barbara Doe", :age=>1024}
|
1425
1527
|
> barbara = FoobaraDemo::CapyCafe::CreateCapybara.run!(create_capybara_inputs)
|
1426
1528
|
==> <Capybara:2>
|
@@ -1430,47 +1532,15 @@ $ ./animal_house_import.rb
|
|
1430
1532
|
==> 2
|
1431
1533
|
```
|
1432
1534
|
|
1433
|
-
|
1535
|
+
Now let's make use of our domain mapper in a command, which is its intended purpose:
|
1536
|
+
|
1537
|
+
```ruby
|
1434
1538
|
|
1435
|
-
```irb
|
1436
|
-
> FoobaraDemo::CapyCafe::IncrementAge.run!(capybara: barbara)
|
1437
|
-
==> <Capybara:2>
|
1438
|
-
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: barbara)
|
1439
|
-
==> <Capybara:2>
|
1440
|
-
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: barbara).age
|
1441
|
-
==> 1025
|
1442
1539
|
```
|
1443
1540
|
|
1444
1541
|
Now let's create a command that makes use of our domain mapper which is the typical usage pattern:
|
1445
1542
|
|
1446
1543
|
```ruby
|
1447
|
-
#!/usr/bin/env ruby
|
1448
|
-
|
1449
|
-
require "foobara/remote_imports"
|
1450
|
-
|
1451
|
-
[9292, 9293].each do |port|
|
1452
|
-
Foobara::RemoteImports::ImportCommand.run!(manifest_url: "http://localhost:#{port}/manifest")
|
1453
|
-
end
|
1454
|
-
|
1455
|
-
module FoobaraDemo
|
1456
|
-
module AnimalHouse
|
1457
|
-
foobara_domain!
|
1458
|
-
end
|
1459
|
-
end
|
1460
|
-
|
1461
|
-
module FoobaraDemo
|
1462
|
-
module AnimalHouse
|
1463
|
-
class Animal < Foobara::Model
|
1464
|
-
attributes do
|
1465
|
-
first_name :string
|
1466
|
-
last_name :string
|
1467
|
-
birthday :date
|
1468
|
-
species :symbol, one_of: %i[capybara cat tartigrade]
|
1469
|
-
end
|
1470
|
-
end
|
1471
|
-
end
|
1472
|
-
end
|
1473
|
-
|
1474
1544
|
module FoobaraDemo
|
1475
1545
|
module CapyCafe
|
1476
1546
|
class ImportAnimal < Foobara::Command
|
@@ -1487,7 +1557,7 @@ module FoobaraDemo
|
|
1487
1557
|
|
1488
1558
|
possible_input_error :animal, NotACapybara
|
1489
1559
|
|
1490
|
-
depends_on CreateCapybara
|
1560
|
+
depends_on CreateCapybara, DomainMappers::MapAnimalToCapybara
|
1491
1561
|
|
1492
1562
|
def execute
|
1493
1563
|
create_capybara
|
@@ -1497,14 +1567,6 @@ module FoobaraDemo
|
|
1497
1567
|
|
1498
1568
|
attr_accessor :capybara
|
1499
1569
|
|
1500
|
-
def validate
|
1501
|
-
species = animal.species
|
1502
|
-
|
1503
|
-
unless species == :capybara
|
1504
|
-
add_input_error :animal, NotACapybara, animal: animal, species: species
|
1505
|
-
end
|
1506
|
-
end
|
1507
|
-
|
1508
1570
|
def create_capybara
|
1509
1571
|
self.capybara = run_mapped_subcommand!(CreateCapybara, animal)
|
1510
1572
|
end
|
@@ -1517,66 +1579,456 @@ Note that we can automatically map `animal` to CreateCapybara inputs by calling
|
|
1517
1579
|
|
1518
1580
|
Let's play with it:
|
1519
1581
|
|
1520
|
-
```
|
1582
|
+
```
|
1583
|
+
$ ./animal_house_import.rb
|
1521
1584
|
> basil = FoobaraDemo::CapyCafe::ImportAnimal.run!(animal: { species: :capybara, first_name: "Basil", last_name: "Doe", birthday: "1000-01-01" })
|
1522
1585
|
==> <Capybara:3>
|
1523
|
-
> basil.age
|
1586
|
+
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: basil).age
|
1524
1587
|
==> 1024
|
1525
|
-
> basil.id
|
1526
|
-
==> 3
|
1527
1588
|
> FoobaraDemo::CapyCafe::IncrementAge.run!(capybara: basil)
|
1528
1589
|
==> <Capybara:3>
|
1529
1590
|
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: basil).age
|
1530
1591
|
==> 1025
|
1531
1592
|
```
|
1532
1593
|
|
1533
|
-
|
1594
|
+
Great! Notice how we have avoided putting pieces of the AnimalHouse mental model in our ImportAnimal command which
|
1595
|
+
is part of the CapyCafe mental model. Even pieces of error-handling/validation could be moved out using domain mappers
|
1596
|
+
as we've done here.
|
1597
|
+
|
1598
|
+
And we can even discover that an error might occur when running the command:
|
1599
|
+
|
1600
|
+
```
|
1601
|
+
> FoobaraDemo::CapyCafe::ImportAnimal.possible_errors.map(&:key).map(&:to_s).grep /not_a/
|
1602
|
+
==> ["foobara_demo::capy_cafe::domain_mappers::map_animal_to_capybara>runtime.not_a_capybara"]
|
1603
|
+
```
|
1604
|
+
|
1605
|
+
This is pretty nice because it means that tooling/external systems can discover and make use of these errors! This
|
1606
|
+
again helps with abstracting away integration code and putting the spotlight on implementing the actual
|
1607
|
+
problem/solution domain.
|
1608
|
+
|
1609
|
+
Let's actually go ahead and cause NotACapybara error:
|
1610
|
+
|
1611
|
+
```
|
1612
|
+
> outcome = FoobaraDemo::CapyCafe::ImportAnimal.run(animal: { species: :tartigrade, first_name: "Tara", last_name: "Tigrade", birthday: "1000-01-01" })
|
1613
|
+
==>
|
1614
|
+
#<Foobara::Outcome:0x00007fc310fb2c98
|
1615
|
+
...
|
1616
|
+
> outcome.errors_sentence
|
1617
|
+
==> "Can only import a capybara not a tartigrade"
|
1618
|
+
```
|
1619
|
+
|
1620
|
+
### Types
|
1621
|
+
|
1622
|
+
#### Builtin types
|
1623
|
+
|
1624
|
+
Foobara comes with a number of builtin types. Let's see what they are with this little hack:
|
1625
|
+
|
1626
|
+
```
|
1627
|
+
> Foobara::Util.print_tree(Foobara::Namespace.global.foobara_all_type, to_parent: :base_type, to_name: :full_type_name)
|
1628
|
+
╭──────╮
|
1629
|
+
│ duck │
|
1630
|
+
╰──┬───╯
|
1631
|
+
│ ╭─────────────╮
|
1632
|
+
├─┤ atomic_duck │
|
1633
|
+
│ ╰──────┬──────╯
|
1634
|
+
│ │ ╭─────────╮
|
1635
|
+
│ ├─┤ boolean │
|
1636
|
+
│ │ ╰─────────╯
|
1637
|
+
│ │ ╭──────╮
|
1638
|
+
│ ├─┤ date │
|
1639
|
+
│ │ ╰──────╯
|
1640
|
+
│ │ ╭──────────╮
|
1641
|
+
│ ├─┤ datetime │
|
1642
|
+
│ │ ╰──────────╯
|
1643
|
+
│ │ ╭───────╮
|
1644
|
+
│ ├─┤ model │
|
1645
|
+
│ │ ╰───┬───╯
|
1646
|
+
│ │ │ ╭──────────────────────────────────╮
|
1647
|
+
│ │ ├─┤ FoobaraDemo::AnimalHouse::Animal │
|
1648
|
+
│ │ │ ╰──────────────────────────────────╯
|
1649
|
+
│ │ │ ╭─────────────────╮
|
1650
|
+
│ │ └─┤ detached_entity │
|
1651
|
+
│ │ ╰────────┬────────╯
|
1652
|
+
│ │ │ ╭─────────────────────────────────╮
|
1653
|
+
│ │ ├─┤ FoobaraDemo::CapyCafe::Capybara │
|
1654
|
+
│ │ │ ╰─────────────────────────────────╯
|
1655
|
+
│ │ │ ╭────────╮
|
1656
|
+
│ │ └─┤ entity │
|
1657
|
+
│ │ ╰────────╯
|
1658
|
+
│ │ ╭────────╮
|
1659
|
+
│ ├─┤ number │
|
1660
|
+
│ │ ╰───┬────╯
|
1661
|
+
│ │ │ ╭─────────────╮
|
1662
|
+
│ │ ├─┤ big_decimal │
|
1663
|
+
│ │ │ ╰─────────────╯
|
1664
|
+
│ │ │ ╭───────╮
|
1665
|
+
│ │ ├─┤ float │
|
1666
|
+
│ │ │ ╰───────╯
|
1667
|
+
│ │ │ ╭─────────╮
|
1668
|
+
│ │ └─┤ integer │
|
1669
|
+
│ │ ╰─────────╯
|
1670
|
+
│ │ ╭────────╮
|
1671
|
+
│ ├─┤ string │
|
1672
|
+
│ │ ╰───┬────╯
|
1673
|
+
│ │ │ ╭───────╮
|
1674
|
+
│ │ └─┤ email │
|
1675
|
+
│ │ ╰───────╯
|
1676
|
+
│ │ ╭────────╮
|
1677
|
+
│ └─┤ symbol │
|
1678
|
+
│ ╰────────╯
|
1679
|
+
│ ╭──────────╮
|
1680
|
+
└─┤ duckture │
|
1681
|
+
╰────┬─────╯
|
1682
|
+
│ ╭───────╮
|
1683
|
+
├─┤ array │
|
1684
|
+
│ ╰───┬───╯
|
1685
|
+
│ │ ╭───────╮
|
1686
|
+
│ └─┤ tuple │
|
1687
|
+
│ ╰───────╯
|
1688
|
+
│ ╭───────────────────╮
|
1689
|
+
└─┤ associative_array │
|
1690
|
+
╰─────────┬─────────╯
|
1691
|
+
│ ╭────────────╮
|
1692
|
+
└─┤ attributes │
|
1693
|
+
╰────────────╯
|
1694
|
+
```
|
1695
|
+
|
1696
|
+
Obviously Capybara and Animal are not builtin types but you get the point.
|
1697
|
+
|
1698
|
+
#### Custom types
|
1699
|
+
|
1700
|
+
Let's have a capybara diving competition. Which means we need judges. So let's create a new domain,
|
1701
|
+
`CapybaraDivingCompetition`, and a new entity, `Judge`:
|
1702
|
+
|
1703
|
+
```ruby
|
1704
|
+
module FoobaraDemo
|
1705
|
+
module CapybaraDivingCompetition
|
1706
|
+
foobara_domain!
|
1707
|
+
|
1708
|
+
depends_on CapyCafe
|
1709
|
+
|
1710
|
+
class Judge < Foobara::Model
|
1711
|
+
attributes do
|
1712
|
+
email :string
|
1713
|
+
favorite_diver CapyCafe::Capybara, :allow_nil
|
1714
|
+
end
|
1715
|
+
end
|
1716
|
+
end
|
1717
|
+
end
|
1718
|
+
```
|
1719
|
+
|
1720
|
+
So we have an email to identify the judge and which diver is their favorite.
|
1721
|
+
But wait... there's an age-old problem... email addresses are case-insensitive. One way to solve this is to make
|
1722
|
+
sure to downcase the emails whenever we receive them anywhere in the app. If we don't want to worry about that, we
|
1723
|
+
can just make that behavior be part of the type itself:
|
1724
|
+
|
1725
|
+
```ruby
|
1726
|
+
...
|
1727
|
+
class Judge < Foobara::Model
|
1728
|
+
attributes do
|
1729
|
+
email :string, :downcase
|
1730
|
+
...
|
1731
|
+
```
|
1732
|
+
|
1733
|
+
Here we are using a downcase transformer. This will always downcase the value everywhere.
|
1734
|
+
|
1735
|
+
Let's go ahead and a validator while we're at it because why not?
|
1736
|
+
|
1737
|
+
```ruby
|
1738
|
+
...
|
1739
|
+
class Judge < Foobara::Model
|
1740
|
+
attributes do
|
1741
|
+
email :string, :downcase, matches: /\A[^@]+@[^@]+\.[^@]+\z/
|
1742
|
+
...
|
1743
|
+
```
|
1744
|
+
|
1745
|
+
OK let's play with this really quickly:
|
1746
|
+
|
1747
|
+
```
|
1748
|
+
$ ./part_1c_custom_types.rb
|
1749
|
+
> judge = FoobaraDemo::CapybaraDivingCompetition::Judge.new(email: "ASDF@asdf.com")
|
1750
|
+
==> #<FoobaraDemo::CapybaraDivingCompetition::Judge:0x00007f780b978418 @attributes={:email=>"asdf@asdf.com"}, @mutable=true>
|
1751
|
+
> judge.valid?
|
1752
|
+
==> true
|
1753
|
+
> judge.email
|
1754
|
+
==> "asdf@asdf.com"
|
1755
|
+
> judge = FoobaraDemo::CapybaraDivingCompetition::Judge.new(email: "asdf.com")
|
1756
|
+
==> #<FoobaraDemo::CapybaraDivingCompetition::Judge:0x00007f780ba3cb88 @attributes={:email=>"asdf.com"}, @mutable=true>
|
1757
|
+
> judge.valid?
|
1758
|
+
==> false
|
1759
|
+
> judge.validation_errors.first.to_h
|
1760
|
+
==>
|
1761
|
+
{:key=>"data.email.does_not_match",
|
1762
|
+
:path=>[:email],
|
1763
|
+
:runtime_path=>[],
|
1764
|
+
:category=>:data,
|
1765
|
+
:symbol=>:does_not_match,
|
1766
|
+
:message=>"\"asdf.com\" did not match /\\A[^@]+@[^@]+\\.[^@]+\\z/",
|
1767
|
+
:context=>{:value=>"asdf.com", :regex=>"(?-mix:\\A[^@]+@[^@]+\\.[^@]+\\z)"},
|
1768
|
+
:is_fatal=>false}
|
1769
|
+
```
|
1770
|
+
|
1771
|
+
So we can see that our email was automatically downcased. We can also see that if we leave out @ we get an error.
|
1772
|
+
|
1773
|
+
But what if we want to use this type all over the place? It would be not great to copy/paste it around because,
|
1774
|
+
for example, it would be nice to improve the validation error message. Do we want to find/fix all its usages when we
|
1775
|
+
do that?
|
1776
|
+
|
1777
|
+
Instead, we can create a custom type and use it:
|
1778
|
+
|
1779
|
+
```ruby
|
1780
|
+
class Judge < Foobara::Model
|
1781
|
+
email_address_type = domain.foobara_type_from_declaration(:string, :downcase, matches: /\A[^@]+@[^@]+\.[^@]+\z/)
|
1782
|
+
|
1783
|
+
attributes do
|
1784
|
+
email email_address_type, :required
|
1785
|
+
end
|
1786
|
+
end
|
1787
|
+
```
|
1788
|
+
|
1789
|
+
However, it is better to register it on the domain. Then it can be used by name and will appear in the manifests
|
1790
|
+
by name. So let's do that:
|
1791
|
+
|
1792
|
+
```ruby
|
1793
|
+
module FoobaraDemo
|
1794
|
+
module CapybaraDivingCompetition
|
1795
|
+
foobara_domain!
|
1796
|
+
|
1797
|
+
depends_on CapyCafe
|
1798
|
+
|
1799
|
+
foobara_register_type :email_address, :string, :downcase, matches: /\A[^@]+@[^@]+\.[^@]+\z/
|
1800
|
+
|
1801
|
+
class Judge < Foobara::Model
|
1802
|
+
attributes do
|
1803
|
+
email :email_address, :required
|
1804
|
+
favorite_diver CapyCafe::Capybara, :allow_nil
|
1805
|
+
end
|
1806
|
+
end
|
1807
|
+
end
|
1808
|
+
end
|
1809
|
+
```
|
1534
1810
|
|
1535
1811
|
### Code Generators
|
1536
1812
|
|
1813
|
+
There are a number of code generators we can use that are available through the foob CLI tool. Let's install it:
|
1814
|
+
|
1815
|
+
```
|
1816
|
+
$ gem install foob
|
1817
|
+
```
|
1818
|
+
|
1537
1819
|
#### Generating a new Foobara Ruby project
|
1820
|
+
|
1821
|
+
We've been piling our code into one script so far but out in the real world we would need to organize different
|
1822
|
+
code units into different files into directories with some sort of project structure to it.
|
1823
|
+
|
1824
|
+
We can use foob to generate a project with such a structure:
|
1825
|
+
|
1826
|
+
```
|
1827
|
+
$ foob generate ruby-project --name foobara-demo/capybara-diving-competition
|
1828
|
+
```
|
1829
|
+
|
1830
|
+
Let's generate a bunch of the code we've written so far in this demo. We can see all of the available generators
|
1831
|
+
by running:
|
1832
|
+
|
1833
|
+
```
|
1834
|
+
$ foob g
|
1835
|
+
Usage: foob generate [GENERATOR_KEY] [GENERATOR_OPTIONS]
|
1836
|
+
|
1837
|
+
Available Generators:
|
1838
|
+
|
1839
|
+
autocrud
|
1840
|
+
command
|
1841
|
+
domain
|
1842
|
+
domain-mapper
|
1843
|
+
organization
|
1844
|
+
rack-connector
|
1845
|
+
redis-crud-driver
|
1846
|
+
remote-imports
|
1847
|
+
resque-connector
|
1848
|
+
resque-scheduler-connector
|
1849
|
+
ruby-project
|
1850
|
+
sh-cli-connector
|
1851
|
+
type
|
1852
|
+
typescript-react-command-form
|
1853
|
+
typescript-react-project
|
1854
|
+
typescript-remote-commands
|
1855
|
+
```
|
1856
|
+
|
1857
|
+
We can get help for a specific generator with `foob help [GENERATOR_NAME]`. For example:
|
1858
|
+
|
1859
|
+
```
|
1860
|
+
$ foob help type
|
1861
|
+
Usage: foob [GLOBAL_OPTIONS] type [COMMAND_INPUTS]
|
1862
|
+
|
1863
|
+
Command inputs:
|
1864
|
+
|
1865
|
+
-n, --name NAME
|
1866
|
+
-t, --type TYPE One of: entity, model, type. Default: :type
|
1867
|
+
-d, --description DESCRIPTION
|
1868
|
+
--domain DOMAIN
|
1869
|
+
-o, --organization ORGANIZATION
|
1870
|
+
--output-directory OUTPUT_DIRECTORY
|
1871
|
+
```
|
1872
|
+
|
1873
|
+
So let's use these generators to generate files for various classes/modules we've created so far in this tutorial:
|
1874
|
+
|
1875
|
+
```
|
1876
|
+
$ cd foobara-demo/capybara-diving-competition
|
1877
|
+
$ foob g domain --name FoobaraDemo::IntegerMath
|
1878
|
+
$ foob g domain --name FoobaraDemo::CapyCafe
|
1879
|
+
$ foob g type -t entity --organization FoobaraDemo --domain CapyCafe --name Capybara
|
1880
|
+
$ foob g type --organization FoobaraDemo --domain CapybaraDivingCompetition --name email_address
|
1881
|
+
$ foob g type -t entity --organization FoobaraDemo --domain CapybaraDivingCompetition --name Judge
|
1882
|
+
$ foob g command --name FoobaraDemo::IntegerMath::Add
|
1883
|
+
$ foob g command --name FoobaraDemo::IntegerMath::Subtract
|
1884
|
+
$ foob g command --name FoobaraDemo::IntegerMath::Divide
|
1885
|
+
$ foob g command --name FoobaraDemo::CapyCafe::CreateCapybara
|
1886
|
+
$ foob g command --name FoobaraDemo::CapyCafe::FindCapybara
|
1887
|
+
$ foob g command --name FoobaraDemo::CapyCafe::IncrementAge
|
1888
|
+
$ foob g sh-cli-connector --name capy-cafe
|
1889
|
+
$ foob g local-files-crud-driver
|
1890
|
+
```
|
1891
|
+
|
1892
|
+
This results in the following directory structure:
|
1893
|
+
|
1894
|
+
```
|
1895
|
+
$ tree -a --dirsfirst --prune --matchdirs -I '.git'
|
1896
|
+
.
|
1897
|
+
├── bin
|
1898
|
+
│ ├── capy-cafe
|
1899
|
+
│ └── console
|
1900
|
+
├── boot
|
1901
|
+
│ ├── config.rb
|
1902
|
+
│ ├── crud.rb
|
1903
|
+
│ ├── finish.rb
|
1904
|
+
│ └── start.rb
|
1905
|
+
├── .github
|
1906
|
+
│ └── workflows
|
1907
|
+
│ └── ci.yml
|
1908
|
+
├── lib
|
1909
|
+
│ └── foobara_demo
|
1910
|
+
│ └── capybara_diving_competition.rb
|
1911
|
+
├── spec
|
1912
|
+
│ ├── foobara_demo
|
1913
|
+
│ │ ├── capy_cafe
|
1914
|
+
│ │ │ ├── create_capybara_spec.rb
|
1915
|
+
│ │ │ ├── find_capybara_spec.rb
|
1916
|
+
│ │ │ └── increment_age_spec.rb
|
1917
|
+
│ │ ├── integer_math
|
1918
|
+
│ │ │ ├── add_spec.rb
|
1919
|
+
│ │ │ ├── divide_spec.rb
|
1920
|
+
│ │ │ └── subtract_spec.rb
|
1921
|
+
│ │ └── capybara_diving_competition_spec.rb
|
1922
|
+
│ ├── support
|
1923
|
+
│ │ ├── rubyprof.rb
|
1924
|
+
│ │ ├── simplecov.rb
|
1925
|
+
│ │ ├── term_trap.rb
|
1926
|
+
│ │ └── vcr.rb
|
1927
|
+
│ └── spec_helper.rb
|
1928
|
+
├── src
|
1929
|
+
│ └── foobara_demo
|
1930
|
+
│ ├── capybara_diving_competition
|
1931
|
+
│ │ └── types
|
1932
|
+
│ │ ├── email_address.rb
|
1933
|
+
│ │ └── judge.rb
|
1934
|
+
│ ├── capy_cafe
|
1935
|
+
│ │ ├── types
|
1936
|
+
│ │ │ └── capybara.rb
|
1937
|
+
│ │ ├── create_capybara.rb
|
1938
|
+
│ │ ├── find_capybara.rb
|
1939
|
+
│ │ └── increment_age.rb
|
1940
|
+
│ ├── integer_math
|
1941
|
+
│ │ ├── add.rb
|
1942
|
+
│ │ ├── divide.rb
|
1943
|
+
│ │ └── subtract.rb
|
1944
|
+
│ ├── capybara_diving_competition.rb
|
1945
|
+
│ ├── capy_cafe.rb
|
1946
|
+
│ └── integer_math.rb
|
1947
|
+
├── boot.rb
|
1948
|
+
├── foobara-demo-capybara-diving-competition.gemspec
|
1949
|
+
├── Gemfile
|
1950
|
+
├── .gitignore
|
1951
|
+
├── Guardfile
|
1952
|
+
├── Rakefile
|
1953
|
+
├── README.md
|
1954
|
+
├── .rspec
|
1955
|
+
├── .rubocop.yml
|
1956
|
+
└── version.rb
|
1957
|
+
```
|
1958
|
+
|
1538
1959
|
#### Generating a new Foobara Typescript/React project
|
1539
|
-
#### Generating commands, models, entities, types, domains, organizations, etc...
|
1540
1960
|
|
1541
|
-
|
1961
|
+
We can generate a Typescript React project with the following generator:
|
1542
1962
|
|
1543
|
-
|
1963
|
+
```
|
1964
|
+
$ foob g typescript-react-project -p foobara-demo-frontend
|
1965
|
+
```
|
1544
1966
|
|
1545
|
-
|
1967
|
+
We can import remote commands into our typescript project with:
|
1968
|
+
|
1969
|
+
```
|
1970
|
+
$ foob g typescript-remote-commands --manifest-url http://localhost:9292/manifest
|
1971
|
+
```
|
1972
|
+
|
1973
|
+
And we can generate UI forms automatically with:
|
1974
|
+
|
1975
|
+
```
|
1976
|
+
$ foob g typescript-react-command-form --manifest-url http://localhost:9292/manifest --command-name CreateCapybara
|
1977
|
+
```
|
1546
1978
|
|
1547
1979
|
## Expert Foobara
|
1548
1980
|
|
1549
1981
|
### Callbacks
|
1550
1982
|
|
1551
|
-
|
1983
|
+
There are several callbacks on commands that you can hook into. Also on entities.
|
1984
|
+
|
1985
|
+
TODO: give some code examples
|
1552
1986
|
|
1553
1987
|
### Transactions in Commands
|
1554
1988
|
|
1555
|
-
|
1989
|
+
You can rollback/commit transactions in Commands. You can do this successfully even if the underlying data storage
|
1990
|
+
doesn't support transactions.
|
1991
|
+
|
1992
|
+
TODO: give some code examples
|
1556
1993
|
|
1557
1994
|
### Transactions in tests/console
|
1558
1995
|
|
1559
|
-
|
1996
|
+
You should normally not be creating entities outside of commands. But if you find yourself wanting to, perhaps
|
1997
|
+
in a test suite or console, you can open a transaction so that you can do it.
|
1998
|
+
|
1999
|
+
TODO: give some code examples
|
1560
2000
|
|
1561
2001
|
### Custom crud drivers
|
1562
2002
|
|
1563
|
-
|
2003
|
+
You can write your own CRUD drivers to read/write data from/to wherever you want.
|
1564
2004
|
|
1565
|
-
|
2005
|
+
TODO: give some code examples/cover the CRUD driver API
|
1566
2006
|
|
1567
|
-
|
2007
|
+
### Custom command connectors
|
1568
2008
|
|
1569
|
-
|
2009
|
+
You can write your own command connectors so that you can expose commands using whatever technology you wish.
|
1570
2010
|
|
1571
|
-
TODO
|
2011
|
+
TODO: give some code examples/cover the command connector API
|
1572
2012
|
|
1573
2013
|
### Custom types from scratch
|
1574
2014
|
|
1575
|
-
|
2015
|
+
Instead of creating new types by extending existing types with existing processors/transformers/validators, as we did
|
2016
|
+
in this tutorial with our email_address custom type,
|
2017
|
+
you can also write your own new types from scratch.
|
2018
|
+
|
2019
|
+
TODO: give some code examples/cover the type API
|
1576
2020
|
|
1577
2021
|
### Namespaces
|
1578
2022
|
|
1579
|
-
|
2023
|
+
Several of the concepts we've explored so far are also namespaces.
|
2024
|
+
|
2025
|
+
TODO: give some code examples
|
2026
|
+
|
2027
|
+
### Value processors
|
2028
|
+
|
2029
|
+
A low-level concept upon which several things like types and serializers are built in Foobara are value processors.
|
2030
|
+
|
2031
|
+
TODO: give some code examples
|
1580
2032
|
|
1581
2033
|
# Additional learning materials/Documentation
|
1582
2034
|
|
@@ -1603,7 +2055,7 @@ The build will fail if test coverage is below 100%
|
|
1603
2055
|
|
1604
2056
|
You should be able to do the typical stuff:
|
1605
2057
|
|
1606
|
-
```
|
2058
|
+
```
|
1607
2059
|
git clone git@github.com:foobara/foobara
|
1608
2060
|
cd foobara
|
1609
2061
|
bundle
|