foobara 0.0.28 → 0.0.29

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8bfc7b95d3e92c97900136f5ddf441f5229f71d1884bc37a8439dc5e4e3e727a
4
- data.tar.gz: fe04bd8fbc04cc7c8ec86aea2c4ce0390de2da79073d3f1236eaca00d63e6bb9
3
+ metadata.gz: 8722f293cfd3487965f3863448b8cf175481df41f364fe9c8a880c19430cbd86
4
+ data.tar.gz: 14aafe8e69546446ec65694d6f48ea14b44af2e94bbd2a05a0cd71209831bfd8
5
5
  SHA512:
6
- metadata.gz: 04ce017d39e5fa817da0f37402eee57a1c7cfc858ac9c88c9cda3bbf8d8515a489d7f3771d3df0ed038aa88bdfed34f3bb34ef7cb0fb56e28940fa7a7d130967
7
- data.tar.gz: 5dc2e9c070253ead42dcdda5dd95100ac21a0ecd069c2b4bf8705f00b754275bb2aa87cece6473e453b4be25c918683b0e7115d9d605d33f05ad45f85bf7f5fa
6
+ metadata.gz: 024d6d200f25cef54f5aa8a719af536d6f5e8fbf767fa6e9aa30ac04ba6f6c2f30815964cf1e0a079944e7a588c4afb91aaadbabe6c47ca9e001f0f3b01e8649
7
+ data.tar.gz: a31f5904edef9faf390075de61229dd0890e0251a1b9e5e5d870a900f7ef1744d0cf4dbedf2812e9f9559773cc8f9dc5cbec89518a237adbfc82b4dace2d5642
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.0.29] - 2024-12-07
2
+
3
+ - Fix problems with extending models/entities
4
+ - Add a mutable class helper for models and improve mutable
5
+ use a bit
6
+
1
7
  ## [0.0.28] - 2024-12-05
2
8
 
3
9
  - Make Domain#foobara_depends_on? give a more intuitive answer
data/README.md CHANGED
@@ -1,76 +1,1096 @@
1
- # Foobara
1
+
2
+ <!-- TOC -->
3
+ * [What is/Why Foobara?](#what-iswhy-foobara)
4
+ * [Commands](#commands)
5
+ * [Discoverability](#discoverability)
6
+ * [Implications of command-centric + discoverability](#implications-of-command-centric--discoverability)
7
+ * [Other features for helping with Domain complexity](#other-features-for-helping-with-domain-complexity)
8
+ * [Installation](#installation)
9
+ * [Usage/Tutorial](#usagetutorial)
10
+ * [Foobara 101](#foobara-101)
11
+ * [Commands](#commands-1)
12
+ * [Organizations and Domains](#organizations-and-domains)
13
+ * [Types](#types)
14
+ * [Models](#models)
15
+ * [Entities](#entities)
16
+ * [Command connectors](#command-connectors)
17
+ * [Command-line connectors](#command-line-connectors)
18
+ * [HTTP Command Connectors](#http-command-connectors)
19
+ * [Async Command Connectors](#async-command-connectors)
20
+ * [Scheduler Command Connectors](#scheduler-command-connectors)
21
+ * [Intermediate Foobara](#intermediate-foobara)
22
+ * [Remote Commands](#remote-commands)
23
+ * [Subcommands](#subcommands)
24
+ * [Custom Errors](#custom-errors)
25
+ * [Input Errors](#input-errors)
26
+ * [Runtime Errors](#runtime-errors)
27
+ * [Advanced Foobara](#advanced-foobara)
28
+ * [Domain Mappers](#domain-mappers)
29
+ * [Code Generators](#code-generators)
30
+ * [Generating a new Foobara Ruby project](#generating-a-new-foobara-ruby-project)
31
+ * [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
+ * [Expert Foobara](#expert-foobara)
35
+ * [Callbacks](#callbacks)
36
+ * [Transactions in Commands](#transactions-in-commands)
37
+ * [Transactions in tests/console](#transactions-in-testsconsole)
38
+ * [Custom crud drivers](#custom-crud-drivers)
39
+ * [Custom command connectors](#custom-command-connectors)
40
+ * [Value processors](#value-processors)
41
+ * [Custom types from scratch](#custom-types-from-scratch)
42
+ * [Namespaces](#namespaces)
43
+ * [Additional learning materials/Documentation](#additional-learning-materialsdocumentation)
44
+ * [Contributing](#contributing)
45
+ * [Developing locally](#developing-locally)
46
+ * [Monorepo Structure](#monorepo-structure)
47
+ * [Licensing](#licensing)
48
+ <!-- TOC -->
49
+
50
+ # What is/Why Foobara?
2
51
 
3
52
  Foobara is a software framework meant to help with projects that have
4
53
  a complicated business domain. It accomplishes this by helping to
5
54
  build projects that are command-centric and discoverable, as well as some other features.
6
55
 
7
- ### Commands
56
+ ## Commands
8
57
 
9
58
  * Foobara commands are meant to encapsulate high-level domain operations.
10
59
  * They serve as the public interface to Foobara systems/subsystems.
11
60
  * They are organized into Organizations and Domains.
12
61
 
13
- ### Discoverability
62
+ ## Discoverability
63
+
14
64
  * This means there is a formal machine-readable description of the systems/subsystems
15
65
  * The implication of this is that integration code can be abstracted away.
16
66
 
17
- ### Implications of command-centric + discoverability
67
+ ## Implications of command-centric + discoverability
68
+
18
69
  * The system better communicates the mental model of the problem and the chosen solution
19
70
  * Engineers are able to spend more time writing code relevant to the domain and less time
20
71
  writing code related to specific tech-stack, software pattern, or architecture decisions.
21
72
  * Engineers can spend more time operating within a specific mental model at a time instead of
22
73
  multiple mental models all at once.
23
74
 
24
- ### Other features for helping with Domain complexity
75
+ ## Other features for helping with Domain complexity
25
76
 
77
+ * Domains and Organizations
78
+ * Domains are namespaces of Commands, types, and errors
79
+ * Domains (and commands) have explicit, unidirectional dependencies on other domains
80
+ * Organizations are namespaces of Domains
26
81
  * Domain mappers
27
- * These can map a concept from one domain to another.
82
+ * These can map a concept from one domain to another
28
83
  * This separation of concerns leads to commands that have code
29
- that reflects the domain they belong to as opposed to logic from many different domains.
84
+ that reflects the domain they belong to as opposed to logic from many different domains
30
85
  * Remote commands
31
- * These have the same interface as commands that live in other systems and act as a proxy to them.
32
- * This allows rearchitecting of systems without changing interfaces and so reducing refactoring/testing required.
86
+ * These have the same interface as commands that live in other systems and act as a proxy to them
87
+ * This allows rearchitecting of systems without changing interfaces and so reducing refactoring/testing required
33
88
  * These currently exist for both Ruby and Typescript
89
+ * Code generators
90
+ * Similar to remote commands, discoverability enables other types of tooling, including code generators,
91
+ documentation tools, etc
34
92
  * An extract-repo script
35
- * Can be used to extract files from one Foobara project to another, preserving history.
36
- * Making this easier can help with rearchitecting systems.
93
+ * Can be used to extract files from one Foobara project to another, preserving history
94
+ * Making this easier can help with rearchitecting systems
37
95
  * Custom crud-drivers (if needed)
38
96
  * You could hypothetically write your own custom CRUD driver that knows how
39
- to assemble an entity record with a clean mental model from a mismodeled legacy database.
97
+ to assemble an entity record with a clean mental model from a mismodeled legacy database
40
98
 
41
- ## Installation
99
+ # Installation
42
100
 
43
101
  To add foobara to an existing project, you can add `foobara` gem to your Gemfile or .gemspec as you normally would.
102
+ You can also `gem install foobara` and whatever additional foobara gems you need and use them in
103
+ scripts by requiring them.
104
+
105
+ You could also use a generator to create a new Ruby Foobara project using the `foob` gem with `gem install foob` and
106
+ then run `foob generate ruby-project --name your-org/your-new-project-name`
107
+
108
+ To create a new Typescript React project using a foobara generator, you could install the `foob` gem with `gem install foob` and then
109
+ run `foob generate typescript-react-project --project-dir your-org/your-new-project-name`
110
+
111
+ And then you can import remote commands/types/domains/errors from an existing Ruby foobara backend using:
112
+
113
+ `foob g typescript-remote-commands --manifest-url http://your.foobara.ruby.backend/manifest`
114
+
115
+ And you can also automatically generate some forms for your commands as a nice starting-point with:
116
+
117
+ `foob g typescript-react-command-form --command-name SomeOrg::SomeDomain::SomeCommand`
118
+
119
+ # Usage/Tutorial
120
+
121
+ Let's explore various Foobara concepts with some code examples!
122
+
123
+ ## Foobara 101
124
+
125
+ ### Commands
126
+
127
+ Foobara commands are meant to encapsulate high-level domain operations and are meant
128
+ to be the public interface to Foobara systems/subsystems.
129
+
130
+ Command's interface is heavily inspired by the great cypriss/mutations gem. Let's create a command that adds two numbers to
131
+ demonstrate Command's interface:
132
+
133
+ ```ruby
134
+ #!/usr/bin/env ruby
135
+
136
+ require "foobara"
137
+
138
+ class Add < Foobara::Command
139
+ inputs do
140
+ operand1 :integer, :required
141
+ operand2 :integer, :required
142
+ end
143
+
144
+ result :integer
145
+
146
+ def execute
147
+ add_operands
148
+
149
+ sum
150
+ end
151
+
152
+ attr_accessor :sum
153
+
154
+ def add_operands
155
+ self.sum = operand1 + operand2
156
+ end
157
+ end
158
+
159
+ require "irb"
160
+ IRB.start(__FILE__)
161
+ ```
162
+
163
+ You need to `chmod u+x add.rb` to make it executable (assuming you put this in add.rb)
164
+
165
+ IRB at the end just gives us an interactive session. You could remove that and just put whatever code to test `Add`
166
+ that you want.
167
+
168
+ Note: for brevity, from now on we will leave the shebang and irb calls out of the examples.
169
+
170
+ Some things to note about recommended conventions:
171
+ * It is recommended that your #execute method be self documenting and only call helper methods
172
+ preferably passing no arguments.
173
+ * We use runtime context via `attr_accessor :sum` to store the computed sum.
174
+
175
+ Let's play with it!
176
+
177
+ We can run our Add command several ways. First, let's create an instance of it and call the #run method:
178
+
179
+ ```irb
180
+ $ ./add.rb
181
+ > command = Add.new(operand1: 2, operand2: 5)
182
+ ==> #<Add:0xad20 @raw_inputs={:operand1=>2, :operand2=>5}, @error_collectio...
183
+ > outcome = command.run
184
+ ==> #<Foobara::Outcome:0x00007fd9e60a3800...
185
+ > outcome.success?
186
+ ==> true
187
+ > outcome.result
188
+ ==> 7
189
+ ```
190
+
191
+ When we run a command we get an Outcome. We can ask it if it is successful with #success? and
192
+ we can also get the result with #result and errors with #errors and other helper methods.
193
+
194
+ We can also just run it with .run without creating an instance:
195
+
196
+ ```irb
197
+ > outcome = Add.run(operand1: 2, operand2: 5)
198
+ ==> #<Foobara::Outcome:0x00007ffbcc641318...
199
+ > outcome.success?
200
+ ==> true
201
+ > outcome.result
202
+ ==> 7
203
+ ```
204
+
205
+ And we can use .run! if we want just the result or an exception raised:
206
+
207
+ ```irb
208
+ > Add.run!(operand1: 2, operand2: 5)
209
+ ==> 7
210
+ ```
211
+
212
+ Let's cause some errors!
213
+
214
+ ```irb
215
+ > outcome = Add.run(operand1: "foo", operand2: 5)
216
+ ==> #<Foobara::Outcome:0x00007ffbcc60aea8...
217
+ > outcome.success?
218
+ ==> false
219
+ > puts outcome.errors_sentence
220
+ At operand1: Cannot cast "foo" to an integer. Expected it to be a Integer, or be a string of digits optionally with a minus sign in front
221
+ ```
222
+
223
+ Here we used something that wasn't castable to an integer.
224
+
225
+ ```irb
226
+ > outcome = Add.run
227
+ ==> #<Foobara::Outcome:0x00007ffbcb9d97b0...
228
+ > outcome.success?
229
+ ==> false
230
+ > puts outcome.errors_sentence
231
+ Missing required attribute operand1, and Missing required attribute operand2
232
+ ```
233
+
234
+ Here we omitted some required attributes.
235
+
236
+ ### Organizations and Domains
237
+
238
+ Domains operate as namespaces for Commands, types, and errors. Domains are namespaces, typically of Commands, types,
239
+ errors, and DomainMappers. They should group concepts related to one conceptual domain.
240
+ They can depend on other domains with unidirectional dependencies
241
+
242
+ Let's put our Add command into an IntegerMath domain:
243
+
244
+ ```ruby
245
+ module IntegerMath
246
+ foobara_domain!
247
+ end
248
+
249
+ module IntegerMath
250
+ class Add < Foobara::Command
251
+ inputs do
252
+ operand1 :integer, :required
253
+ operand2 :integer, :required
254
+ end
255
+
256
+ result :integer
257
+
258
+ def execute
259
+ add_operands
260
+
261
+ sum
262
+ end
263
+
264
+ attr_accessor :sum
265
+
266
+ def add_operands
267
+ self.sum = operand1 + operand2
268
+ end
269
+ end
270
+ end
271
+ ```
272
+
273
+ We create a domain by calling `.foobara_domain!` on the module we wish to make into a domain.
274
+
275
+ The typical way of putting commands and other Foobara concepts into a domain is to just define them inside that module.
276
+
277
+ We can play a bit with our new domain:
278
+
279
+ ```irb
280
+ > IntegerMath.foobara_command_classes
281
+ ==> [IntegerMath::Add]
282
+ > IntegerMath.foobara_lookup(:Add)
283
+ ==> IntegerMath::Add
284
+ ```
285
+
286
+ Organizations are namespaces of Domains. Commonly these might be the name of the team or company implementing the
287
+ domains in the organization.
288
+
289
+ Let's create an Organization and just call it FoobaraExamples and place our Domain in it:
290
+
291
+ ```ruby
292
+ module FoobaraExamples
293
+ foobara_organization!
294
+
295
+ module IntegerMath
296
+ foobara_domain!
297
+
298
+ class Add < Foobara::Command
299
+ inputs do
300
+ operand1 :integer, :required
301
+ operand2 :integer, :required
302
+ end
303
+
304
+ result :integer
305
+
306
+ def execute
307
+ add_operands
308
+
309
+ sum
310
+ end
311
+
312
+ attr_accessor :sum
313
+
314
+ def add_operands
315
+ self.sum = operand1 + operand2
316
+ end
317
+ end
318
+ end
319
+ end
320
+ ```
321
+
322
+ And we can play with our Organization:
323
+
324
+ ```irb
325
+ > FoobaraExamples.foobara_domains
326
+ ==> [FoobaraExamples::IntegerMath]
327
+ ```
328
+
329
+ ### Types
330
+
331
+ We have so far seen one Foobara type which is `integer` but there are many others.
332
+
333
+ We used :integer to type the operands of our Add command. There are many ways to express types in Foobara
334
+ but in this case we used the attributes DSL. It has the form:
335
+
336
+ `<attribute_naem> <type_symbol> [processors] [description]`
337
+
338
+ We used a processor `:required` but there are many others and you can create your own.
339
+
340
+ We could have for example done:
341
+
342
+ ```ruby
343
+ some_integer :integer, :required, one_of: [10, 20, 30], max: 100, min: 0, "An integer with some pointless validations!"
344
+ ```
345
+
346
+ Not really useful but shows some existing processors that can be applied to integers.
347
+
348
+ We will avoid going deeper for now since this is Foobara 101 still so let's keep moving along.
349
+
350
+ ### Models
351
+
352
+ A very important type when implementing complex domains is `model`
353
+
354
+ Let's create a simple `Capybara` model:
355
+
356
+ ```ruby
357
+ class Capybara < Foobara::Model
358
+ attributes do
359
+ name :string, :required, "Official name"
360
+ nickname :string, "Informal name for friends"
361
+ age :integer, :required, "The number of times this capybara has gone around the sun"
362
+ end
363
+ end
364
+ ```
365
+
366
+ There are different ways to express types in Foobara. Here, we are using an attributes DSL.
367
+
368
+ Let's make some instances of our Capybara model
369
+
370
+ ```bash
371
+ > fumiko = Capybara.new(name: "Fumiko", nickname: "foo", age: 100)
372
+ ==> #<Capybara:0x00007fa27913d6e8 @attributes={:name=>"Fumiko", :nickname=>"foo", :age=>100}, @mutable=true>
373
+ > fumiko.name
374
+ ==> "Fumiko"
375
+ > fumiko.age
376
+ ==> 100
377
+ ```
378
+
379
+ Let's use our model type in a command! Let's make a command called `IncrementAge` to
380
+ carry out a Capybara making it around the sun:
381
+
382
+ ```ruby
383
+ class Capybara < Foobara::Model
384
+ attributes do
385
+ name :string, :required, "Official name"
386
+ nickname :string, "Informal name for friends"
387
+ age :integer, :required, "The number of times this capybara has gone around the sun"
388
+ end
389
+ end
390
+
391
+ class IncrementAge < Foobara::Command
392
+ inputs do
393
+ capybara Capybara, :required
394
+ end
395
+
396
+ result Capybara
397
+
398
+ def execute
399
+ increment_age
400
+
401
+ capybara
402
+ end
403
+
404
+ def increment_age
405
+ capybara.age += 1
406
+ end
407
+ end
408
+ ```
409
+
410
+ Let's increment some ages!
411
+
412
+ ```irb
413
+ > barbara = Capybara.new(name: "Barbara", age: 200, nickname: "bar")
414
+ ==> #<Capybara:0x00007f0ac121dbf8 @attributes={:name=>"Barbara", :age=>200, :nickname=>"bar"}, @mutable=true>
415
+ > barbara.age
416
+ ==> 200
417
+ > IncrementAge.run!(capybara: barbara)
418
+ ==> #<Capybara:0x00007f0ac121dbf8 @attributes={:name=>"Barbara", :age=>201, :nickname=>"bar"}, @mutable=true>
419
+ > barbara.age
420
+ ==> 201
421
+ ```
422
+
423
+ Here we incremented Barbara's age.
424
+
425
+ Check this out though...
426
+
427
+ ```ruby
428
+ > basil = IncrementAge.run!(capybara: { name: "Basil", age: 300, nickname: "baz" })
429
+ ==> #<Capybara:0x00007f0ac1295f40 @attributes={:name=>"Basil", :age=>301, :nickname=>"baz"}, @mutable=true>
430
+ > basil.age
431
+ ==> 301
432
+ ```
433
+
434
+ Whoa, what is this? We passed in attributes for a Capybara instead of a capybara and it gave us
435
+ back a capybara model instance. This comes in convenient in various use-cases.
436
+
437
+ ### Entities
438
+
439
+ Let's upgrade our Capybara model to an entity:
440
+
441
+ ```ruby
442
+ crud_driver = Foobara::Persistence::CrudDrivers::InMemory.new
443
+ Foobara::Persistence.default_crud_driver = crud_driver
444
+
445
+ class Capybara < Foobara::Entity
446
+ attributes do
447
+ id :integer
448
+ name :string, :required, "Official name"
449
+ nickname :string, "Informal name for friends"
450
+ age :integer, :required, "The number of times this capybara has gone around the sun"
451
+ end
452
+
453
+ primary_key :id
454
+ end
455
+ ```
456
+
457
+ Here, we added an InMemory CRUD driver and set it as the default. This lets us write/read records to/from memory.
458
+
459
+ 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
+
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:
528
+
529
+ ```ruby
530
+ class CreateCapybara < Foobara::Command
531
+ description "Just creates a capybara!"
532
+
533
+ inputs Capybara.attributes_for_create
534
+ result Capybara
535
+
536
+ def execute
537
+ create_capybara
538
+
539
+ capybara
540
+ end
541
+
542
+ attr_accessor :capybara
543
+
544
+ def create_capybara
545
+ self.capybara = Capybara.create(inputs)
546
+ end
547
+ end
548
+ ```
549
+
550
+ And a basic FindCapybara command:
551
+
552
+ ```ruby
553
+ class FindCapybara < Foobara::Command
554
+ inputs do
555
+ id Capybara.primary_key_type, :required
556
+ end
557
+
558
+ result Capybara
559
+
560
+ def execute
561
+ load_capybara
562
+
563
+ capybara
564
+ end
565
+
566
+ attr_accessor :capybara
567
+
568
+ def load_capybara
569
+ self.capybara = Capybara.load(id)
570
+ end
571
+ end
572
+ ```
573
+
574
+ And now let's create some Capybara records and manipulate them:
575
+
576
+ ```ruby
577
+ > fumiko = CreateCapybara.run!(name: "Fumiko", nickname: "foo", age: 100)
578
+ ==> <Capybara:1>
579
+ > barbara = CreateCapybara.run!(name: "Barbara", nickname: "bar", age: 200)
580
+ ==> <Capybara:2>
581
+ > basil = CreateCapybara.run!(name: "Basil", nickname: "baz", age: 300)
582
+ ==> <Capybara:3>
583
+ > basil.age
584
+ ==> 300
585
+ > basil = IncrementAge.run!(capybara: 3)
586
+ ==> <Capybara:3>
587
+ > basil.age
588
+ ==> 301
589
+ > basil = FindCapybara.run!(capybara: 3)
590
+ ==> <Capybara:3>
591
+ > basil.age
592
+ ==> 301
593
+ ```
594
+
595
+ We were able to increment Basil's age using his primary key and we were also able to find his record.
596
+
597
+ 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
+ ephemeral memory. Let's instead persist it to a file. Let's install a file crud driver:
599
+
600
+ ```bash
601
+ > gem install foobara-local-files-crud-driver
602
+ ```
603
+
604
+ And now let's swap out the InMemory crud driver with our file crud driver:
605
+
606
+ ```ruby
607
+ require "foobara/local_files_crud_driver"
608
+
609
+ crud_driver = Foobara::LocalFilesCrudDriver.new
610
+ Foobara::Persistence.default_crud_driver = crud_driver
611
+ ```
612
+
613
+ Now let's create our records again and look at them on disk:
614
+
615
+ ```irb
616
+ > CreateCapybara.run!(name: "Fumiko", nickname: "foo", age: 100)
617
+ ==> <Capybara:1>
618
+ > CreateCapybara.run!(name: "Barbara", nickname: "bar", age: 200)
619
+ ==> <Capybara:2>
620
+ > CreateCapybara.run!(name: "Basil", nickname: "baz", age: 300)
621
+ ==> <Capybara:3>
622
+ > puts File.read("local_data/records.yml")
623
+ ---
624
+ capybara:
625
+ sequence: 4
626
+ records:
627
+ 1:
628
+ :name: Fumiko
629
+ :nickname: foo
630
+ :age: 100
631
+ :id: 1
632
+ 2:
633
+ :name: Barbara
634
+ :nickname: bar
635
+ :age: 200
636
+ :id: 2
637
+ 3:
638
+ :id: 3
639
+ :name: Basil
640
+ :nickname: baz
641
+ :age: 300
642
+ ```
643
+
644
+ Great! Now let's re-run our script and manipulate some data:
645
+
646
+ ```irb
647
+ > basil = FindCapybara.run!(id: 3)
648
+ ==> <Capybara:3>
649
+ > basil.age
650
+ ==> 300
651
+ > basil = IncrementAge.run!(capybara: 3)
652
+ ==> <Capybara:3>
653
+ > basil.age
654
+ ==> 301
655
+ ```
656
+
657
+ We were able to find Basil in a fresh run of our script!
658
+
659
+ Let's find Basil again in another fresh run:
660
+
661
+ ```irb
662
+ > basil = FindCapybara.run!(id: 3)
663
+ ==> <Capybara:3>
664
+ > basil.age
665
+ ==> 301
666
+ ```
667
+
668
+ Basil is still a respectable 301 years old!
669
+
670
+ ### Command connectors
671
+
672
+ Command connectors allow us to expose our commands to the outside world using various technologies
673
+
674
+ #### Command-line connectors
675
+
676
+ Let's install a command-line connector for bash:
677
+
678
+ ```bash
679
+ gem install foobara-sh-cli-connector
680
+ ```
681
+
682
+ Let's use it in our script by adding the following to the bottom of our script:
683
+
684
+ ```ruby
685
+ require "foobara/sh_cli_connector"
686
+
687
+
688
+ command_connector = Foobara::CommandConnectors::ShCliConnector.new
689
+
690
+ command_connector.connect(CreateCapybara)
691
+ command_connector.connect(IncrementAge)
692
+ command_connector.connect(FindCapybara)
693
+
694
+ command_connector.run(ARGV)
695
+ ```
696
+
697
+ And either rename the script to capy-cafe or symlink it.
698
+
699
+ Now let's run our script again:
700
+
701
+ ```irb
702
+ $ ./capy-cafe
703
+ Usage: capy-cafe [GLOBAL_OPTIONS] [ACTION] [COMMAND_OR_TYPE] [COMMAND_INPUTS]
704
+
705
+ Available commands:
706
+
707
+ CreateCapybara Just creates a capybara!
708
+ IncrementAge A trip around the sun!
709
+ FindCapybara Just tell us who you want to find!
710
+ ```
711
+
712
+ Ohhh we get some help now and a list of command we can run. Let's learn more about FindCapybara:
713
+
714
+ ```
715
+ $ ./capy-cafe help FindCapybara
716
+ Usage: capy-cafe [GLOBAL_OPTIONS] FindCapybara [COMMAND_INPUTS]
717
+
718
+ Just tell us who you want to find!
719
+
720
+ Command inputs:
721
+
722
+ -i, --id ID Required
723
+ ```
724
+
725
+ Oh OK, well, let's try to find Basil:
726
+
727
+ ```
728
+ $ ./capy-cafe FindCapybara --id 3
729
+ id: 3,
730
+ name: "Basil",
731
+ nickname: "baz",
732
+ age: 301
733
+ ```
734
+
735
+ Great! Let's see if we can increment Basil's age:
736
+
737
+ ```
738
+ $ ./capy-cafe help IncrementAge
739
+ Usage: capy-cafe [GLOBAL_OPTIONS] IncrementAge [COMMAND_INPUTS]
740
+
741
+ A trip around the sun!
742
+
743
+ Command inputs:
744
+
745
+ -c, --capybara CAPYBARA Required
746
+
747
+ $ ./capy-cafe IncrementAge --capybara 3
748
+ id: 3,
749
+ name: "Basil",
750
+ nickname: "baz",
751
+ age: 302
752
+ $ ./capy-cafe FindCapybara --id 3
753
+ id: 3,
754
+ name: "Basil",
755
+ nickname: "baz",
756
+ age: 302
757
+ ```
758
+
759
+ Yay! Now Basil is an even more respectable 302 years old!
760
+
761
+ #### HTTP Command Connectors
762
+
763
+ Let's now replace our command-line connector with an HTTP connector:
764
+
765
+ We'll choose a Rack connector for now:
766
+
767
+ ```
768
+ gem install foobara-rack-connector
769
+ ```
770
+
771
+ And we can wire it up by replacing the CLI connector code at the bottom of the script with this instead:
772
+
773
+ ```ruby
774
+ require "foobara/rack_connector"
775
+ require "rackup/server"
776
+
777
+ command_connector = Foobara::CommandConnectors::Http::Rack.new
778
+
779
+ command_connector.connect(CreateCapybara)
780
+ command_connector.connect(IncrementAge)
781
+ command_connector.connect(FindCapybara)
782
+
783
+ Rackup::Server.start(app: command_connector)
784
+ ```
785
+
786
+ NOTE: Normally we would call `run command_connector` in a config.ru file but we're hacking this up in a script
787
+ instead of in a structured project so we'll just boot the server this way.
788
+
789
+ If we run it we see:
790
+
791
+ ```
792
+ Puma starting in single mode...
793
+ * Puma version: 6.5.0 ("Sky's Version")
794
+ * Ruby version: ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]
795
+ * Min threads: 0
796
+ * Max threads: 5
797
+ * Environment: development
798
+ * PID: 189938
799
+ * Listening on http://0.0.0.0:9292
800
+ Use Ctrl-C to stop
801
+ ```
802
+
803
+ Great! Our server has booted!
804
+
805
+ We can get help by going to http://localhost:9292/help or help with specific commands or types by going to http://localhost:9292/help/Capybara
806
+ or http://localhost:9292/help/FindCapybara etc.
807
+
808
+ Let's curl FindCapybara to find Fumiko:
809
+
810
+ ```
811
+ $ curl http://localhost:9292/run/FindCapybara?id=1
812
+ {"name":"Fumiko","nickname":"foo","age":100,"id":1}
813
+ ```
814
+
815
+ Yay! We found Fumiko!
816
+
817
+ Let's celebrate her birthday:
818
+
819
+ ```ruby
820
+ $ curl http://localhost:9292/run/IncrementAge?capybara=1
821
+ {"id":1,"name":"Fumiko","nickname":"foo","age":101}
822
+ $ curl http://localhost:9292/run/FindCapybara?id=1
823
+ {"name":"Fumiko","nickname":"foo","age":101,"id":1}
824
+ ```
825
+
826
+ And now she is 101 as expected.
827
+
828
+ Let's try exposing our commands through the Rails router.
829
+
830
+ 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
+
832
+ ```ruby
833
+ gem install rails
834
+ 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
+ ```
836
+
837
+ Now in `config/routes.rb` we could add:
838
+
839
+ ```ruby
840
+ require "foobara/rails_command_connector"
841
+
842
+ Foobara::CommandConnectors::RailsCommandConnector.new
843
+
844
+ command_connector.connect(CreateCapybara)
845
+ command_connector.connect(IncrementAge)
846
+ command_connector.connect(FindCapybara)
847
+ ```
848
+
849
+ We can start rails with:
850
+
851
+ ```
852
+ $ rails s
853
+ ```
854
+
855
+ And then hit our previous URLs although now the port is 3000:
856
+
857
+ ```
858
+ $ curl http://localhost:3000/run/IncrementAge?capybara=1
859
+ {"id":1,"name":"Fumiko","nickname":"foo","age":102}
860
+ $ curl http://localhost:3000/run/FindCapybara?id=1
861
+ {"name":"Fumiko","nickname":"foo","age":102,"id":1}
862
+ ```
863
+
864
+ And now Fumiko is 102!
865
+
866
+ We could also instead of calling #connect we could use a rails routes DSL to connect commands:
867
+
868
+ ```ruby
869
+ require "foobara/rails_command_connector"
870
+
871
+ Foobara::CommandConnectors::RailsCommandConnector.new
872
+
873
+ require "foobara/rails/routes"
874
+
875
+ Rails.application.routes.draw do
876
+ command CreateCapybara
877
+ command IncrementAge
878
+ command FindCapybara
879
+ end
880
+ ```
881
+
882
+ This has the same effect as the previous code and is just a stylistic alternative.
883
+
884
+ #### Async Command Connectors
885
+
886
+ TODO
887
+
888
+ #### Scheduler Command Connectors
889
+
890
+ TODO
891
+
892
+ ## Intermediate Foobara
893
+
894
+ ### Metadata manifests for discoverability
895
+
896
+ Foobara concepts all have a manifest of metadata that can be queried programmatically. This facilitates
897
+ automation tooling and abstracting away integration code.
898
+
899
+ Let's take a quick look at some metadata in our existing systems.
900
+
901
+ Let's for example ask our Capybara entity for its manifest:
902
+
903
+ ```irb
904
+ > Capybara.foobara_manifest
905
+ ==>
906
+ {:attributes_type=>
907
+ {:type=>:attributes,
908
+ :element_type_declarations=>
909
+ {:id=>{:type=>:integer},
910
+ :name=>{:description=>"Official name", :type=>:string},
911
+ :nickname=>{:description=>"Informal name for friends", :type=>:string},
912
+ :age=>{:description=>"The number of times this capybara has gone around the sun", :type=>:integer}},
913
+ :required=>[:name, :age]},
914
+ :organization_name=>"global_organization",
915
+ :domain_name=>"global_domain",
916
+ :model_name=>"Capybara",
917
+ :model_base_class=>"Foobara::Entity",
918
+ :model_class=>"Capybara",
919
+ :entity_name=>"Capybara",
920
+ :primary_key_attribute=>:id}
921
+ ```
922
+
923
+ Let's ask our Rack connector for a list of commands it exposes:
924
+
925
+ ```irb
926
+ > command_connector.foobara_manifest[:command].keys
927
+ ==> [:CreateCapybara, :FindCapybara, :IncrementAge]
928
+ ```
929
+
930
+ We can see all the different categories of concepts available by looking at the top-level keys:
931
+
932
+ ```irb
933
+ > puts command_connector.foobara_manifest.keys.sort
934
+ command
935
+ domain
936
+ error
937
+ organization
938
+ processor
939
+ processor_class
940
+ type
941
+ ```
942
+
943
+ ### Remote Commands
944
+
945
+ One use of these metadata manifests is importing remote commands/orgs/domains/errors/types. This allows us to run
946
+ commands from other systems as if they were implemented locally.
947
+
948
+ Let's install foobara-remote-imports:
949
+
950
+ ```
951
+ $ gem install foobara-remote-imports
952
+ ```
953
+
954
+ And lLet's create a new script and import our various Capybara commands over HTTP:
955
+
956
+ ```ruby
957
+ #!/usr/bin/env ruby
958
+
959
+ require "foobara"
960
+ require "foobara/remote_imports"
961
+
962
+ Foobara::RemoteImports::ImportCommand.run!(manifest_url: "http://localhost:9292/manifest", cache: true)
963
+
964
+ require "irb"
965
+ IRB.start(__FILE__)
966
+ ```
967
+
968
+ Let's run this new script and play with it:
969
+
970
+ ```
971
+ $ ./part_2b_remote_commands_import.rb
972
+ > capybara = FindCapybara.run!(id: 1)
973
+ ==> #<Capybara:0x00007f6895bb2998 @attributes={:name=>"Fumiko", :nickname=>"foo", :age=>100, :id=>1}, @mutable=false>
974
+ > capybara.age
975
+ ==> 100
976
+ ```
977
+
978
+ Great! We can now move commands, types, etc, around between systems without needing to refactor calling code. Even
979
+ errors work the same way:
980
+
981
+ ```
982
+
983
+ ```
984
+
985
+ ### Subcommands
986
+
987
+ TODO
988
+
989
+ ### Custom Errors
990
+
991
+ #### Input Errors
992
+
993
+ TODO
994
+
995
+ #### Runtime Errors
996
+
997
+ TODO
998
+
999
+ ## Advanced Foobara
1000
+
1001
+ ### Domain Mappers
1002
+
1003
+ TODO
1004
+
1005
+ ### Code Generators
1006
+
1007
+ #### Generating a new Foobara Ruby project
1008
+ #### Generating a new Foobara Typescript/React project
1009
+ #### Geerating commands, models, entities, types, domains, organizations, etc...
1010
+
1011
+ TODO
1012
+
1013
+ ### Custom types
1014
+
1015
+ TODO
1016
+
1017
+ ## Expert Foobara
1018
+
1019
+ ### Callbacks
1020
+
1021
+ TODO
1022
+
1023
+ ### Transactions in Commands
1024
+
1025
+ TODO
1026
+
1027
+ ### Transactions in tests/console
1028
+
1029
+ TODO
1030
+
1031
+ ### Custom crud drivers
1032
+
1033
+ TODO
1034
+
1035
+ ### Custom command connectors
1036
+
1037
+ TODO
1038
+
1039
+ ### Value processors
1040
+
1041
+ TODO
1042
+
1043
+ ### Custom types from scratch
1044
+
1045
+ TODO
1046
+
1047
+ ### Namespaces
1048
+
1049
+ TODO
1050
+
1051
+ # Additional learning materials/Documentation
1052
+
1053
+ * Overview and code demo videos:
1054
+ * https://foobara.com/videos
1055
+ * https://www.youtube.com/@FoobaraFlix
1056
+ * YARD Docs
1057
+ * All docs combined: https://docs.foobara.com/all/
1058
+ * Per-repository docs: https://foobara.com/docs
1059
+
1060
+ # Contributing
44
1061
 
45
- To create a new project using a foobara generator, you could install the `foob` gem with `gem install foob` and then
46
- run `foob generate ruby-project --name your-org/your-new-project-name`
1062
+ Probably a good idea to reach out if you'd like to contribute code or documentation or other
1063
+ forms of help. We could pair on what you have in mind and you could drive or at least we can make sure
1064
+ it's a good use of time. I can be reached at azimux@gmail.com
47
1065
 
48
- ## Usage
1066
+ You can contribute via a github pull request as is typical
49
1067
 
50
- You can find a code demo video and an overview video of what Foobara is at https://foobara.com
1068
+ Make sure the test suite and linter pass locally before opening a pull request
51
1069
 
52
- ## Contributing
1070
+ The build will fail if test coverage is below 100%
53
1071
 
54
- Can contribute via a github pull request as is typical but see info about licensing below first.
1072
+ ## Developing locally
55
1073
 
56
- Make sure the test suite and linter pass locally before opening a pull request.
57
- The build will fail if test coverage is below 100%.
1074
+ You should be able to do the typical stuff:
58
1075
 
59
- It might be a good idea to reach out for advice if unsure how to chip away at the part of this project
60
- that you are interested in.
1076
+ ```bash
1077
+ git clone git@github.com:foobara/foobara
1078
+ cd foobara
1079
+ bundle
1080
+ rake
1081
+ ```
61
1082
 
62
- ### Developing locally
1083
+ And if the tests/linter pass then you could dive into modifying the code
63
1084
 
64
- You should be able to run `bundle install` and then `rake` to run the test suite and the linter.
1085
+ ## Monorepo Structure
65
1086
 
66
- ### Monorepo Structure
1087
+ Foobara is split up into many projects
67
1088
 
68
- Foobara is split up into many projects. Many are in separate repositories. This repository however is unique
69
- in the Foobara ecosystem of projects because it is a monorepo. Sometimes projects are extracted from here
70
- into their own repositories.
1089
+ Many are in separate repositories which you can see at: https://github.com/orgs/foobara/repositories
71
1090
 
72
- Each project has its own directory in the projects/ directory.
1091
+ This repository, however, is a monorepo. Sometimes projects are extracted from here
1092
+ into their own repositories. Each project in this repository has its own directory in the projects/ directory.
73
1093
 
74
- ### Licensing
1094
+ # Licensing
75
1095
 
76
1096
  Foobara is licensed under the Mozilla Public License Version 2.0. Please see LICENSE.txt for more info.
@@ -264,17 +264,13 @@ module Foobara
264
264
  type
265
265
  end
266
266
 
267
- def foobara_register_model(model_class, reregister: false)
267
+ def foobara_register_model(model_class)
268
268
  type = model_class.model_type
269
269
 
270
270
  if type.scoped_path_set? && foobara_registered?(type.scoped_full_name, mode: Namespace::LookupMode::DIRECT)
271
- if reregister
272
- foobara_unregister(type)
273
- else
274
- # :nocov:
275
- raise AlreadyRegisteredError, "Already registered: #{type.inspect}"
276
- # :nocov:
277
- end
271
+ # :nocov:
272
+ raise AlreadyRegisteredError, "Already registered: #{type.inspect}"
273
+ # :nocov:
278
274
  end
279
275
 
280
276
  foobara_register(type)
@@ -283,10 +279,6 @@ module Foobara
283
279
  type.target_class
284
280
  end
285
281
 
286
- def foobara_reregister_model(model_class)
287
- foobara_register_model(model_class, reregister: true)
288
- end
289
-
290
282
  # TODO: kill this off
291
283
  def foobara_register_entity(name, *args, &block)
292
284
  # TODO: introduce a Namespace#scope method to simplify this a bit
@@ -25,7 +25,13 @@ module Foobara
25
25
  end
26
26
 
27
27
  def entity_class
28
- Object.const_get(parent_declaration_data[:model_class])
28
+ type = parent_declaration_data[:type]
29
+
30
+ if type == :entity
31
+ Object.const_get(parent_declaration_data[:model_class])
32
+ else
33
+ Foobara::Namespace.current.foobara_lookup_type!(type).target_class
34
+ end
29
35
  end
30
36
  end
31
37
  end
@@ -10,6 +10,31 @@ module Foobara
10
10
  attr_reader :model_type
11
11
  attr_writer :attributes_type
12
12
 
13
+ def mutable(*args)
14
+ args_size = args.size
15
+ case args.size
16
+ when 0
17
+ if defined?(@mutable_override)
18
+ @mutable_override
19
+ else
20
+ type = model_type
21
+
22
+ if type
23
+ if type.declaration_data.key?(:mutable)
24
+ type.declaration_data[:mutable]
25
+ end
26
+ end
27
+ end
28
+ when 1
29
+ @mutable_override = args.first
30
+ set_model_type
31
+ else
32
+ # :nocov:
33
+ raise ArgumentError, "Expected 0 or 1 arguments but got #{args_size}"
34
+ # :nocov:
35
+ end
36
+ end
37
+
13
38
  def attributes(*args, **opts, &)
14
39
  new_type = domain.foobara_type_from_declaration(*args, **opts, &)
15
40
 
@@ -42,13 +67,16 @@ module Foobara
42
67
  if attributes_type
43
68
  declaration = type_declaration(attributes_type.declaration_data)
44
69
 
45
- domain.foobara_type_from_declaration(declaration)
46
-
47
- unless @model_type
48
- # :nocov:
49
- raise "Expected model type to automatically be registered"
50
- # :nocov:
70
+ if model_type
71
+ unless Foobara::TypeDeclarations.declarations_equal?(declaration, model_type.declaration_data)
72
+ domain.foobara_unregister(model_type)
73
+ self.model_type = nil
74
+ domain.foobara_type_from_declaration(declaration)
75
+ end
76
+ else
77
+ domain.foobara_type_from_declaration(declaration)
51
78
  end
79
+
52
80
  end
53
81
  end
54
82
 
@@ -65,11 +93,12 @@ module Foobara
65
93
  type: :model,
66
94
  name: model_name,
67
95
  model_module: model_module_name,
68
- model_class: self,
69
- model_base_class: superclass,
96
+ model_class: name,
97
+ model_base_class: superclass.name,
70
98
  attributes_declaration:,
71
99
  description:,
72
- _desugarized: { type_absolutified: true }
100
+ _desugarized: { type_absolutified: true },
101
+ mutable:
73
102
  )
74
103
  end
75
104
 
@@ -14,7 +14,13 @@ module Foobara
14
14
  end
15
15
 
16
16
  def model_class
17
- Object.const_get(parent_declaration_data[:model_class])
17
+ type = parent_declaration_data[:type]
18
+
19
+ if type == :model
20
+ Object.const_get(parent_declaration_data[:model_class])
21
+ else
22
+ Foobara::Namespace.current.foobara_lookup_type!(type).target_class
23
+ end
18
24
  end
19
25
  end
20
26
  end
@@ -44,9 +44,7 @@ module Foobara
44
44
  Object
45
45
  end
46
46
 
47
- model_class = if klass.is_a?(::Class)
48
- klass
49
- elsif klass && Object.const_defined?(klass) && Object.const_get(klass).is_a?(::Class)
47
+ model_class = if klass && Object.const_defined?(klass) && Object.const_get(klass).is_a?(::Class)
50
48
  Object.const_get(klass)
51
49
  else
52
50
  model_base_class = strictish_type_declaration[:model_base_class] || default_model_base_class
@@ -35,12 +35,9 @@ module Foobara
35
35
  domain = model_class.domain || Domain.global
36
36
 
37
37
  if existing_model_type
38
- if existing_model_type.declaration_data != type.declaration_data &&
39
- domain.foobara_type_registered?(existing_model_type)
40
- type.type_symbol = type.declaration_data[:name]
41
- model_class.model_type = type
42
- domain.foobara_reregister_model(model_class)
43
- end
38
+ # :nocov:
39
+ raise "Did not expect #{type.declaration_data[:name]} to already exist"
40
+ # :nocov:
44
41
  else
45
42
  model_class.model_type = type
46
43
  type.type_symbol = type.declaration_data[:name]
@@ -42,6 +42,15 @@ module Foobara
42
42
  def strict_stringified?
43
43
  Thread.foobara_var_get(:foobara_type_declarations_mode) == Mode::STRICT_STRINGIFIED
44
44
  end
45
+
46
+ # TODO: we should desugarize these but can't because of a bug where desugarizing entities results in creating the
47
+ # entity class in memory, whoops.
48
+ def declarations_equal?(declaration1, declaration2)
49
+ declaration1 = declaration1.reject { |(k, _v)| k.to_s.start_with?("_") }.to_h
50
+ declaration2 = declaration2.reject { |(k, _v)| k.to_s.start_with?("_") }.to_h
51
+
52
+ declaration1 == declaration2
53
+ end
45
54
  end
46
55
  end
47
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.28
4
+ version: 0.0.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-05 00:00:00.000000000 Z
11
+ date: 2024-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: foobara-util