hecks 2.5.1 → 2.7.0
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 +4 -4
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/bench/cli.rb +74 -0
- data/lib/hecks/bench/environment.rb +100 -0
- data/lib/hecks/bench/postgres_probe.rb +41 -0
- data/lib/hecks/bench/report.rb +116 -0
- data/lib/hecks/bench/ruby_runner.rb +151 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +165 -0
- data/lib/hecks/bench/stats.rb +75 -0
- data/lib/hecks/bench/suite.rb +172 -0
- data/lib/hecks/bench/workload.rb +137 -0
- data/lib/hecks/bench.rb +28 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +153 -0
- data/lib/hecks/bluebook/model_check.rb +21 -2
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +136 -69
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_stdio_guard.rb +160 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +187 -138
- data/lib/hecks/projections/deploy/lambda.rb +5 -5
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/loader.rb +26 -2
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +4 -2
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +14 -9
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +3 -1
- metadata +59 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require_relative "vendor"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module EmbryonautBluebook
|
|
6
|
+
# The command line behind `bin/vendor_bluebook`.
|
|
7
|
+
#
|
|
8
|
+
# It lives in the gem, not in a script, so a consuming project reaches it
|
|
9
|
+
# through its own bundle without a copy of the script:
|
|
10
|
+
#
|
|
11
|
+
# bundle exec ruby -rhecks \
|
|
12
|
+
# -e 'exit Hecks::EmbryonautBluebook::VendorCli.run(ARGV)' payments@1.2.0
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# vendor_bluebook <package>[@<version-or-commit>] [--from <path>] [--root <path>]
|
|
17
|
+
#
|
|
18
|
+
# `--from` defaults to `$EMBRYONAUT_BLUEBOOKS_SRC` and `--root` to the current
|
|
19
|
+
# directory. `ALLOW_DOWNGRADE=1` in the environment permits a release older
|
|
20
|
+
# than the vendored one.
|
|
21
|
+
module VendorCli
|
|
22
|
+
USAGE = "usage: vendor_bluebook <package>[@<version-or-commit>] [--from PATH] [--root PATH]".freeze
|
|
23
|
+
|
|
24
|
+
# Runs the command and reports on the given streams.
|
|
25
|
+
#
|
|
26
|
+
# @param argv [Array<String>] the command-line arguments
|
|
27
|
+
# @param env [Hash{String => String}] the environment
|
|
28
|
+
# @param out [IO] where the report goes
|
|
29
|
+
# @param err [IO] where a refusal goes
|
|
30
|
+
# @param root [String] the default project root
|
|
31
|
+
# @return [Integer] the exit status, 0 when the package was vendored
|
|
32
|
+
def self.run(argv, env: ENV, out: $stdout, err: $stderr, root: Dir.pwd)
|
|
33
|
+
options = { from: env["EMBRYONAUT_BLUEBOOKS_SRC"], root: root }
|
|
34
|
+
spec = parse(argv, options)
|
|
35
|
+
return usage(err) unless spec && options[:from]
|
|
36
|
+
|
|
37
|
+
name, _, ref = spec.partition("@")
|
|
38
|
+
result = Vendor.new(name, from: options[:from], root: options[:root], ref: ref.empty? ? nil : ref,
|
|
39
|
+
allow_downgrade: env["ALLOW_DOWNGRADE"] == "1").call
|
|
40
|
+
report(result, options[:from], out)
|
|
41
|
+
0
|
|
42
|
+
rescue Vendoring::Error, OptionParser::ParseError => e
|
|
43
|
+
err.puts(e.message)
|
|
44
|
+
1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Reads the options into a hash and picks out the package argument.
|
|
48
|
+
#
|
|
49
|
+
# @param argv [Array<String>] the command-line arguments
|
|
50
|
+
# @param options [Hash{Symbol => String}] receives `:from` and `:root` when given
|
|
51
|
+
# @return [String, nil] the one positional argument, nil unless exactly one was given
|
|
52
|
+
def self.parse(argv, options)
|
|
53
|
+
parser = OptionParser.new
|
|
54
|
+
parser.on("--from PATH") { |path| options[:from] = path }
|
|
55
|
+
parser.on("--root PATH") { |path| options[:root] = path }
|
|
56
|
+
rest = parser.parse(argv)
|
|
57
|
+
rest.first if rest.length == 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.usage(err)
|
|
61
|
+
err.puts(USAGE)
|
|
62
|
+
2
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.report(result, from, out)
|
|
66
|
+
label = result.release? ? "#{result.version} (#{result.tag}, #{result.commit[0, 7]})" : result.commit[0, 12]
|
|
67
|
+
out.puts("Vendored embryonaut_bluebooks/#{result.package} #{label} from #{from} into #{result.dir}")
|
|
68
|
+
out.puts(shape_line(result))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The shape label tells a re-vendor that only moved the pin apart from one
|
|
72
|
+
# that will mint a new era on the next deploy.
|
|
73
|
+
def self.shape_line(result)
|
|
74
|
+
now = result.shape.join(" ")
|
|
75
|
+
if result.previous_shape.nil?
|
|
76
|
+
"Shape: #{now} (no earlier vendored copy to compare against)"
|
|
77
|
+
elsif result.shape_changed?
|
|
78
|
+
"SHAPE CHANGED: the next deploy mints a new era; add its translation edge first.\n " \
|
|
79
|
+
"before: #{result.previous_shape.join(' ')}\n after: #{now}"
|
|
80
|
+
else
|
|
81
|
+
"Shape unchanged: #{now}. No new era on deploy."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private_class_method :parse, :usage, :report, :shape_line
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -13,10 +13,10 @@ module Hecks
|
|
|
13
13
|
#
|
|
14
14
|
# This module and its `uses_embryonaut_bluebook`
|
|
15
15
|
# DSL word (hecksagon_builder.rb) were built on a prior commit of this
|
|
16
|
-
# repo (933d1dd), vendored out to a real consumer (
|
|
16
|
+
# repo (933d1dd), vendored out to a real consumer (a client site's domain,
|
|
17
17
|
# for embryonaut_bluebooks/payments), and then lost from this repo's own
|
|
18
18
|
# reachable history — a hard reset or rebase left no branch containing
|
|
19
|
-
# that commit. The
|
|
19
|
+
# that commit. The consumer's vendor snapshot (a `git archive` of that
|
|
20
20
|
# commit, committed into their repo) was the only surviving copy; this
|
|
21
21
|
# file is ported forward from it, checked against current `main`'s own
|
|
22
22
|
# conventions rather than copied wholesale, since the two trees had
|
|
@@ -64,7 +64,18 @@ module Hecks
|
|
|
64
64
|
# => "Payments"), not a separate ledger. A vendored package's directory
|
|
65
65
|
# name and its declared `Hecks.bluebook` name are the one convention
|
|
66
66
|
# this reuses from Framework rather than reinventing.
|
|
67
|
+
#
|
|
68
|
+
# ## Vendoring
|
|
69
|
+
#
|
|
70
|
+
# `vendor!` is the other direction: it pins a package from a local checkout
|
|
71
|
+
# of the registry into a consuming project's own `vendor/` tree, and is the
|
|
72
|
+
# one implementation behind `bin/vendor_bluebook` (see Vendoring and Vendor).
|
|
67
73
|
module EmbryonautBluebook
|
|
74
|
+
autoload :Lock, File.join(__dir__, "embryonaut_bluebook/lock")
|
|
75
|
+
autoload :Shape, File.join(__dir__, "embryonaut_bluebook/shape")
|
|
76
|
+
autoload :Vendor, File.join(__dir__, "embryonaut_bluebook/vendor")
|
|
77
|
+
autoload :VendorCli, File.join(__dir__, "embryonaut_bluebook/vendor_cli")
|
|
78
|
+
|
|
68
79
|
# Loads a vendored embryonaut bluebook package's `.bluebook` files, once
|
|
69
80
|
# per registry.
|
|
70
81
|
#
|
|
@@ -89,10 +100,25 @@ module Hecks
|
|
|
89
100
|
if files.empty?
|
|
90
101
|
raise Runtime::WiringError,
|
|
91
102
|
"no vendored embryonaut bluebook named #{name.inspect} at #{dir} — " \
|
|
92
|
-
"
|
|
103
|
+
"vendor it with bin/vendor_bluebook #{name}"
|
|
93
104
|
end
|
|
94
105
|
|
|
95
106
|
files.each { |file| Kernel.load(file) }
|
|
96
107
|
end
|
|
108
|
+
|
|
109
|
+
# Pins one package of the registry into a project's `vendor/` tree.
|
|
110
|
+
#
|
|
111
|
+
# @param name [String, Symbol] the package's directory name, such as `"payments"`
|
|
112
|
+
# @param from [String] path of a local checkout of the registry repository
|
|
113
|
+
# @param ref [String, nil] a release version (`"1.2.0"`), a release tag name, any
|
|
114
|
+
# commit-ish, or nil for the newest release
|
|
115
|
+
# @param root [String] the consuming project's root
|
|
116
|
+
# @param allow_downgrade [Boolean] whether a release older than the vendored one is allowed
|
|
117
|
+
# @return [Vendor::Result] the commit, version and storage shape now vendored
|
|
118
|
+
# @raise [Vendoring::Error] if the source, release or files are unusable, or a
|
|
119
|
+
# version-policy refusal applies (see Vendor)
|
|
120
|
+
def self.vendor!(name, from:, root:, ref: nil, allow_downgrade: false)
|
|
121
|
+
Vendor.new(name, from: from, root: root, ref: ref, allow_downgrade: allow_downgrade).call
|
|
122
|
+
end
|
|
97
123
|
end
|
|
98
124
|
end
|
|
@@ -2,7 +2,7 @@ module Hecks
|
|
|
2
2
|
module Forms
|
|
3
3
|
# The value-object classification every consumer of a resolved VO
|
|
4
4
|
# shape needs, spelled once — `field_shape.rb` (a command's own
|
|
5
|
-
# form), `ui_schema.rb` (
|
|
5
|
+
# form), `ui_schema.rb` (the console app's table/detail-panel
|
|
6
6
|
# renderer, a separate app, not this repo), and
|
|
7
7
|
# `adapters/driven/sql_query_builder.rb` (a query's own ORDER BY/
|
|
8
8
|
# where compiler) each grew their own copy of "is this VO money-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Hecks.bluebook "ConsoleSettings" do
|
|
2
|
-
vision "A generic admin console's own curated UI facts — nav, columns, state tones, which rows a detail panel shows — as real dispatched data instead of a hand-rewritten YAML file. A UI opinion the DOMAIN it's pointed at has no way to answer, never a business record; see
|
|
2
|
+
vision "A generic admin console's own curated UI facts — nav, columns, state tones, which rows a detail panel shows — as real dispatched data instead of a hand-rewritten YAML file. A UI opinion the DOMAIN it's pointed at has no way to answer, never a business record; see the console app's own presentation_config.rb for the fuller story."
|
|
3
3
|
supporting
|
|
4
4
|
|
|
5
5
|
# A FRAMEWORK MEMBER LIKE IDENTITY/GOVERNANCE — attach with
|
|
@@ -7,7 +7,7 @@ Hecks.bluebook "ConsoleSettings" do
|
|
|
7
7
|
# `.hecksagon`, plus a `Hecks.hecksagon "ConsoleSettings" do ... end`
|
|
8
8
|
# persistence binding (Heki is the common case — a small, local,
|
|
9
9
|
# per-app settings store, no shared database needed). This used to
|
|
10
|
-
# boot in a SECOND, independent registry (
|
|
10
|
+
# boot in a SECOND, independent registry (the console app's own
|
|
11
11
|
# console_settings_domain.rb) specifically so no consuming app needed
|
|
12
12
|
# any change at all — that property is real and now given up on
|
|
13
13
|
# purpose, in favor of the one binding mechanism every other
|
|
@@ -30,7 +30,7 @@ Hecks.bluebook "ConsoleSettings" do
|
|
|
30
30
|
# (Overview, below) and every `extra_json`/`identity_extra_json`
|
|
31
31
|
# field throughout this chapter were BOTH originally scoped out on an
|
|
32
32
|
# "confirmed unused in any real presentation.yml" basis that turned
|
|
33
|
-
# out to be checked against the wrong file —
|
|
33
|
+
# out to be checked against the wrong file — the console app's own
|
|
34
34
|
# fixture, never this app's actual, live one. Migrating the real file
|
|
35
35
|
# found both very much in use.
|
|
36
36
|
|
|
@@ -82,7 +82,7 @@ module Hecks
|
|
|
82
82
|
return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
settings = registry.
|
|
85
|
+
settings = registry.binding_settings(bluebook.name, Hecks::Ports::Persistence::VERB, adapter_name)
|
|
86
86
|
db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
|
|
87
87
|
begin
|
|
88
88
|
lineage = Hecks::Adapters::PostgresEra::Lineage.new(db, bluebook.name)
|
|
@@ -93,13 +93,17 @@ module Hecks
|
|
|
93
93
|
# `adapter: :postgres_era`; ignored by every other adapter
|
|
94
94
|
# @param schema [String, nil] the schema name, required only for
|
|
95
95
|
# `adapter: :postgres_era`; ignored by every other adapter
|
|
96
|
+
# @param scratch [Hash{Symbol => String}] `:database` and/or `:schema` for
|
|
97
|
+
# `adapter: :postgres` to use instead of the shared fuzz scratch schema, so a caller
|
|
98
|
+
# such as `bin/bench` never drops the schema a concurrent `bin/fuzz` is using;
|
|
99
|
+
# ignored by every other adapter
|
|
96
100
|
# @yield [String] the freshly rebound copy's root directory, ready to boot
|
|
97
101
|
# @yieldreturn [Object] anything; becomes this method's own return value
|
|
98
102
|
# @return [Object] whatever the given block returns
|
|
99
103
|
# @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
|
|
100
104
|
# `:postgres_era`, or if `adapter: :postgres_era` is given without both
|
|
101
105
|
# `database:` and `schema:`
|
|
102
|
-
def call(domain_path, adapter: :memory, database: nil, schema: nil)
|
|
106
|
+
def call(domain_path, adapter: :memory, database: nil, schema: nil, scratch: {})
|
|
103
107
|
Dir.mktmpdir("hecks-fuzz") do |tmp|
|
|
104
108
|
copy = File.join(tmp, File.basename(domain_path))
|
|
105
109
|
copy_dereferencing(domain_path, copy)
|
|
@@ -107,7 +111,7 @@ module Hecks
|
|
|
107
111
|
case adapter
|
|
108
112
|
when :memory then rebind_to_memory!(copy)
|
|
109
113
|
when :sqlite then rebind_to_sqlite!(copy)
|
|
110
|
-
when :postgres then rebind_to_postgres!(copy)
|
|
114
|
+
when :postgres then rebind_to_postgres!(copy, **scratch)
|
|
111
115
|
when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
|
|
112
116
|
else raise ArgumentError,
|
|
113
117
|
"unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
|
|
@@ -196,8 +200,8 @@ module Hecks
|
|
|
196
200
|
end
|
|
197
201
|
end
|
|
198
202
|
|
|
199
|
-
# Rewrites every `.hecksagon` in the copy to bind through Memory and
|
|
200
|
-
# `.world` files, so the boot needs no settings
|
|
203
|
+
# Rewrites every `.hecksagon` in the copy to bind through Memory and replaces its
|
|
204
|
+
# `.world` files with a `default_adapter "Memory"` one, so the boot needs no settings.
|
|
201
205
|
#
|
|
202
206
|
# @param copy [String] the isolated copy's root directory
|
|
203
207
|
# @return [void]
|
|
@@ -215,8 +219,11 @@ module Hecks
|
|
|
215
219
|
# `data/<table>.db` under `root:` when unset — the same
|
|
216
220
|
# zero-config default Memory gets from having no `.world` at
|
|
217
221
|
# all), so the simplest correct fix for either target is
|
|
218
|
-
# dropping `.world` from the copy
|
|
219
|
-
|
|
222
|
+
# dropping the shipped `.world` from the copy. What replaces it is
|
|
223
|
+
# only a `default_adapter`, so an aggregate the hecksagon leaves
|
|
224
|
+
# unbound (the real world's `default_adapter` bound it) still lands on
|
|
225
|
+
# this mode's adapter — see `write_default_worlds!`.
|
|
226
|
+
write_default_worlds!(copy, "Memory")
|
|
220
227
|
end
|
|
221
228
|
|
|
222
229
|
# Same dance as memory, one adapter over — `Adapters::Sqlite#
|
|
@@ -233,7 +240,7 @@ module Hecks
|
|
|
233
240
|
def rebind_to_sqlite!(copy)
|
|
234
241
|
rewrite_bindings!(copy, "SqlitePersistence")
|
|
235
242
|
strip_translations!(copy)
|
|
236
|
-
|
|
243
|
+
write_default_worlds!(copy, "SqlitePersistence")
|
|
237
244
|
end
|
|
238
245
|
|
|
239
246
|
# **The expensive one** — Postgres has no zero-config default the way
|
|
@@ -262,15 +269,20 @@ module Hecks
|
|
|
262
269
|
FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
|
|
263
270
|
|
|
264
271
|
# Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
|
|
265
|
-
# shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema
|
|
272
|
+
# shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema unless the
|
|
273
|
+
# caller names its own, which lets `bin/bench` run without touching the fuzzer's.
|
|
266
274
|
#
|
|
267
275
|
# @param copy [String] the isolated copy's root directory
|
|
276
|
+
# @param database [String, nil] the scratch database, `FUZZ_POSTGRES_DATABASE` when nil
|
|
277
|
+
# @param schema [String, nil] the scratch schema, `FUZZ_POSTGRES_SCHEMA` when nil
|
|
268
278
|
# @return [void]
|
|
269
|
-
def rebind_to_postgres!(copy)
|
|
279
|
+
def rebind_to_postgres!(copy, database: nil, schema: nil)
|
|
270
280
|
require "pg"
|
|
281
|
+
database ||= FUZZ_POSTGRES_DATABASE
|
|
282
|
+
schema ||= FUZZ_POSTGRES_SCHEMA
|
|
271
283
|
rewrite_bindings!(copy, "Postgres")
|
|
272
284
|
strip_translations!(copy)
|
|
273
|
-
ensure_fuzz_schema!
|
|
285
|
+
ensure_fuzz_schema!(database, schema)
|
|
274
286
|
|
|
275
287
|
# One `.world` per directory a `.hecksagon` actually lives in, not
|
|
276
288
|
# one at `copy`'s own root — `Folder#load_domain` resolves a
|
|
@@ -278,35 +290,20 @@ module Hecks
|
|
|
278
290
|
# recursively (`Folder#load_each`); a domain can hold several
|
|
279
291
|
# `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
|
|
280
292
|
# file (banking.hecksagon declares "Banking", "Governance", and
|
|
281
|
-
# "Identity" together), so every name found in one
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
<<~WORLD
|
|
291
|
-
Hecks.world "#{name}" do
|
|
292
|
-
persisted_by("Postgres") do
|
|
293
|
-
database "#{FUZZ_POSTGRES_DATABASE}"
|
|
294
|
-
schema "#{FUZZ_POSTGRES_SCHEMA}"
|
|
295
|
-
end
|
|
293
|
+
# "Identity" together), so every name found in one directory
|
|
294
|
+
# is bundled into one `.world` written there, not scattered by name.
|
|
295
|
+
write_worlds!(copy, "hecks_fuzz_postgres.world") do |name|
|
|
296
|
+
<<~WORLD
|
|
297
|
+
Hecks.world "#{name}" do
|
|
298
|
+
default_adapter "Postgres"
|
|
299
|
+
persisted_by("Postgres") do
|
|
300
|
+
database "#{database}"
|
|
301
|
+
schema "#{schema}"
|
|
296
302
|
end
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
# Any pre-existing `.world` this copy shipped with (a real
|
|
302
|
-
# deployment's own connection string) is now redundant with — and
|
|
303
|
-
# would conflict with, `Registry#add_world`'s own header on
|
|
304
|
-
# loading the same domain name twice — the fresh one just
|
|
305
|
-
# written above, so it goes the same way `rebind_to_memory!`
|
|
306
|
-
# already sends every `.world` in a Memory-mode boot.
|
|
307
|
-
Dir.glob(File.join(copy, "**", "*.world")).each do |path|
|
|
308
|
-
File.delete(path) unless File.basename(path) == "hecks_fuzz_postgres.world"
|
|
303
|
+
end
|
|
304
|
+
WORLD
|
|
309
305
|
end
|
|
306
|
+
write_unbound_chapter_worlds!(copy)
|
|
310
307
|
end
|
|
311
308
|
|
|
312
309
|
# Admin connection lives outside the tmp copy entirely — same as
|
|
@@ -318,8 +315,10 @@ module Hecks
|
|
|
318
315
|
# recreated on every call, which is what actually isolates one
|
|
319
316
|
# ephemeral boot's data from the next.
|
|
320
317
|
#
|
|
318
|
+
# @param database [String] the scratch database to create if missing
|
|
319
|
+
# @param schema [String] the schema to drop and recreate inside it
|
|
321
320
|
# @return [void]
|
|
322
|
-
def ensure_fuzz_schema!
|
|
321
|
+
def ensure_fuzz_schema!(database = FUZZ_POSTGRES_DATABASE, schema = FUZZ_POSTGRES_SCHEMA)
|
|
323
322
|
# `Adapters::Postgres#initialize` opens one real `PG::Connection`
|
|
324
323
|
# per aggregate and never explicitly closes it — fine for a
|
|
325
324
|
# process that boots once and runs, exactly what every other
|
|
@@ -342,17 +341,18 @@ module Hecks
|
|
|
342
341
|
# something to route around by connecting less carefully.
|
|
343
342
|
GC.start
|
|
344
343
|
|
|
345
|
-
|
|
344
|
+
@fuzz_databases_ready ||= {}
|
|
345
|
+
unless @fuzz_databases_ready[database]
|
|
346
346
|
admin = PG.connect(dbname: "postgres")
|
|
347
347
|
exists = admin.exec_params(
|
|
348
|
-
"SELECT 1 FROM pg_database WHERE datname = $1", [
|
|
348
|
+
"SELECT 1 FROM pg_database WHERE datname = $1", [database]
|
|
349
349
|
).ntuples.positive?
|
|
350
|
-
admin.exec(%(CREATE DATABASE "#{
|
|
350
|
+
admin.exec(%(CREATE DATABASE "#{database}")) unless exists
|
|
351
351
|
admin.close
|
|
352
|
-
@
|
|
352
|
+
@fuzz_databases_ready[database] = true
|
|
353
353
|
end
|
|
354
354
|
|
|
355
|
-
db = PG.connect(dbname:
|
|
355
|
+
db = PG.connect(dbname: database)
|
|
356
356
|
# **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
|
|
357
357
|
# own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
|
|
358
358
|
# actually has something to drop (every boot after the first)
|
|
@@ -361,7 +361,7 @@ module Hecks
|
|
|
361
361
|
# output under a wall of "drop cascades to table ..." on every
|
|
362
362
|
# single ephemeral boot.
|
|
363
363
|
db.exec("SET client_min_messages = warning")
|
|
364
|
-
quoted = db.quote_ident(
|
|
364
|
+
quoted = db.quote_ident(schema)
|
|
365
365
|
db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
|
|
366
366
|
db.exec("CREATE SCHEMA #{quoted}")
|
|
367
367
|
db.close
|
|
@@ -416,9 +416,9 @@ module Hecks
|
|
|
416
416
|
ensure_postgres_era_schema!(database: database, schema: schema)
|
|
417
417
|
|
|
418
418
|
# Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
|
|
419
|
-
# postgres!`
|
|
420
|
-
# load_domain` globs `*.world` non-recursively) — see
|
|
421
|
-
#
|
|
419
|
+
# postgres!` uses, for the identical reason (`Folder#
|
|
420
|
+
# load_domain` globs `*.world` non-recursively) — see
|
|
421
|
+
# `write_worlds!`. `PostgresEra`
|
|
422
422
|
# additionally takes `schema:` (postgres_era.rb's own "shared-
|
|
423
423
|
# instance isolation" comment): the caller-supplied throwaway
|
|
424
424
|
# schema is what actually isolates this one ephemeral boot from
|
|
@@ -444,36 +444,103 @@ module Hecks
|
|
|
444
444
|
# database it is about to throw away; the one-line warning
|
|
445
445
|
# PostgresEra prints per boot under the opt-in is the honest
|
|
446
446
|
# price. Inert on a machine whose ambient user is ordinary.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
447
|
+
write_worlds!(copy, "hecks_fuzz_postgres_era.world") do |name|
|
|
448
|
+
<<~WORLD
|
|
449
|
+
Hecks.world "#{name}" do
|
|
450
|
+
default_adapter "PostgresEra"
|
|
451
|
+
persisted_by("PostgresEra") do
|
|
452
|
+
database "#{database}"
|
|
453
|
+
schema "#{schema}"
|
|
454
|
+
allow_superuser true
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
WORLD
|
|
458
|
+
end
|
|
459
|
+
write_unbound_chapter_worlds!(copy)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Replaces every `.world` in the copy with one that declares only `default_adapter`,
|
|
463
|
+
# so the copy needs no settings and an aggregate its hecksagon leaves unbound
|
|
464
|
+
# (a chapter whose real world bound it through `default_adapter`) persists through
|
|
465
|
+
# `adapter_name` like every bound one.
|
|
466
|
+
#
|
|
467
|
+
# @param copy [String] the isolated copy's root directory
|
|
468
|
+
# @param adapter_name [String] the persistence adapter every unbound aggregate falls
|
|
469
|
+
# back to, such as `"Memory"` or `"SqlitePersistence"`
|
|
470
|
+
# @return [void]
|
|
471
|
+
def write_default_worlds!(copy, adapter_name)
|
|
472
|
+
write_worlds!(copy, "hecks_fuzz_default.world") do |name|
|
|
473
|
+
<<~WORLD
|
|
474
|
+
Hecks.world "#{name}" do
|
|
475
|
+
default_adapter "#{adapter_name}"
|
|
476
|
+
end
|
|
477
|
+
WORLD
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Gives every chapter no hecksagon names a world of its own that falls back to Memory.
|
|
482
|
+
#
|
|
483
|
+
# A chapter with a bluebook and no hecksagon block (the framework's Privacy) has no
|
|
484
|
+
# bind, and the worlds `write_worlds!` wrote declare a `default_adapter` that needs a
|
|
485
|
+
# `database`: left alone, the chapter resolves that adapter through the project's
|
|
486
|
+
# world with no connection and the boot refuses (`WiringError ... needs a database
|
|
487
|
+
# connection`). It persisted through Memory before those worlds carried a
|
|
488
|
+
# `default_adapter`, and a chapter's own world's default wins over the project's, so
|
|
489
|
+
# this keeps it there.
|
|
490
|
+
#
|
|
491
|
+
# @param copy [String] the isolated copy's root directory
|
|
492
|
+
# @return [void]
|
|
493
|
+
def write_unbound_chapter_worlds!(copy)
|
|
494
|
+
named = Dir.glob(File.join(copy, "**", "*.hecksagon")).flat_map do |path|
|
|
495
|
+
File.read(path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
496
|
+
end
|
|
497
|
+
bluebooks = Dir.glob(File.join(copy, "**", "*.bluebook")).group_by { |path| File.dirname(path) }
|
|
498
|
+
bluebooks.each do |dir, files|
|
|
499
|
+
unbound = files.flat_map { |path| File.read(path).scan(/Hecks\.bluebook[\s(]+"([^"]+)"/).flatten }
|
|
500
|
+
.uniq - named
|
|
501
|
+
named.concat(unbound)
|
|
502
|
+
next if unbound.empty?
|
|
503
|
+
|
|
504
|
+
worlds = unbound.map { |name| %(Hecks.world "#{name}" do\n default_adapter "Memory"\nend\n) }
|
|
505
|
+
File.write(File.join(dir, "hecks_fuzz_unbound.world"), worlds.join("\n"))
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Writes one `.world` file per directory that holds a `.hecksagon`, naming every
|
|
510
|
+
# `Hecks.hecksagon` block found in that directory, and deletes every other `.world`
|
|
511
|
+
# in the copy.
|
|
512
|
+
#
|
|
513
|
+
# One world file per directory, every hecksagon name in that directory — never one
|
|
514
|
+
# write per `*.hecksagon` file. A domain's `context_map.hecksagon` sits beside its
|
|
515
|
+
# main hecksagon (the `qa` domain declares Governance in one and QualityControl in
|
|
516
|
+
# the other), and a second `File.write` to the same world path would drop the first
|
|
517
|
+
# file's names, leaving that domain bound to Postgres with no `database` to open.
|
|
518
|
+
#
|
|
519
|
+
# Any `.world` the copy shipped with (a real deployment's own connection string) is
|
|
520
|
+
# redundant with, and would conflict with (`Registry#add_world`'s own header, on
|
|
521
|
+
# loading the same domain name twice), the fresh one written here, so it goes the same
|
|
522
|
+
# way `rebind_to_memory!` sends every `.world` in a Memory-mode boot.
|
|
523
|
+
#
|
|
524
|
+
# @param copy [String] the isolated copy's root directory
|
|
525
|
+
# @param world_file [String] the basename of the world file written in each directory
|
|
526
|
+
# @yield [name] builds the world text for one hecksagon name
|
|
527
|
+
# @yieldparam name [String] a domain name a `Hecks.hecksagon` block in the directory declares
|
|
528
|
+
# @yieldreturn [String] the `Hecks.world` block for that name
|
|
529
|
+
# @return [void]
|
|
530
|
+
def write_worlds!(copy, world_file, &world_for)
|
|
531
|
+
names_by_dir = Hash.new { |hash, dir| hash[dir] = [] }
|
|
453
532
|
Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
|
|
454
533
|
names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
|
|
455
|
-
|
|
534
|
+
names_by_dir[File.dirname(hecksagon_path)].concat(names)
|
|
456
535
|
end
|
|
457
|
-
|
|
458
|
-
names = names.uniq
|
|
536
|
+
names_by_dir.each do |dir, names|
|
|
459
537
|
next if names.empty?
|
|
460
538
|
|
|
461
|
-
|
|
462
|
-
File.write(world_path, names.map do |name|
|
|
463
|
-
<<~WORLD
|
|
464
|
-
Hecks.world "#{name}" do
|
|
465
|
-
persisted_by("PostgresEra") do
|
|
466
|
-
database "#{database}"
|
|
467
|
-
schema "#{schema}"
|
|
468
|
-
allow_superuser true
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
WORLD
|
|
472
|
-
end.join("\n"))
|
|
539
|
+
File.write(File.join(dir, world_file), names.uniq.map(&world_for).join("\n"))
|
|
473
540
|
end
|
|
474
541
|
|
|
475
542
|
Dir.glob(File.join(copy, "**", "*.world")).each do |path|
|
|
476
|
-
File.delete(path) unless File.basename(path) ==
|
|
543
|
+
File.delete(path) unless File.basename(path) == world_file
|
|
477
544
|
end
|
|
478
545
|
end
|
|
479
546
|
|
|
@@ -4,8 +4,10 @@ Hecks.bluebook "World" do
|
|
|
4
4
|
|
|
5
5
|
identified_by WorldName, as: :domain
|
|
6
6
|
|
|
7
|
-
attribute :realm,
|
|
8
|
-
attribute :latest,
|
|
7
|
+
attribute :realm, Realm
|
|
8
|
+
attribute :latest, Latest
|
|
9
|
+
attribute :default_database, DefaultDatabase
|
|
10
|
+
attribute :default_adapter, DefaultAdapter
|
|
9
11
|
|
|
10
12
|
value_object "WorldName" do
|
|
11
13
|
attribute :value, String
|
|
@@ -22,6 +24,22 @@ Hecks.bluebook "World" do
|
|
|
22
24
|
invariant("a latest version says something") { !value.to_s.empty? }
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
# The connection every chapter's database-taking persistence adapter
|
|
28
|
+
# uses unless that chapter's own world says otherwise — a world's
|
|
29
|
+
# per-chapter `persisted_by ... database` block, written once.
|
|
30
|
+
value_object "DefaultDatabase" do
|
|
31
|
+
attribute :value, String
|
|
32
|
+
invariant("a default database says something") { !value.to_s.empty? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The persistence adapter every aggregate binds to unless its own
|
|
36
|
+
# chapter's hecksagon binds it — between an explicit bind and the
|
|
37
|
+
# framework's in-memory fallback.
|
|
38
|
+
value_object "DefaultAdapter" do
|
|
39
|
+
attribute :value, String
|
|
40
|
+
invariant("a default adapter names an adapter") { !value.to_s.empty? }
|
|
41
|
+
end
|
|
42
|
+
|
|
25
43
|
command "Declare" do
|
|
26
44
|
role "Deployment"
|
|
27
45
|
goal "Open a world for a domain"
|
|
@@ -29,10 +47,14 @@ Hecks.bluebook "World" do
|
|
|
29
47
|
attribute :domain, WorldName
|
|
30
48
|
attribute :realm, Realm, optional: true
|
|
31
49
|
attribute :latest, Latest, optional: true
|
|
50
|
+
attribute :default_database, DefaultDatabase, optional: true
|
|
51
|
+
attribute :default_adapter, DefaultAdapter, optional: true
|
|
32
52
|
|
|
33
53
|
sets :domain
|
|
34
54
|
sets :realm
|
|
35
55
|
sets :latest
|
|
56
|
+
sets :default_database
|
|
57
|
+
sets :default_adapter
|
|
36
58
|
|
|
37
59
|
emits "WorldDeclared"
|
|
38
60
|
end
|
|
@@ -55,6 +77,12 @@ Hecks.bluebook "World" do
|
|
|
55
77
|
member word: "world", context: "File", body: "keywords", inner: "World", opens: "World", fills: ""
|
|
56
78
|
member word: "realm", context: "World", body: "none", inner: "", opens: "", fills: "realm", calls: "realm_impl"
|
|
57
79
|
member word: "latest", context: "World", body: "none", inner: "", opens: "", fills: "latest", calls: "latest_impl"
|
|
80
|
+
# THE TWO CROSS-CHAPTER DEFAULTS — the same connection string and the
|
|
81
|
+
# same persistence adapter every chapter of a project repeats, written
|
|
82
|
+
# once. A chapter's own world (`persisted_by ... database`) and its own
|
|
83
|
+
# hecksagon bind still win; see docs/implemented/reference/world.md.
|
|
84
|
+
member word: "default_database", context: "World", body: "none", inner: "", opens: "", fills: "default_database", calls: "default_database_impl"
|
|
85
|
+
member word: "default_adapter", context: "World", body: "none", inner: "", opens: "", fills: "default_adapter", calls: "default_adapter_impl"
|
|
58
86
|
|
|
59
87
|
end
|
|
60
88
|
|
|
@@ -78,6 +106,8 @@ Hecks.bluebook "World" do
|
|
|
78
106
|
member keyword: "world", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "domain"
|
|
79
107
|
member keyword: "realm", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "realm"
|
|
80
108
|
member keyword: "latest", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "latest"
|
|
109
|
+
member keyword: "default_database", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "default_database"
|
|
110
|
+
member keyword: "default_adapter", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "default_adapter"
|
|
81
111
|
|
|
82
112
|
end
|
|
83
113
|
end
|