foobara 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -1
- data/README.md +629 -149
- data/projects/builtin_types/src/builtin_types.rb +31 -9
- data/projects/detached_entity/src/extensions/type_declarations/handlers/extend_detached_entity_type_declaration/to_type_transformer.rb +1 -0
- 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?
|
@@ -1422,7 +1520,7 @@ distribution changes.
|
|
1422
1520
|
Normally, we wouldn't make use of a domain mapper in isolation. Like everything else, it should be used in the context
|
1423
1521
|
of a command. But we can play with it directly:
|
1424
1522
|
|
1425
|
-
```
|
1523
|
+
```
|
1426
1524
|
$ ./animal_house_import.rb
|
1427
1525
|
> create_capybara_inputs = FoobaraDemo::CapyCafe::DomainMappers::MapAnimalToCapybara.map!(species: :capybara, first_name: "Barbara", last_name: "Doe", birthday: "1000-01-01")
|
1428
1526
|
==> {:name=>"Barbara Doe", :age=>1024}
|
@@ -1440,15 +1538,6 @@ Now let's make use of our domain mapper in a command, which is its intended purp
|
|
1440
1538
|
|
1441
1539
|
```
|
1442
1540
|
|
1443
|
-
```irb
|
1444
|
-
> FoobaraDemo::CapyCafe::IncrementAge.run!(capybara: barbara)
|
1445
|
-
==> <Capybara:2>
|
1446
|
-
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: barbara)
|
1447
|
-
==> <Capybara:2>
|
1448
|
-
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: barbara).age
|
1449
|
-
==> 1025
|
1450
|
-
```
|
1451
|
-
|
1452
1541
|
Now let's create a command that makes use of our domain mapper which is the typical usage pattern:
|
1453
1542
|
|
1454
1543
|
```ruby
|
@@ -1490,8 +1579,8 @@ Note that we can automatically map `animal` to CreateCapybara inputs by calling
|
|
1490
1579
|
|
1491
1580
|
Let's play with it:
|
1492
1581
|
|
1493
|
-
```
|
1494
|
-
$ ./animal_house_import.rb
|
1582
|
+
```
|
1583
|
+
$ ./animal_house_import.rb
|
1495
1584
|
> basil = FoobaraDemo::CapyCafe::ImportAnimal.run!(animal: { species: :capybara, first_name: "Basil", last_name: "Doe", birthday: "1000-01-01" })
|
1496
1585
|
==> <Capybara:3>
|
1497
1586
|
> FoobaraDemo::CapyCafe::FindCapybara.run!(id: basil).age
|
@@ -1502,53 +1591,444 @@ $ ./animal_house_import.rb
|
|
1502
1591
|
==> 1025
|
1503
1592
|
```
|
1504
1593
|
|
1505
|
-
|
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
|
+
```
|
1506
1810
|
|
1507
1811
|
### Code Generators
|
1508
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
|
+
|
1509
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
|
+
|
1510
1959
|
#### Generating a new Foobara Typescript/React project
|
1511
|
-
#### Generating commands, models, entities, types, domains, organizations, etc...
|
1512
1960
|
|
1513
|
-
|
1961
|
+
We can generate a Typescript React project with the following generator:
|
1962
|
+
|
1963
|
+
```
|
1964
|
+
$ foob g typescript-react-project -p foobara-demo-frontend
|
1965
|
+
```
|
1966
|
+
|
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
|
+
```
|
1514
1972
|
|
1515
|
-
|
1973
|
+
And we can generate UI forms automatically with:
|
1516
1974
|
|
1517
|
-
|
1975
|
+
```
|
1976
|
+
$ foob g typescript-react-command-form --manifest-url http://localhost:9292/manifest --command-name CreateCapybara
|
1977
|
+
```
|
1518
1978
|
|
1519
1979
|
## Expert Foobara
|
1520
1980
|
|
1521
1981
|
### Callbacks
|
1522
1982
|
|
1523
|
-
|
1983
|
+
There are several callbacks on commands that you can hook into. Also on entities.
|
1984
|
+
|
1985
|
+
TODO: give some code examples
|
1524
1986
|
|
1525
1987
|
### Transactions in Commands
|
1526
1988
|
|
1527
|
-
|
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
|
1528
1993
|
|
1529
1994
|
### Transactions in tests/console
|
1530
1995
|
|
1531
|
-
|
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
|
1532
2000
|
|
1533
2001
|
### Custom crud drivers
|
1534
2002
|
|
1535
|
-
|
2003
|
+
You can write your own CRUD drivers to read/write data from/to wherever you want.
|
1536
2004
|
|
1537
|
-
|
2005
|
+
TODO: give some code examples/cover the CRUD driver API
|
1538
2006
|
|
1539
|
-
|
2007
|
+
### Custom command connectors
|
1540
2008
|
|
1541
|
-
|
2009
|
+
You can write your own command connectors so that you can expose commands using whatever technology you wish.
|
1542
2010
|
|
1543
|
-
TODO
|
2011
|
+
TODO: give some code examples/cover the command connector API
|
1544
2012
|
|
1545
2013
|
### Custom types from scratch
|
1546
2014
|
|
1547
|
-
|
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
|
1548
2020
|
|
1549
2021
|
### Namespaces
|
1550
2022
|
|
1551
|
-
|
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
|
1552
2032
|
|
1553
2033
|
# Additional learning materials/Documentation
|
1554
2034
|
|
@@ -1575,7 +2055,7 @@ The build will fail if test coverage is below 100%
|
|
1575
2055
|
|
1576
2056
|
You should be able to do the typical stuff:
|
1577
2057
|
|
1578
|
-
```
|
2058
|
+
```
|
1579
2059
|
git clone git@github.com:foobara/foobara
|
1580
2060
|
cd foobara
|
1581
2061
|
bundle
|