foobara 0.0.37 → 0.0.39

Sign up to get free protection for your applications and to get access to all the features.
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
- * [What is/Why Foobara?](#what-iswhy-foobara)
3
- * [Commands](#commands)
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-1)
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
- # What is/Why Foobara?
60
+ # Overview of Features/Concepts/Goals
51
61
 
52
- Foobara is a software framework meant to help with projects that have
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```ruby
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
- In fact, `entity` inherits `model`. We could look at the hierarchy of Capybara with the following hack:
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
- ```ruby
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
- ```bash
601
- > gem install foobara-local-files-crud-driver
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```bash
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
- ```irb
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
- ```ruby
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
- ```ruby
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
- TODO
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
- TODO
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
- ```irb
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
- ```irb
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
- ```irb
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
- ```ruby
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
- ```irb
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
- ```irb
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
- ```irb
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
- {"data.divisor.divide_by_zero"=>
1190
- {:key=>"data.divisor.divide_by_zero",
1191
- :path=>[:divisor],
1192
- :runtime_path=>[],
1193
- :category=>:data,
1194
- :symbol=>:divide_by_zero,
1195
- :message=>"Cannot divide by zero",
1196
- :context=>{},
1197
- :is_fatal=>false}}
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
- ```irb
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
- ```irb
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
- ```irb
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
- class AnimalToCapybara < Foobara::DomainMapper
1390
- from AnimalHouse::Animal
1391
- to CreateCapybara
1487
+ module DomainMappers
1488
+ class MapAnimalToCapybara < Foobara::DomainMapper
1489
+ from AnimalHouse::Animal
1490
+ to CreateCapybara
1392
1491
 
1393
- def map(animal)
1394
- age = birthday_to_age(animal.birthday)
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
- def birthday_to_age(birthday)
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
- today < birthday_this_year ? age - 1 : age
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
- ```irb
1523
+ ```
1422
1524
  $ ./animal_house_import.rb
1423
- > create_capybara_inputs = FoobaraDemo::CapyCafe::AnimalToCapybara.call(species: :capybara, first_name: "Barbara", last_name: "Doe", birthday: "1000-01-01")
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
- And we can increment Barbara's age now that she has been imported into our CapyCafe:
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
- ```irb
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
- TODO
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
- TODO
1961
+ We can generate a Typescript React project with the following generator:
1542
1962
 
1543
- ### Custom types
1963
+ ```
1964
+ $ foob g typescript-react-project -p foobara-demo-frontend
1965
+ ```
1544
1966
 
1545
- TODO
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
- TODO
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
- TODO
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
- TODO
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
- TODO
2003
+ You can write your own CRUD drivers to read/write data from/to wherever you want.
1564
2004
 
1565
- ### Custom command connectors
2005
+ TODO: give some code examples/cover the CRUD driver API
1566
2006
 
1567
- TODO
2007
+ ### Custom command connectors
1568
2008
 
1569
- ### Value processors
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
- TODO
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
- TODO
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
- ```bash
2058
+ ```
1607
2059
  git clone git@github.com:foobara/foobara
1608
2060
  cd foobara
1609
2061
  bundle