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.
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?
@@ -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
- ```irb
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
- ```irb
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
- 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
+ ```
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
- TODO
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
- ### Custom types
1973
+ And we can generate UI forms automatically with:
1516
1974
 
1517
- TODO
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
- TODO
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
- 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
1528
1993
 
1529
1994
  ### Transactions in tests/console
1530
1995
 
1531
- 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
1532
2000
 
1533
2001
  ### Custom crud drivers
1534
2002
 
1535
- TODO
2003
+ You can write your own CRUD drivers to read/write data from/to wherever you want.
1536
2004
 
1537
- ### Custom command connectors
2005
+ TODO: give some code examples/cover the CRUD driver API
1538
2006
 
1539
- TODO
2007
+ ### Custom command connectors
1540
2008
 
1541
- ### Value processors
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
- 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
1548
2020
 
1549
2021
  ### Namespaces
1550
2022
 
1551
- 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
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
- ```bash
2058
+ ```
1579
2059
  git clone git@github.com:foobara/foobara
1580
2060
  cd foobara
1581
2061
  bundle