foobara 0.0.34 → 0.0.35

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: bc9733f9966487e9f526b01792182ff02718e7f3ac445256d24397942eddf4ec
4
- data.tar.gz: 4e611e1bfae4bc3418108ad93680555e6497d1426a465697827064523ed940d5
3
+ metadata.gz: f2cf6302b2afba48242f5cc283a4029ebb6337cccc367a633752cd9866c69d69
4
+ data.tar.gz: 05b4068afedd82c0712e977d9810ebc1bcacb9c06ab90205e1985966f5ece820
5
5
  SHA512:
6
- metadata.gz: a57062ce5885cc626ac32c66dcf9902c1ace2d50d2182c0ec188ba01e3bc662f9e0d6e2fe4d8b6626a23b25f1046e71da813d0b35e8b3d37c895f074d901feb7
7
- data.tar.gz: 7d6724fff8dc1ee6961f6585ab9c5b78560962725511cd3d316451dda387d8455ddc0a9808c8617aabed61d4aeb656698a1b7a92ef2b760a7248c96bc6b84f81
6
+ metadata.gz: 48722536d6aee98bebaad5934153c5d46223857343292054f6f053c80a7784d187f853514bb3271831c6158056490326731501d2a890d51a386c6626cbd76932
7
+ data.tar.gz: 1371ff6d6b3a81b4cf7228f6cd15a70265955da87dca6bcc580a938217f2443d3add0f8aaad85e705cca00cb101cb8587723360ada4f8a9b765895400d4b44e6
data/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
- ## [0.0.34] - 2024-12-10
1
+ ## [0.0.35] - 2024-12-10
2
2
 
3
3
  - Fix bug with command-named convenience functions
4
+ - Make domain mappers cast their inputs
4
5
 
5
6
  ## [0.0.33] - 2024-12-09
6
7
 
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,495 @@ 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
+
1423
+ ```
1424
+
1425
+ ```ruby
1426
+ class ImportAnimal < Foobara::Command
1427
+ class NotACapybara < Foobara::DataError
1428
+ context species: :symbol, animal: AnimalHouse::Animal
1429
+
1430
+ def message
1431
+ "Can only import a capybara not a #{species}"
1432
+ end
1433
+ end
1434
+
1435
+ inputs animal: AnimalHouse::Animal
1436
+ result Capybara
1437
+
1438
+ possible_input_error :animal, NotACapybara
1439
+
1440
+ depends_on CreateCapybara
1441
+
1442
+ def execute
1443
+ create_capybara
1444
+
1445
+ capybara
1446
+ end
1447
+
1448
+ attr_accessor :capybara
1449
+
1450
+ def validate
1451
+ species = animal.species
1452
+
1453
+ unless species == :capybara
1454
+ add_input_error :animal, NotACapybara, animal: animal, species: species
1455
+ end
1456
+ end
1457
+
1458
+ def create_capybara
1459
+ self.capybara = run_mapped_subcommand!(CreateCapybara, animal)
1460
+ end
1461
+ end
1462
+ ```
1463
+
1003
1464
  TODO
1004
1465
 
1005
1466
  ### Code Generators
1006
1467
 
1007
1468
  #### Generating a new Foobara Ruby project
1008
1469
  #### Generating a new Foobara Typescript/React project
1009
- #### Geerating commands, models, entities, types, domains, organizations, etc...
1470
+ #### Generating commands, models, entities, types, domains, organizations, etc...
1010
1471
 
1011
1472
  TODO
1012
1473
 
@@ -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.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi