foobara 0.0.34 → 0.0.36

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc9733f9966487e9f526b01792182ff02718e7f3ac445256d24397942eddf4ec
4
- data.tar.gz: 4e611e1bfae4bc3418108ad93680555e6497d1426a465697827064523ed940d5
3
+ metadata.gz: 796f1d29d3a7ab80b6ae136f254a80549a75ec952ec1219701960abe24bd1256
4
+ data.tar.gz: c99fc6d286203521a9242dbc7ed042a27161b12fb213eec3b44c02f4cca26236
5
5
  SHA512:
6
- metadata.gz: a57062ce5885cc626ac32c66dcf9902c1ace2d50d2182c0ec188ba01e3bc662f9e0d6e2fe4d8b6626a23b25f1046e71da813d0b35e8b3d37c895f074d901feb7
7
- data.tar.gz: 7d6724fff8dc1ee6961f6585ab9c5b78560962725511cd3d316451dda387d8455ddc0a9808c8617aabed61d4aeb656698a1b7a92ef2b760a7248c96bc6b84f81
6
+ metadata.gz: bd63e1dfe5afab9b6b29c855352d3db79a1fa4c04f912c385eeeeb187d2bedbf558fa929d021b276b510ce8d9b62aa4d41da92f8fba7aa6d12e6b752b47d80db
7
+ data.tar.gz: 992b76e3ba595df506570c311a93174e9745ed9e9f5af310cd3b56c1b0274b668f4f38bd3344499c11dd352b5e37ab7bf400ef4421e0b9810d774f91290505c7
data/CHANGELOG.md CHANGED
@@ -1,6 +1,8 @@
1
- ## [0.0.34] - 2024-12-10
1
+ ## [0.0.36] - 2024-12-10
2
2
 
3
3
  - Fix bug with command-named convenience functions
4
+ - Make domain mappers cast their inputs
5
+ - Exclude detached entities from associations
4
6
 
5
7
  ## [0.0.33] - 2024-12-09
6
8
 
data/README.md CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  <!-- TOC -->
3
2
  * [What is/Why Foobara?](#what-iswhy-foobara)
4
3
  * [Commands](#commands)
@@ -19,6 +18,7 @@
19
18
  * [Async Command Connectors](#async-command-connectors)
20
19
  * [Scheduler Command Connectors](#scheduler-command-connectors)
21
20
  * [Intermediate Foobara](#intermediate-foobara)
21
+ * [Metadata manifests for discoverability](#metadata-manifests-for-discoverability)
22
22
  * [Remote Commands](#remote-commands)
23
23
  * [Subcommands](#subcommands)
24
24
  * [Custom Errors](#custom-errors)
@@ -979,34 +979,554 @@ Great! We can now move commands, types, etc, around between systems without need
979
979
  errors work the same way:
980
980
 
981
981
  ```
982
-
982
+ > puts FindCapybara.run(id: "asdf").errors_sentence
983
+ At id: Cannot cast "asdf" to an integer. Expected it to be a Integer, or be a string of digits optionally with a minus sign in front
983
984
  ```
984
985
 
985
986
  ### Subcommands
986
987
 
987
- TODO
988
+ Inevitably, we'll want one high-level domain operation to be able to invoke another high-level domain operation. And
989
+ because Foobara commands are the public interfaces to our systems/subsystems, we really really want to be able to do
990
+ this when a command needs a behavior from another domain as an implementation detail.
991
+
992
+ Let's create a command that calls another. Remember our `Add` command from earlier?
993
+ Let's implement a contrived Subtract command that is implemented using Add:
994
+
995
+ ```ruby
996
+
997
+ class Subtract < Foobara::Command
998
+ inputs do
999
+ operand1 :integer, :required
1000
+ operand2 :integer, :required
1001
+ end
1002
+
1003
+ result :integer
1004
+
1005
+ depends_on Add
1006
+
1007
+ def execute
1008
+ subtract_operands
1009
+
1010
+ difference
1011
+ end
1012
+
1013
+ attr_accessor :difference
1014
+
1015
+ def subtract_operands
1016
+ self.difference = run_subcommand!(Add, operand1:, operand2: -operand2)
1017
+ end
1018
+ end
1019
+ ```
1020
+
1021
+ We call our subcommand using `#run_subcommand!`. This will run the command and return the result. If an error occurs,
1022
+ the errors from Add will be appended to our errors for Subtract, causing it to fail.
1023
+
1024
+ Note that we declare that Subtract depends on Add. We do this using `.depends_on`. This helps in a few ways.
1025
+ For one, Subtract.possible_errors can include errors that might happen in Add.
1026
+ This allows us to see a command dependencies using graphing tools and what-not and enforce a unidirectional
1027
+ dependency graph of commands.
1028
+
1029
+ Let's play with it:
1030
+
1031
+ ```ruby
1032
+ > Subtract.run!(operand1: 5, operand2: 2)
1033
+ ==> 3
1034
+ ```
1035
+
1036
+ We get the answer we expected!
1037
+
1038
+ A little bit advanced but let's look at the possible errors for Subtract:
1039
+
1040
+ ```irb
1041
+ > Subtract.possible_errors.map(&:key).map(&:to_s).sort
1042
+ ==>
1043
+ ["add>data.cannot_cast",
1044
+ "add>data.missing_required_attribute",
1045
+ "add>data.operand1.cannot_cast",
1046
+ "add>data.operand1.missing_required_attribute",
1047
+ "add>data.operand2.cannot_cast",
1048
+ "add>data.operand2.missing_required_attribute",
1049
+ "add>data.unexpected_attributes",
1050
+ "data.cannot_cast",
1051
+ "data.missing_required_attribute",
1052
+ "data.operand1.cannot_cast",
1053
+ "data.operand1.missing_required_attribute",
1054
+ "data.operand2.cannot_cast",
1055
+ "data.operand2.missing_required_attribute",
1056
+ "data.unexpected_attributes"]
1057
+ ```
1058
+
1059
+ We can see some errors from Add here. Note: we actually know in this case that we don't expect these errors to occur.
1060
+ We could filter these out to improve the information to the outside world/tooling/generators but that's beyond
1061
+ the intermediate level.
988
1062
 
989
1063
  ### Custom Errors
990
1064
 
1065
+ Speaking of errors, an intermediate Foobara skill is defining a custom error.
1066
+
991
1067
  #### Input Errors
992
1068
 
993
- TODO
1069
+ Let's make a DivideByZeroError as an example. First, let's make a command that would use it. Q: can you
1070
+ guess which command we're going to make next? A: Divide!
1071
+
1072
+ ```ruby
1073
+ class Divide < Foobara::Command
1074
+ inputs do
1075
+ dividend :integer, :required
1076
+ divisor :integer, :required
1077
+ end
1078
+
1079
+ result :integer
1080
+
1081
+ depends_on Subtract
1082
+
1083
+ def execute
1084
+ initialize_quotient_to_zero
1085
+ make_operands_positive_and_determine_if_result_is_negative
1086
+
1087
+ until dividend_less_than_divisor?
1088
+ increment_quotient
1089
+ subtract_divisor_from_dividend
1090
+ end
1091
+
1092
+ negate_quotient if negative_result?
1093
+
1094
+ quotient
1095
+ end
1096
+
1097
+ attr_accessor :negative_result, :quotient
1098
+
1099
+ def make_operands_positive_and_determine_if_result_is_negative
1100
+ self.negative_result = false
1101
+
1102
+ if dividend < 0
1103
+ self.dividend = -dividend
1104
+ self.negative_result = !negative_result
1105
+ end
1106
+
1107
+ if divisor < 0
1108
+ self.divisor = -divisor
1109
+ self.negative_result = !negative_result
1110
+ end
1111
+ end
1112
+
1113
+ def negate_quotient
1114
+ self.quotient = -quotient
1115
+ end
1116
+
1117
+ def dividend_less_than_divisor?
1118
+ dividend < divisor
1119
+ end
1120
+
1121
+ def negative_result?
1122
+ negative_result
1123
+ end
1124
+
1125
+ def increment_quotient
1126
+ self.quotient += 1
1127
+ end
1128
+
1129
+ def subtract_divisor_from_dividend
1130
+ self.dividend = run_subcommand!(Subtract, operand1: dividend, operand2: divisor)
1131
+ end
1132
+
1133
+ def initialize_quotient_to_zero
1134
+ self.quotient = 0
1135
+ end
1136
+
1137
+ attr_writer :dividend, :divisor
1138
+
1139
+ def dividend
1140
+ @dividend || super
1141
+ end
1142
+
1143
+ def divisor
1144
+ @divisor || super
1145
+ end
1146
+ end
1147
+ ```
1148
+
1149
+ This one is pretty long because it has a more complex algorithm. Note how the #execute method
1150
+ has a self-documenting form of the algorithm in it. That is a good best-practice when it comes to commands.
1151
+ In a real project, this would be encapsulating a high-level domain operation. Having various levels
1152
+ of abstraction of the algorithm mixed together can harm our ability to see what the domain
1153
+ operation entails at a high-level.
1154
+
1155
+ Let's play with it:
1156
+
1157
+ ```irb
1158
+ > Divide.run!(dividend: 6, divisor: 7)
1159
+ ==> 0
1160
+ > Divide.run!(dividend: 8, divisor: 7)
1161
+ ==> 1
1162
+ > Divide.run!(dividend: 49, divisor: 7)
1163
+ ==> 7
1164
+ ```
1165
+
1166
+ We get the expected integer division results. However, if we pass
1167
+ 0 as the divisor, it will hang forever.
1168
+
1169
+ Time for our custom error!
1170
+
1171
+ ```ruby
1172
+ class Divide < Foobara::Command
1173
+ possible_input_error :divisor, :divide_by_zero, message: "Cannot divide by zero"
1174
+
1175
+ ...
1176
+ ```
1177
+
1178
+ This is one way we can express a custom error for associated with a specific input.
1179
+
1180
+ Let's try it out!
1181
+
1182
+ ```irb
1183
+ > outcome = Divide.run(dividend: 49, divisor: 0)
1184
+ ==> #<Foobara::Outcome:0x00007f504d178e38...
1185
+ > outcome.success?
1186
+ ==> false
1187
+ > outcome.errors_hash
1188
+ ==>
1189
+ {"data.divisor.divide_by_zero"=>
1190
+ {:key=>"data.divisor.divide_by_zero",
1191
+ :path=>[:divisor],
1192
+ :runtime_path=>[],
1193
+ :category=>:data,
1194
+ :symbol=>:divide_by_zero,
1195
+ :message=>"Cannot divide by zero",
1196
+ :context=>{},
1197
+ :is_fatal=>false}}
1198
+ > outcome.errors_sentence
1199
+ ==> "Cannot divide by zero"
1200
+ ```
1201
+
1202
+ And we can see the error in the command's list of possible errors:
1203
+
1204
+ ```irb
1205
+ > Divide.possible_errors.map(&:key).map(&:to_s).grep /zero/
1206
+ ==> ["data.divisor.divide_by_zero"]
1207
+ ```
1208
+
1209
+ And of course, as expected, tooling has access to information about this error and the command's possible error through manifest
1210
+ metadata:
1211
+
1212
+ ```irb
1213
+ > Foobara.manifest[:command][:Divide][:possible_errors]["data.divisor.divide_by_zero"][:error]
1214
+ ==> "Divide::DivideByZeroError"
1215
+ > Foobara.manifest[:error][:"Divide::DivideByZeroError"][:parent]
1216
+ ==> [:command, "Divide"]
1217
+ ```
1218
+
1219
+ There's an alternative way to express these custom errors:
1220
+
1221
+ ```ruby
1222
+ class Divide < Foobara::Command
1223
+ class DivideByZeroError < Foobara::DataError
1224
+ def message
1225
+ "Cannot divide by zero"
1226
+ end
1227
+ end
1228
+
1229
+ possible_input_error :divisor, DivideByZeroError
1230
+ ```
1231
+
1232
+ Both do the same thing.
994
1233
 
995
1234
  #### Runtime Errors
996
1235
 
997
- TODO
1236
+ Often, you a command will have to fail due to an error that isn't related to a specific input. For these situations,
1237
+ you want a runtime error. Let's convert our DivideByZeroError to a runtime error just for demonstration purposes:
1238
+
1239
+ ```ruby
1240
+ class Divide < Foobara::Command
1241
+ possible_error :divide_by_zero, message: "Cannot divide by zero"
1242
+
1243
+ def execute
1244
+ validate_divisor
1245
+
1246
+ ...
1247
+ end
1248
+
1249
+ def validate_divisor
1250
+ if divisor == 0
1251
+ add_runtime_error DivideByZeroError
1252
+ end
1253
+ end
1254
+
1255
+ ...
1256
+ ```
1257
+
1258
+ And let's try it out:
1259
+
1260
+ ```irb
1261
+ > outcome = Divide.run(dividend: 49, divisor: 0)
1262
+ ==> #<Foobara::Outcome:0x00007f030fe3b8b8...
1263
+ > outcome.success?
1264
+ ==> false
1265
+ > outcome.errors_sentence
1266
+ ==> "Cannot divide by zero"
1267
+ > outcome.errors_hash
1268
+ ==>
1269
+ {"runtime.divide_by_zero"=>
1270
+ {:key=>"runtime.divide_by_zero",
1271
+ :path=>[],
1272
+ :runtime_path=>[],
1273
+ :category=>:runtime,
1274
+ :symbol=>:divide_by_zero,
1275
+ :message=>"Cannot divide by zero",
1276
+ :context=>{},
1277
+ :is_fatal=>false}}
1278
+ ```
1279
+
1280
+ Very similar behavior to before but this time it's a runtime error.
998
1281
 
999
1282
  ## Advanced Foobara
1000
1283
 
1001
1284
  ### Domain Mappers
1002
1285
 
1286
+ We should really move our various commands into their proper orgs/domains now for the remaining advanced/expert
1287
+ examples.
1288
+
1289
+ In an integer_math_server.rb file, let's put Add/Subtract/Divide and expose them via HTTP:
1290
+
1291
+ ```ruby
1292
+ #!/usr/bin/env ruby
1293
+
1294
+ require "foobara/rack_connector"
1295
+ require "rackup/server"
1296
+
1297
+ module FoobaraDemo
1298
+ foobara_organization!
1299
+
1300
+ module IntegerMath
1301
+ foobara_domain!
1302
+
1303
+ class Add < Foobara::Command
1304
+ ...
1305
+ end
1306
+
1307
+
1308
+ command_connector = Foobara::CommandConnectors::Http::Rack.new
1309
+ command_connector.connect(FoobaraDemo)
1310
+
1311
+ Rackup::Server.start(app: command_connector)
1312
+ ```
1313
+
1314
+ Note: here we have just connected the entire organization. This is just a lazy way for us to expose all commands.
1315
+
1316
+ Let's do the same for our capybara commands.
1317
+ In a capy_cafe_server.rb file, let's put CreateCapybara/IncrementAge/FindCapybara and expose them via HTTP:
1318
+
1319
+ ```ruby
1320
+ #!/usr/bin/env ruby
1321
+
1322
+ require "foobara/local_files_crud_driver"
1323
+ require "foobara/rack_connector"
1324
+ require "rackup/server"
1325
+
1326
+ crud_driver = Foobara::LocalFilesCrudDriver.new
1327
+ Foobara::Persistence.default_crud_driver = crud_driver
1328
+
1329
+ module FoobaraDemo
1330
+ foobara_organization!
1331
+
1332
+ module CapyCafe
1333
+ foobara_domain!
1334
+
1335
+ class Capybara < Foobara::Entity
1336
+ ...
1337
+
1338
+ command_connector = Foobara::CommandConnectors::Http::Rack.new
1339
+
1340
+ command_connector.connect(FoobaraDemo)
1341
+
1342
+ Rackup::Server.start(app: command_connector, Port: 9293)
1343
+ ```
1344
+
1345
+ We'll start this one on 9293 since it will have the same URL as our integer math server.
1346
+
1347
+ Now, let's come up with a contrived use-case for a domain mapper. Let's say there's some information about capybaras
1348
+ in some other model in some other domain that we could import into our CapyCafe domain.
1349
+
1350
+ Let's code up such a domain/model in yet another file called capy_cafe_import.rb,
1351
+ let's set import our other two domains:
1352
+
1353
+ ```ruby
1354
+ #!/usr/bin/env ruby
1355
+
1356
+ require "foobara/remote_imports"
1357
+
1358
+ [9292, 9293].each do |port|
1359
+ Foobara::RemoteImports::ImportCommand.run!(manifest_url: "http://localhost:#{port}/manifest")
1360
+ end
1361
+ ```
1362
+
1363
+ And now let's define an Animal model that could be imported into our CapyCafe domain as a Capybara record:
1364
+
1365
+ ```ruby
1366
+ module FoobaraDemo
1367
+ module AnimalHouse
1368
+ foobara_domain!
1369
+
1370
+ class Animal < Foobara::Model
1371
+ attributes do
1372
+ first_name :string
1373
+ last_name :string
1374
+ birthday :date
1375
+ species :symbol, one_of: %i[capybara cat tartigrade]
1376
+ end
1377
+ end
1378
+ end
1379
+ end
1380
+ ```
1381
+
1382
+ And now let's define a domain mapper that knows how to map an AnimalHouse::Animal to a CapyCafe::Capybara:
1383
+
1384
+ ```ruby
1385
+ module FoobaraDemo
1386
+ module CapyCafe
1387
+ foobara_depends_on AnimalHouse
1388
+
1389
+ class AnimalToCapybara < Foobara::DomainMapper
1390
+ from AnimalHouse::Animal
1391
+ to CreateCapybara
1392
+
1393
+ def map(animal)
1394
+ age = birthday_to_age(animal.birthday)
1395
+
1396
+ {
1397
+ name: "#{animal.first_name} #{animal.last_name}",
1398
+ age:
1399
+ }
1400
+ end
1401
+
1402
+ def birthday_to_age(birthday)
1403
+ today = Date.today
1404
+ age = today.year - birthday.year
1405
+ birthday_this_year = Date.new(birthday.year + age, birthday.month, birthday.day)
1406
+
1407
+ today < birthday_this_year ? age - 1 : age
1408
+ end
1409
+ end
1410
+ end
1411
+ end
1412
+ ```
1413
+
1414
+ Note: that we have a bit of an unusual architecture here: we are defining CapyCafe commands in two different systems.
1415
+ A point of Foobara is that regardless of how these commands are distributed calling code doesn't change as this
1416
+ distribution changes.
1417
+
1418
+ Normally, we wouldn't make use of a domain mapper in isolation. Like everything else, it should be used in the context
1419
+ of a command. But we can play with it directly:
1420
+
1421
+ ```irb
1422
+ $ ./animal_house_import.rb
1423
+ > create_capybara_inputs = FoobaraDemo::CapyCafe::AnimalToCapybara.call(species: :capybara, first_name: "Barbara", last_name: "Doe", birthday: "1000-01-01")
1424
+ ==> {:name=>"Barbara Doe", :age=>1024}
1425
+ > barbara = FoobaraDemo::CapyCafe::CreateCapybara.run!(create_capybara_inputs)
1426
+ ==> <Capybara:2>
1427
+ > barbara.age
1428
+ ==> 1024
1429
+ > barbara.id
1430
+ ==> 2
1431
+ ```
1432
+
1433
+ And we can increment Barbara's age now that she has been imported into our CapyCafe:
1434
+
1435
+ ```irb
1436
+ > FoobaraDemo::CapyCafe::IncrementAge.run!(capybara: barbara)
1437
+ ==> <Capybara:2>
1438
+ > FoobaraDemo::CapyCafe::FindCapybara.run!(id: barbara)
1439
+ ==> <Capybara:2>
1440
+ > FoobaraDemo::CapyCafe::FindCapybara.run!(id: barbara).age
1441
+ ==> 1025
1442
+ ```
1443
+
1444
+ Now let's create a command that makes use of our domain mapper which is the typical usage pattern:
1445
+
1446
+ ```ruby
1447
+ #!/usr/bin/env ruby
1448
+
1449
+ require "foobara/remote_imports"
1450
+
1451
+ [9292, 9293].each do |port|
1452
+ Foobara::RemoteImports::ImportCommand.run!(manifest_url: "http://localhost:#{port}/manifest")
1453
+ end
1454
+
1455
+ module FoobaraDemo
1456
+ module AnimalHouse
1457
+ foobara_domain!
1458
+ end
1459
+ end
1460
+
1461
+ module FoobaraDemo
1462
+ module AnimalHouse
1463
+ class Animal < Foobara::Model
1464
+ attributes do
1465
+ first_name :string
1466
+ last_name :string
1467
+ birthday :date
1468
+ species :symbol, one_of: %i[capybara cat tartigrade]
1469
+ end
1470
+ end
1471
+ end
1472
+ end
1473
+
1474
+ module FoobaraDemo
1475
+ module CapyCafe
1476
+ class ImportAnimal < Foobara::Command
1477
+ class NotACapybara < Foobara::DataError
1478
+ context species: :symbol, animal: AnimalHouse::Animal
1479
+
1480
+ def message
1481
+ "Can only import a capybara not a #{species}"
1482
+ end
1483
+ end
1484
+
1485
+ inputs animal: AnimalHouse::Animal
1486
+ result Capybara
1487
+
1488
+ possible_input_error :animal, NotACapybara
1489
+
1490
+ depends_on CreateCapybara
1491
+
1492
+ def execute
1493
+ create_capybara
1494
+
1495
+ capybara
1496
+ end
1497
+
1498
+ attr_accessor :capybara
1499
+
1500
+ def validate
1501
+ species = animal.species
1502
+
1503
+ unless species == :capybara
1504
+ add_input_error :animal, NotACapybara, animal: animal, species: species
1505
+ end
1506
+ end
1507
+
1508
+ def create_capybara
1509
+ self.capybara = run_mapped_subcommand!(CreateCapybara, animal)
1510
+ end
1511
+ end
1512
+ end
1513
+ end
1514
+ ```
1515
+
1516
+ Note that we can automatically map `animal` to CreateCapybara inputs by calling `#run_mapped_subcommand!`
1517
+
1518
+ Let's play with it:
1519
+
1520
+ ```irb
1521
+
1522
+ ```
1003
1523
  TODO
1004
1524
 
1005
1525
  ### Code Generators
1006
1526
 
1007
1527
  #### Generating a new Foobara Ruby project
1008
1528
  #### Generating a new Foobara Typescript/React project
1009
- #### Geerating commands, models, entities, types, domains, organizations, etc...
1529
+ #### Generating commands, models, entities, types, domains, organizations, etc...
1010
1530
 
1011
1531
  TODO
1012
1532
 
@@ -114,7 +114,7 @@ module Foobara
114
114
  path = DataPath.new,
115
115
  result = {}
116
116
  )
117
- if type.extends?(BuiltinTypes[:detached_entity])
117
+ if type.extends?(BuiltinTypes[:entity])
118
118
  result[path.to_s] = type
119
119
  elsif type.extends?(BuiltinTypes[:tuple])
120
120
  element_types = type.element_types
@@ -140,11 +140,15 @@ module Foobara
140
140
  end
141
141
 
142
142
  def call(from_value)
143
+ from_value = from_type.process_value!(from_value)
144
+
143
145
  mapped_value = map(from_value)
144
146
 
145
147
  to_type.process_value!(mapped_value)
146
148
  end
147
149
 
150
+ # TODO: can we make _from_value passed to #initialize instead? That way it doesn't have to be passed around
151
+ # between various helper methods
148
152
  def map(_from_value)
149
153
  # :nocov:
150
154
  raise "subclass repsonsibility"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.34
4
+ version: 0.0.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi