foobara 0.0.27 → 0.0.29
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +1051 -31
- data/projects/command/src/concerns/subcommands.rb +1 -1
- data/projects/domain/src/domain_module_extension.rb +10 -20
- data/projects/entity/src/extensions/builtin_types/entity/casters/hash.rb +7 -1
- data/projects/model/src/concerns/types.rb +38 -9
- data/projects/model/src/extensions/builtin_types/model/casters/hash.rb +7 -1
- data/projects/model/src/extensions/type_declarations/handlers/extend_model_type_declaration/model_class_desugarizer.rb +1 -3
- data/projects/model/src/extensions/type_declarations/handlers/extend_model_type_declaration/to_type_transformer.rb +3 -6
- data/projects/type_declarations/src/type_declarations.rb +9 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8722f293cfd3487965f3863448b8cf175481df41f364fe9c8a880c19430cbd86
|
4
|
+
data.tar.gz: 14aafe8e69546446ec65694d6f48ea14b44af2e94bbd2a05a0cd71209831bfd8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 024d6d200f25cef54f5aa8a719af536d6f5e8fbf767fa6e9aa30ac04ba6f6c2f30815964cf1e0a079944e7a588c4afb91aaadbabe6c47ca9e001f0f3b01e8649
|
7
|
+
data.tar.gz: a31f5904edef9faf390075de61229dd0890e0251a1b9e5e5d870a900f7ef1744d0cf4dbedf2812e9f9559773cc8f9dc5cbec89518a237adbfc82b4dace2d5642
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
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
|
+
|
7
|
+
## [0.0.28] - 2024-12-05
|
8
|
+
|
9
|
+
- Make Domain#foobara_depends_on? give a more intuitive answer
|
10
|
+
|
1
11
|
## [0.0.27] - 2024-12-04
|
2
12
|
|
3
13
|
- Add some Error DSL convenience methods (symbol/message/context)
|
data/README.md
CHANGED
@@ -1,76 +1,1096 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
1066
|
+
You can contribute via a github pull request as is typical
|
49
1067
|
|
50
|
-
|
1068
|
+
Make sure the test suite and linter pass locally before opening a pull request
|
51
1069
|
|
52
|
-
|
1070
|
+
The build will fail if test coverage is below 100%
|
53
1071
|
|
54
|
-
|
1072
|
+
## Developing locally
|
55
1073
|
|
56
|
-
|
57
|
-
The build will fail if test coverage is below 100%.
|
1074
|
+
You should be able to do the typical stuff:
|
58
1075
|
|
59
|
-
|
60
|
-
|
1076
|
+
```bash
|
1077
|
+
git clone git@github.com:foobara/foobara
|
1078
|
+
cd foobara
|
1079
|
+
bundle
|
1080
|
+
rake
|
1081
|
+
```
|
61
1082
|
|
62
|
-
|
1083
|
+
And if the tests/linter pass then you could dive into modifying the code
|
63
1084
|
|
64
|
-
|
1085
|
+
## Monorepo Structure
|
65
1086
|
|
66
|
-
|
1087
|
+
Foobara is split up into many projects
|
67
1088
|
|
68
|
-
|
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
|
-
|
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
|
-
|
1094
|
+
# Licensing
|
75
1095
|
|
76
1096
|
Foobara is licensed under the Mozilla Public License Version 2.0. Please see LICENSE.txt for more info.
|
@@ -20,7 +20,7 @@ module Foobara
|
|
20
20
|
domain = self.class.domain
|
21
21
|
sub_domain = subcommand_class.domain
|
22
22
|
|
23
|
-
unless domain.
|
23
|
+
unless domain.foobara_can_call_subcommands_from?(sub_domain)
|
24
24
|
raise CannotAccessDomain,
|
25
25
|
"Cannot access #{sub_domain} or its commands because #{domain} does not depend on it"
|
26
26
|
end
|
@@ -264,17 +264,13 @@ module Foobara
|
|
264
264
|
type
|
265
265
|
end
|
266
266
|
|
267
|
-
def foobara_register_model(model_class
|
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
|
-
|
272
|
-
|
273
|
-
|
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
|
@@ -353,16 +345,14 @@ module Foobara
|
|
353
345
|
nil
|
354
346
|
end
|
355
347
|
|
356
|
-
def
|
348
|
+
def foobara_can_call_subcommands_from?(other_domain)
|
357
349
|
other_domain = Domain.to_domain(other_domain)
|
350
|
+
other_domain == self || self == GlobalDomain || foobara_depends_on?(other_domain)
|
351
|
+
end
|
358
352
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
# depends_on with the global domain in .foobara_domain! but not as clear how to fix the check
|
363
|
-
# against self.
|
364
|
-
self == GlobalDomain || other_domain == self || other_domain == GlobalDomain ||
|
365
|
-
foobara_depends_on.include?(other_domain.foobara_full_domain_name)
|
353
|
+
def foobara_depends_on?(other_domain)
|
354
|
+
other_domain = Domain.to_domain(other_domain)
|
355
|
+
other_domain == GlobalDomain || foobara_depends_on.include?(other_domain.foobara_full_domain_name)
|
366
356
|
end
|
367
357
|
|
368
358
|
def foobara_depends_on(*domains)
|
@@ -25,7 +25,13 @@ module Foobara
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def entity_class
|
28
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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:
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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.
|
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-
|
11
|
+
date: 2024-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: foobara-util
|