foobara 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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