factory.ex (14873B)
1 # Zenflows is designed to implement the Valueflows vocabulary, 2 # written and maintained by srfsh <info@dyne.org>. 3 # Copyright (C) 2021-2023 Dyne.org foundation <foundation@dyne.org>. 4 # 5 # This program is free software: you can redistribute it and/or modify 6 # it under the terms of the GNU Affero General Public License as published by 7 # the Free Software Foundation, either version 3 of the License, or 8 # (at your option) any later version. 9 # 10 # This program is distributed in the hope that it will be useful, 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # GNU Affero General Public License for more details. 14 # 15 # You should have received a copy of the GNU Affero General Public License 16 # along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 defmodule ZenflowsTest.Help.Factory do 19 @moduledoc """ 20 Defines shortcuts for DB testing. 21 """ 22 23 alias Zenflows.DB.Repo 24 alias Zenflows.VF 25 26 defdelegate id(), to: Zenflows.DB.ID, as: :gen 27 28 @doc "Returns `DateTime.utc_now/0`." 29 @spec now() :: DateTime.t() 30 def now() do 31 DateTime.utc_now() 32 end 33 34 @doc "Like `now/0`, but piped to `DateTime.to_iso8601/1`." 35 @spec iso_now() :: String.t() 36 def iso_now() do 37 now() |> DateTime.to_iso8601() 38 end 39 40 @doc """ 41 Returns the same string with a unique positive integer attached at 42 the end. 43 """ 44 @spec str(String.t()) :: String.t() 45 def str(s) do 46 "#{s}#{System.unique_integer([:positive])}" 47 end 48 49 @doc """ 50 Returns a list of string composed of one or ten items. Each item is 51 generated by piping `str` to `uniq/1` 52 """ 53 @spec str_list(String.t(), non_neg_integer(), non_neg_integer()) :: [String.t()] 54 def str_list(s, min \\ 1, max \\ 10) do 55 max = Enum.random(1..max) 56 Enum.map(min..max, fn _ -> str(s) end) 57 end 58 59 @doc """ 60 Returns a random integer between 0 (inclusive) and `max` (exclusive). 61 """ 62 @spec int() :: integer() 63 def int(max \\ 100) do 64 ceil(float(max)) 65 end 66 67 @doc """ 68 Returns a random float between 0 (inclusive) and 1 (exclusive) 69 multiplied by `mul`, which is 100 by default. 70 """ 71 @spec float() :: float() 72 def float(mul \\ 100) do 73 :rand.uniform() * mul 74 end 75 76 @doc """ 77 Returns a string that represents a `t:Decimal.t()` between 0 78 (inclusive) and 1 (exclusive) multiplied by `mul`, which is 100 by 79 default. 80 """ 81 @spec decimal() :: String.t() 82 def decimal(mul \\ 100) do 83 to_string(decimald(mul)) 84 end 85 86 @doc """ 87 Returns a `t:Decimal.t()` between 0 (inclusive) and 1 (exclusive) 88 multiplied by `mul`, which is 100 by default. 89 """ 90 @spec decimald() :: Decimal.t() 91 def decimald(mul \\ 100) do 92 Decimal.from_float(float(mul)) 93 end 94 95 @doc "Returns a random boolean." 96 @spec bool() :: boolean() 97 def bool() do 98 :rand.uniform() < 0.5 99 end 100 101 @doc "Returns a unique URI string." 102 @spec uri() :: String.t() 103 def uri() do 104 str("schema://user@host:port/path") 105 end 106 107 @doc "Returns a file list." 108 @spec file_list(non_neg_integer(), non_neg_integer()) :: [File.t()] 109 def file_list(min \\ 1, max \\ 10) do 110 max = Enum.random(1..max) 111 Enum.map(min..max, fn _ -> 112 bin = str("some binary") 113 hash = :crypto.hash(:sha512, bin) |> Base.url_encode64(padding: false) 114 %Zenflows.File{ 115 name: str("some name"), 116 description: str("some description"), 117 mime_type: str("some mime type"), 118 extension: str("some extension"), 119 signature: str("some signature"), 120 bin: bin, 121 hash: hash, 122 size: byte_size(bin), 123 } 124 end) 125 end 126 127 @doc "Inserts a schema into the database with field overrides." 128 @spec insert!(atom(), %{required(atom()) => term()}) :: struct() 129 def insert!(_, _ \\ %{}) 130 def insert!(:economic_event, _), do: insert_economic_event!() 131 def insert!(:economic_resource, _), do: insert_economic_resource!() 132 def insert!(name, params) do 133 name |> build!(params) |> Repo.insert!() 134 end 135 136 @doc "Builds a schema with field overrides." 137 @spec build!(atom(), %{required(atom()) => term()}) :: struct() 138 def build!(name, params \\ %{}) do 139 name |> build() |> struct!(params) 140 end 141 142 @doc """ 143 Like `build!/2`, but returns just a map. 144 Useful for things like IDuration in the GraphQL spec. 145 """ 146 @spec build_map!(atom()) :: map() 147 def build_map!(name) do 148 build!(name) 149 |> Map.delete(:__struct__) 150 |> Map.delete(:__meta__) 151 end 152 153 def build(:time_unit) do 154 Enum.random(VF.TimeUnit.values()) 155 end 156 157 def build(:iduration) do 158 %{ 159 unit_type: build(:time_unit), 160 numeric_duration: decimald(), 161 } 162 end 163 164 def build(:unit) do 165 %VF.Unit{ 166 label: str("some label"), 167 symbol: str("some symbol"), 168 } 169 end 170 171 def build(:imeasure) do 172 %VF.Measure{ 173 has_unit: build(:unit), 174 has_numerical_value: decimald(), 175 } 176 end 177 178 def build(:spatial_thing) do 179 %VF.SpatialThing{ 180 name: str("some name"), 181 mappable_address: str("some mappable_address"), 182 lat: decimald(), 183 long: decimald(), 184 alt: decimald(), 185 note: str("some note"), 186 } 187 end 188 189 def build(:action_id) do 190 Enum.random(VF.Action.ID.values()) 191 end 192 193 def build(:process_specification) do 194 %VF.ProcessSpecification{ 195 name: str("some name"), 196 note: str("some note"), 197 } 198 end 199 200 def build(:resource_specification) do 201 %VF.ResourceSpecification{ 202 name: str("some name"), 203 resource_classified_as: str_list("some uri"), 204 note: str("some note"), 205 images: file_list(), 206 default_unit_of_effort: build(:unit), 207 default_unit_of_resource: build(:unit), 208 } 209 end 210 211 def build(:recipe_resource) do 212 %VF.RecipeResource{ 213 name: str("some name"), 214 unit_of_resource: build(:unit), 215 unit_of_effort: build(:unit), 216 resource_classified_as: str_list("some uri"), 217 resource_conforms_to: build(:resource_specification), 218 substitutable: bool(), 219 note: str("some note"), 220 images: file_list(), 221 } 222 end 223 224 def build(:recipe_process) do 225 dur = build(:iduration) 226 %VF.RecipeProcess{ 227 name: str("some name"), 228 note: str("some note"), 229 process_classified_as: str_list("some uri"), 230 process_conforms_to: build(:process_specification), 231 has_duration_unit_type: dur.unit_type, 232 has_duration_numeric_duration: dur.numeric_duration, 233 } 234 end 235 236 def build(:recipe_exchange) do 237 %VF.RecipeExchange{ 238 name: str("some name"), 239 note: str("some note"), 240 } 241 end 242 243 def build(:recipe_flow) do 244 resqty = build(:imeasure) 245 effqty = build(:imeasure) 246 %VF.RecipeFlow{ 247 action_id: build(:action_id), 248 recipe_input_of: build(:recipe_process), 249 recipe_output_of: build(:recipe_process), 250 recipe_flow_resource: build(:recipe_resource), 251 resource_quantity_has_unit: resqty.has_unit, 252 resource_quantity_has_numerical_value: resqty.has_numerical_value, 253 effort_quantity_has_unit: effqty.has_unit, 254 effort_quantity_has_numerical_value: effqty.has_numerical_value, 255 recipe_clause_of: build(:recipe_exchange), 256 note: str("some note"), 257 } 258 end 259 260 def build(:person) do 261 %VF.Person{ 262 type: :per, 263 name: str("some name"), 264 images: file_list(), 265 note: str("some note"), 266 primary_location: build(:spatial_thing), 267 user: str("some user"), 268 email: "#{str("user")}@example.com", 269 # Normally, these are encoded by zenroom (with whatever 270 # encodings it chooses to use), but for testing, this'll 271 # work alright. 272 ecdh_public_key: Base.encode64("some ecdh_public_key"), 273 eddsa_public_key: Base.encode64("some eddsa_public_key"), 274 ethereum_address: Base.encode64("some ethereum_address"), 275 reflow_public_key: Base.encode64("some reflow_public_key"), 276 bitcoin_public_key: Base.encode64("some bitcoin_public_key"), 277 } 278 end 279 280 def build(:organization) do 281 %VF.Organization{ 282 type: :org, 283 name: str("some name"), 284 images: file_list(), 285 classified_as: str_list("some uri"), 286 note: str("some note"), 287 primary_location: build(:spatial_thing), 288 } 289 end 290 291 def build(:agent) do 292 type = if(bool(), do: :person, else: :person) 293 struct(VF.Agent, build_map!(type)) 294 end 295 296 def build(:role_behavior) do 297 %VF.RoleBehavior{ 298 name: str("some name"), 299 note: str("some note"), 300 } 301 end 302 303 def build(:agent_relationship_role) do 304 %VF.AgentRelationshipRole{ 305 role_behavior: build(:role_behavior), 306 role_label: str("some role label"), 307 inverse_role_label: str("some role label"), 308 note: str("some note"), 309 } 310 end 311 312 def build(:agent_relationship) do 313 %VF.AgentRelationship{ 314 subject: build(:agent), 315 object: build(:agent), 316 relationship: build(:agent_relationship_role), 317 # in_scope_of: 318 note: str("some note"), 319 } 320 end 321 322 def build(:agreement) do 323 %VF.Agreement{ 324 name: str("some name"), 325 note: str("some note"), 326 } 327 end 328 329 def build(:scenario_definition) do 330 dur = build(:iduration) 331 %VF.ScenarioDefinition{ 332 name: str("some name"), 333 note: str("some note"), 334 has_duration_unit_type: dur.unit_type, 335 has_duration_numeric_duration: dur.numeric_duration, 336 } 337 end 338 339 def build(:scenario) do 340 recurse? = bool() 341 342 %VF.Scenario{ 343 name: str("some name"), 344 note: str("some note"), 345 has_beginning: now(), 346 has_end: now(), 347 defined_as: build(:scenario_definition), 348 refinement_of: if(recurse?, do: build(:scenario)), 349 } 350 end 351 352 def build(:plan) do 353 %VF.Plan{ 354 name: str("some name"), 355 due: now(), 356 note: str("some note"), 357 refinement_of: build(:scenario), 358 } 359 end 360 361 def build(:process) do 362 %VF.Process{ 363 name: str("some name"), 364 note: str("some note"), 365 has_beginning: now(), 366 has_end: now(), 367 finished: bool(), 368 classified_as: str_list("some uri"), 369 based_on: build(:process_specification), 370 # in_scope_of: 371 planned_within: build(:plan), 372 nested_in: build(:scenario), 373 } 374 end 375 376 def build(:product_batch) do 377 %VF.ProductBatch{ 378 batch_number: str("some batch number"), 379 expiry_date: now(), 380 production_date: now(), 381 } 382 end 383 384 def build(:appreciation) do 385 %VF.Appreciation{ 386 appreciation_of: build(:economic_event), 387 appreciation_with: build(:economic_event), 388 note: str("some note"), 389 } 390 end 391 392 def build(:intent) do 393 agent_mutex? = bool() 394 resqty = build(:imeasure) 395 effqty = build(:imeasure) 396 availqty = build(:imeasure) 397 398 %VF.Intent{ 399 name: str("some name"), 400 action_id: build(:action_id), 401 provider: if(agent_mutex?, do: build(:agent)), 402 receiver: unless(agent_mutex?, do: build(:agent)), 403 input_of: build(:process), 404 output_of: build(:process), 405 resource_classified_as: str_list("some uri"), 406 resource_conforms_to: build(:resource_specification), 407 resource_inventoried_as_id: insert_economic_resource!().id, 408 resource_quantity_has_unit: resqty.has_unit, 409 resource_quantity_has_numerical_value: resqty.has_numerical_value, 410 effort_quantity_has_unit: effqty.has_unit, 411 effort_quantity_has_numerical_value: effqty.has_numerical_value, 412 available_quantity_has_unit: availqty.has_unit, 413 available_quantity_has_numerical_value: availqty.has_numerical_value, 414 at_location: build(:spatial_thing), 415 has_beginning: now(), 416 has_end: now(), 417 has_point_in_time: now(), 418 due: now(), 419 finished: bool(), 420 images: file_list(), 421 note: str("some note"), 422 # in_scope_of: 423 agreed_in: str("some uri"), 424 } 425 end 426 427 def build(:commitment) do 428 datetime_mutex? = bool() 429 resource_mutex? = bool() 430 resqty = build(:imeasure) 431 effqty = build(:imeasure) 432 433 %VF.Commitment{ 434 action_id: build(:action_id), 435 provider: build(:agent), 436 receiver: build(:agent), 437 input_of: build(:process), 438 output_of: build(:process), 439 resource_classified_as: str_list("some uri"), 440 resource_conforms_to: if(resource_mutex?, do: build(:resource_specification)), 441 resource_inventoried_as_id: unless(resource_mutex?, do: insert_economic_resource!().id), 442 resource_quantity_has_unit: resqty.has_unit, 443 resource_quantity_has_numerical_value: resqty.has_numerical_value, 444 effort_quantity_has_unit: effqty.has_unit, 445 effort_quantity_has_numerical_value: effqty.has_numerical_value, 446 has_beginning: if(datetime_mutex?, do: now()), 447 has_end: if(datetime_mutex?, do: now()), 448 has_point_in_time: unless(datetime_mutex?, do: now()), 449 due: now(), 450 finished: bool(), 451 note: str("some note"), 452 # in_scope_of: 453 agreed_in: str("some uri"), 454 independent_demand_of: build(:plan), 455 at_location: build(:spatial_thing), 456 clause_of: build(:agreement), 457 } 458 end 459 460 def build(:fulfillment) do 461 resqty = build(:imeasure) 462 effqty = build(:imeasure) 463 464 %VF.Fulfillment{ 465 note: str("some note"), 466 fulfilled_by: build(:economic_event), 467 fulfills: build(:commitment), 468 resource_quantity_has_unit: resqty.has_unit, 469 resource_quantity_has_numerical_value: resqty.has_numerical_value, 470 effort_quantity_has_unit: effqty.has_unit, 471 effort_quantity_has_numerical_value: effqty.has_numerical_value, 472 } 473 end 474 475 def build(:satisfaction) do 476 resqty = build(:imeasure) 477 effqty = build(:imeasure) 478 mutex? = bool() 479 480 %VF.Satisfaction{ 481 satisfied_by_commitment: if(mutex?, do: build(:commitment)), 482 satisfied_by_event_id: unless(mutex?, do: insert_economic_event!().id), 483 satisfies: build(:intent), 484 resource_quantity_has_unit: resqty.has_unit, 485 resource_quantity_has_numerical_value: resqty.has_numerical_value, 486 effort_quantity_has_unit: effqty.has_unit, 487 effort_quantity_has_numerical_value: effqty.has_numerical_value, 488 note: str("some note"), 489 } 490 end 491 492 def build(:claim) do 493 resqty = build(:imeasure) 494 effqty = build(:imeasure) 495 496 %VF.Claim{ 497 action_id: build(:action_id), 498 provider: build(:agent), 499 receiver: build(:agent), 500 resource_classified_as: str_list("some uri"), 501 resource_conforms_to: build(:resource_specification), 502 resource_quantity_has_unit: resqty.has_unit, 503 resource_quantity_has_numerical_value: resqty.has_numerical_value, 504 effort_quantity_has_unit: effqty.has_unit, 505 effort_quantity_has_numerical_value: effqty.has_numerical_value, 506 triggered_by: if(bool(), do: build(:economic_event), else: nil), 507 due: now(), 508 finished: bool(), 509 agreed_in: str("some uri"), 510 note: str("some note"), 511 # in_scope_of: 512 } 513 end 514 515 def build(:settlement) do 516 resqty = build(:imeasure) 517 effqty = build(:imeasure) 518 519 %VF.Settlement{ 520 settled_by: build(:economic_event), 521 settles: build(:claim), 522 resource_quantity_has_unit: resqty.has_unit, 523 resource_quantity_has_numerical_value: resqty.has_numerical_value, 524 effort_quantity_has_unit: effqty.has_unit, 525 effort_quantity_has_numerical_value: effqty.has_numerical_value, 526 note: str("some note"), 527 } 528 end 529 530 def build(:proposal) do 531 %VF.Proposal{ 532 name: str("some name"), 533 has_beginning: now(), 534 has_end: now(), 535 unit_based: bool(), 536 note: str("some note"), 537 eligible_location: build(:spatial_thing), 538 } 539 end 540 541 def build(:proposed_intent) do 542 %VF.ProposedIntent{ 543 reciprocal: bool(), 544 publishes: build(:intent), 545 published_in: build(:proposal), 546 } 547 end 548 549 def build(:proposed_to) do 550 %VF.ProposedTo{ 551 proposed_to: build(:agent), 552 proposed: build(:proposal), 553 } 554 end 555 556 def insert_economic_event!() do 557 agent = insert!(:agent) 558 Zenflows.VF.EconomicEvent.Domain.create!(%{ 559 action_id: "raise", 560 provider_id: agent.id, 561 receiver_id: agent.id, 562 resource_classified_as: str_list("some uri"), 563 resource_conforms_to_id: insert!(:resource_specification).id, 564 resource_quantity: %{ 565 has_numerical_value: decimald(), 566 has_unit_id: insert!(:unit).id, 567 }, 568 has_point_in_time: now(), 569 }, %{name: str("some name")}) 570 end 571 572 def insert_economic_resource!() do 573 %{resource_inventoried_as_id: id} = insert_economic_event!() 574 Zenflows.VF.EconomicResource.Domain.one!(id) 575 end 576 end