hecks 2.6.0 → 2.8.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/exe/hecks +13 -0
- 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/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- 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/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- 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/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- 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/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- 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/state_codec.rb +1 -1
- 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 +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- 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/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- 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/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Projections
|
|
3
|
+
module Deploy
|
|
4
|
+
module TemplateDiff
|
|
5
|
+
# Compares two loaded CloudFormation templates.
|
|
6
|
+
#
|
|
7
|
+
# Resources, parameters, outputs, conditions and mappings are matched
|
|
8
|
+
# by name, then compared property by property. A list of mappings that
|
|
9
|
+
# each carry a `Name`, `Id`, `Key` or similar field (container
|
|
10
|
+
# definitions, environment variables, tags, origins) is matched by that
|
|
11
|
+
# field, not by position, so inserting one entry reports one addition
|
|
12
|
+
# instead of a cascade of shifted values. Cache behaviors are matched
|
|
13
|
+
# the same way but their order is also checked, because CloudFront
|
|
14
|
+
# takes the first pattern that matches.
|
|
15
|
+
#
|
|
16
|
+
# Two values that differ only in how they are written (a number and
|
|
17
|
+
# the same digits as text, a `!Sub` with nothing but one `${Name}` and
|
|
18
|
+
# a `!Ref Name`) are equal after normalization or marked `cosmetic`.
|
|
19
|
+
module Comparer
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
Change = Struct.new(:path, :kind, :before, :after, :cosmetic, keyword_init: true)
|
|
23
|
+
Entity = Struct.new(:name, :type, :changes, :replaced, keyword_init: true)
|
|
24
|
+
SectionDiff = Struct.new(:added, :removed, :changed, keyword_init: true) do
|
|
25
|
+
# Tells whether nothing in the section differs.
|
|
26
|
+
#
|
|
27
|
+
# @return [Boolean] true when no entity was added, removed or changed
|
|
28
|
+
def empty? = added.empty? && removed.empty? && changed.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ENTITY_SECTIONS = %w[Parameters Resources Outputs Conditions Mappings].freeze
|
|
32
|
+
IDENTITY_KEYS = %w[Name Id Key PolicyName PathPattern HeaderName ContainerName Sid Field].freeze
|
|
33
|
+
ORDERED_BY = %w[PathPattern].freeze
|
|
34
|
+
|
|
35
|
+
# Compares two templates.
|
|
36
|
+
#
|
|
37
|
+
# @param before [Hash{String => Object}] the template as `Loader.load` returns it
|
|
38
|
+
# @param after [Hash{String => Object}] the template to compare it with
|
|
39
|
+
# @return [Hash{String => SectionDiff, Array<Change>}] one `SectionDiff` per entity
|
|
40
|
+
# section
|
|
41
|
+
# that either template has, and `"Template"` mapped to the changes in every other
|
|
42
|
+
# top-level key such as `Description`
|
|
43
|
+
def compare(before, after)
|
|
44
|
+
before = normalize(before)
|
|
45
|
+
after = normalize(after)
|
|
46
|
+
diff = ENTITY_SECTIONS.each_with_object({}) do |section, sections|
|
|
47
|
+
next unless before.key?(section) || after.key?(section)
|
|
48
|
+
|
|
49
|
+
sections[section] = section_diff(section, before[section] || {}, after[section] || {})
|
|
50
|
+
end
|
|
51
|
+
diff["Template"] = template_changes(before, after)
|
|
52
|
+
diff
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def normalize(value)
|
|
56
|
+
case value
|
|
57
|
+
when Hash then normalize_hash(value)
|
|
58
|
+
when Array then value.map { |item| normalize(item) }
|
|
59
|
+
else value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
private_class_method :normalize
|
|
63
|
+
|
|
64
|
+
def normalize_hash(hash)
|
|
65
|
+
result = hash.to_h { |key, item| [key, key == "DependsOn" ? depends_on(item) : normalize(item)] }
|
|
66
|
+
substitution?(result) ? simplify_sub(result) : result
|
|
67
|
+
end
|
|
68
|
+
private_class_method :normalize_hash
|
|
69
|
+
|
|
70
|
+
def depends_on(value)
|
|
71
|
+
Array(value).map(&:to_s).sort
|
|
72
|
+
end
|
|
73
|
+
private_class_method :depends_on
|
|
74
|
+
|
|
75
|
+
def substitution?(hash)
|
|
76
|
+
hash.size == 1 && hash["Fn::Sub"].is_a?(String)
|
|
77
|
+
end
|
|
78
|
+
private_class_method :substitution?
|
|
79
|
+
|
|
80
|
+
# `!Sub "${Name}"` is `!Ref Name`, `!Sub "${A.B}"` is `!GetAtt A.B`, and a `!Sub` with no
|
|
81
|
+
# variable is the plain string.
|
|
82
|
+
def simplify_sub(hash)
|
|
83
|
+
text = hash["Fn::Sub"]
|
|
84
|
+
return text unless text.include?("${")
|
|
85
|
+
|
|
86
|
+
match = text.match(/\A\$\{([^}!]+)\}\z/)
|
|
87
|
+
return hash unless match
|
|
88
|
+
|
|
89
|
+
name = match[1]
|
|
90
|
+
name.include?(".") && !name.start_with?("AWS::") ? { "Fn::GetAtt" => name.split(".", 2) } : { "Ref" => name }
|
|
91
|
+
end
|
|
92
|
+
private_class_method :simplify_sub
|
|
93
|
+
|
|
94
|
+
def section_diff(section, before, after)
|
|
95
|
+
SectionDiff.new(
|
|
96
|
+
added: (after.keys - before.keys).sort.map { |name| [name, type_of(section, after[name])] },
|
|
97
|
+
removed: (before.keys - after.keys).sort.map { |name| [name, type_of(section, before[name])] },
|
|
98
|
+
changed: (before.keys & after.keys).sort.filter_map { |name| entity_diff(section, name, before[name], after[name]) }
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
private_class_method :section_diff
|
|
102
|
+
|
|
103
|
+
def type_of(section, entity)
|
|
104
|
+
entity.is_a?(Hash) && %w[Resources Parameters].include?(section) ? entity["Type"] : nil
|
|
105
|
+
end
|
|
106
|
+
private_class_method :type_of
|
|
107
|
+
|
|
108
|
+
def entity_diff(section, name, before, after)
|
|
109
|
+
return nil if before == after
|
|
110
|
+
|
|
111
|
+
changes = []
|
|
112
|
+
diff_values("", before, after, changes)
|
|
113
|
+
return nil if changes.empty?
|
|
114
|
+
|
|
115
|
+
replaced = section == "Resources" && before.is_a?(Hash) && after.is_a?(Hash) && before["Type"] != after["Type"]
|
|
116
|
+
Entity.new(name: name, type: type_of(section, after), changes: changes, replaced: replaced)
|
|
117
|
+
end
|
|
118
|
+
private_class_method :entity_diff
|
|
119
|
+
|
|
120
|
+
def template_changes(before, after)
|
|
121
|
+
changes = []
|
|
122
|
+
others = (before.keys | after.keys) - ENTITY_SECTIONS
|
|
123
|
+
others.sort.each { |key| diff_values(key, before[key], after[key], changes) unless before[key] == after[key] }
|
|
124
|
+
changes
|
|
125
|
+
end
|
|
126
|
+
private_class_method :template_changes
|
|
127
|
+
|
|
128
|
+
def diff_values(path, before, after, changes)
|
|
129
|
+
return if before == after
|
|
130
|
+
|
|
131
|
+
if before.is_a?(Hash) && after.is_a?(Hash)
|
|
132
|
+
diff_hashes(path, before, after, changes)
|
|
133
|
+
elsif before.is_a?(Array) && after.is_a?(Array)
|
|
134
|
+
diff_arrays(path, before, after, changes)
|
|
135
|
+
else
|
|
136
|
+
changes << Change.new(path: path, kind: :changed, before: before, after: after, cosmetic: same_text?(before, after))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
private_class_method :diff_values
|
|
140
|
+
|
|
141
|
+
def diff_hashes(path, before, after, changes)
|
|
142
|
+
(before.keys | after.keys).sort.each do |key|
|
|
143
|
+
child = path.empty? ? key : "#{path}.#{key}"
|
|
144
|
+
if !after.key?(key)
|
|
145
|
+
changes << Change.new(path: child, kind: :removed, before: before[key])
|
|
146
|
+
elsif !before.key?(key)
|
|
147
|
+
changes << Change.new(path: child, kind: :added, after: after[key])
|
|
148
|
+
else
|
|
149
|
+
diff_values(child, before[key], after[key], changes)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
private_class_method :diff_hashes
|
|
154
|
+
|
|
155
|
+
def diff_arrays(path, before, after, changes)
|
|
156
|
+
key = identity_key(before, after)
|
|
157
|
+
return diff_keyed(path, key, before, after, changes) if key
|
|
158
|
+
if before.sort_by(&:to_s) == after.sort_by(&:to_s)
|
|
159
|
+
return changes << Change.new(path: path, kind: :reordered, before: before, after: after,
|
|
160
|
+
cosmetic: true)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
(0...[before.size, after.size].max).each do |index|
|
|
164
|
+
child = "#{path}[#{index}]"
|
|
165
|
+
if index >= after.size then changes << Change.new(path: child, kind: :removed, before: before[index])
|
|
166
|
+
elsif index >= before.size then changes << Change.new(path: child, kind: :added, after: after[index])
|
|
167
|
+
else diff_values(child, before[index], after[index], changes)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
private_class_method :diff_arrays
|
|
172
|
+
|
|
173
|
+
def diff_keyed(path, key, before, after, changes)
|
|
174
|
+
left = before.to_h { |item| [item[key].to_s, item] }
|
|
175
|
+
right = after.to_h { |item| [item[key].to_s, item] }
|
|
176
|
+
(left.keys | right.keys).each do |name|
|
|
177
|
+
child = "#{path}[#{key}=#{name}]"
|
|
178
|
+
if !right.key?(name) then changes << Change.new(path: child, kind: :removed, before: left[name])
|
|
179
|
+
elsif !left.key?(name) then changes << Change.new(path: child, kind: :added, after: right[name])
|
|
180
|
+
else diff_values(child, left[name], right[name], changes)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
check_order(path, key, left.keys, right.keys, changes)
|
|
184
|
+
end
|
|
185
|
+
private_class_method :diff_keyed
|
|
186
|
+
|
|
187
|
+
def check_order(path, key, left, right, changes)
|
|
188
|
+
return unless ORDERED_BY.include?(key)
|
|
189
|
+
|
|
190
|
+
common = left & right
|
|
191
|
+
return if left.select { |name| common.include?(name) } == right.select { |name| common.include?(name) }
|
|
192
|
+
|
|
193
|
+
changes << Change.new(path: path, kind: :reordered, before: left, after: right, cosmetic: false)
|
|
194
|
+
end
|
|
195
|
+
private_class_method :check_order
|
|
196
|
+
|
|
197
|
+
# A list of mappings is matched by a field only when every entry in both lists has it and
|
|
198
|
+
# no two entries share its value, so a match is never ambiguous.
|
|
199
|
+
def identity_key(before, after)
|
|
200
|
+
lists = [before, after]
|
|
201
|
+
return nil if lists.any? { |list| list.empty? || !list.all?(Hash) }
|
|
202
|
+
|
|
203
|
+
IDENTITY_KEYS.find do |key|
|
|
204
|
+
lists.all? do |list|
|
|
205
|
+
list.all? { |item| scalar?(item[key]) } && list.map do |item|
|
|
206
|
+
item[key].to_s
|
|
207
|
+
end.uniq.size == list.size
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
private_class_method :identity_key
|
|
212
|
+
|
|
213
|
+
def scalar?(value)
|
|
214
|
+
value.is_a?(String) || value.is_a?(Integer)
|
|
215
|
+
end
|
|
216
|
+
private_class_method :scalar?
|
|
217
|
+
|
|
218
|
+
def same_text?(before, after)
|
|
219
|
+
scalars = [before, after].all? do |value|
|
|
220
|
+
[String, Integer, Float, TrueClass, FalseClass].any? do |type|
|
|
221
|
+
value.is_a?(type)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
scalars && before.to_s == after.to_s
|
|
225
|
+
end
|
|
226
|
+
private_class_method :same_text?
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "psych"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
module Deploy
|
|
6
|
+
module TemplateDiff
|
|
7
|
+
# Reads a CloudFormation template in YAML into plain Ruby data.
|
|
8
|
+
#
|
|
9
|
+
# CloudFormation's short forms (`!Ref Name`, `!GetAtt A.B`, `!Sub "..."`)
|
|
10
|
+
# are YAML tags that a stock loader refuses or drops. This loader walks
|
|
11
|
+
# the parsed document and turns each into the long form CloudFormation
|
|
12
|
+
# itself reads (`{"Ref" => "Name"}`, `{"Fn::GetAtt" => ["A", "B"]}`), so
|
|
13
|
+
# a template written with short forms and one written with long forms
|
|
14
|
+
# load to the same data. Comments and key order do not survive loading,
|
|
15
|
+
# which is what a comparison of two templates wants.
|
|
16
|
+
#
|
|
17
|
+
# Nothing here evaluates a template or contacts AWS.
|
|
18
|
+
module Loader
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Turns template text into data.
|
|
22
|
+
#
|
|
23
|
+
# @param text [String] a CloudFormation template written in YAML
|
|
24
|
+
# @return [Hash{String => Object}] the template, with intrinsic tags in their long form
|
|
25
|
+
# @raise [ArgumentError] if the text is not one YAML mapping, or uses an alias
|
|
26
|
+
def load(text)
|
|
27
|
+
document = Psych.parse(text)
|
|
28
|
+
raise ArgumentError, "the template is empty" unless document
|
|
29
|
+
|
|
30
|
+
value = convert(document.root)
|
|
31
|
+
raise ArgumentError, "a CloudFormation template is a mapping at the top level" unless value.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
value
|
|
34
|
+
rescue Psych::SyntaxError => e
|
|
35
|
+
raise ArgumentError, "the template is not valid YAML: #{e.message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Reads a template from a file.
|
|
39
|
+
#
|
|
40
|
+
# @param path [String] the path of a YAML template
|
|
41
|
+
# @return [Hash{String => Object}] the template, as `load` returns it
|
|
42
|
+
# @raise [ArgumentError] if the file is missing or is not a template
|
|
43
|
+
def load_file(path)
|
|
44
|
+
raise ArgumentError, "#{path} does not exist" unless File.file?(path)
|
|
45
|
+
|
|
46
|
+
load(File.read(path))
|
|
47
|
+
rescue ArgumentError => e
|
|
48
|
+
raise if e.message.start_with?(path)
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "#{path}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def convert(node)
|
|
54
|
+
raise ArgumentError, "YAML aliases are not supported in a template" if node.is_a?(Psych::Nodes::Alias)
|
|
55
|
+
|
|
56
|
+
value =
|
|
57
|
+
case node
|
|
58
|
+
when Psych::Nodes::Mapping then mapping(node)
|
|
59
|
+
when Psych::Nodes::Sequence then node.children.map { |child| convert(child) }
|
|
60
|
+
else scalar(node)
|
|
61
|
+
end
|
|
62
|
+
intrinsic?(node.tag) ? intrinsic(node.tag, value) : value
|
|
63
|
+
end
|
|
64
|
+
private_class_method :convert
|
|
65
|
+
|
|
66
|
+
def mapping(node)
|
|
67
|
+
node.children.each_slice(2).to_h { |key, value| [convert(key).to_s, convert(value)] }
|
|
68
|
+
end
|
|
69
|
+
private_class_method :mapping
|
|
70
|
+
|
|
71
|
+
# A plain scalar is typed the way YAML types it (`8080` is a number, `true` a boolean); a
|
|
72
|
+
# quoted or block scalar is always text.
|
|
73
|
+
def scalar(node)
|
|
74
|
+
return node.value unless node.plain && node.tag.nil?
|
|
75
|
+
|
|
76
|
+
Psych::ScalarScanner.new(Psych::ClassLoader::Restricted.new([], [])).tokenize(node.value)
|
|
77
|
+
end
|
|
78
|
+
private_class_method :scalar
|
|
79
|
+
|
|
80
|
+
def intrinsic?(tag)
|
|
81
|
+
tag&.start_with?("!") && !tag.start_with?("!!")
|
|
82
|
+
end
|
|
83
|
+
private_class_method :intrinsic?
|
|
84
|
+
|
|
85
|
+
def intrinsic(tag, value)
|
|
86
|
+
name = tag.delete_prefix("!")
|
|
87
|
+
case name
|
|
88
|
+
when "Ref", "Condition" then { name => value }
|
|
89
|
+
when "GetAtt" then { "Fn::GetAtt" => get_att(value) }
|
|
90
|
+
else { "Fn::#{name}" => value }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :intrinsic
|
|
94
|
+
|
|
95
|
+
# `!GetAtt A.B` is the short form of `["A", "B"]`; only the first dot separates the
|
|
96
|
+
# resource from the attribute, since an attribute may itself contain dots.
|
|
97
|
+
def get_att(value)
|
|
98
|
+
value.is_a?(String) ? value.split(".", 2) : value
|
|
99
|
+
end
|
|
100
|
+
private_class_method :get_att
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "template_diff/loader"
|
|
3
|
+
require_relative "template_diff/comparer"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Projections
|
|
7
|
+
module Deploy
|
|
8
|
+
# An offline comparison of two CloudFormation templates.
|
|
9
|
+
#
|
|
10
|
+
# It answers "what would change if this template replaced that one"
|
|
11
|
+
# from the two files alone, the offline half of checking that a
|
|
12
|
+
# generated template leaves a live stack untouched: when the generated
|
|
13
|
+
# template and the one already deployed differ in nothing but comments,
|
|
14
|
+
# ordering and spelling, a change set against the live stack should be
|
|
15
|
+
# empty too. It reads no AWS state and changes nothing.
|
|
16
|
+
#
|
|
17
|
+
# ## What it reports
|
|
18
|
+
#
|
|
19
|
+
# Resources, parameters, outputs, conditions and mappings that were
|
|
20
|
+
# added, removed or changed, by logical id, and for a changed one every
|
|
21
|
+
# property that differs, with the value before and after. Other
|
|
22
|
+
# top-level keys, such as `Description`, are compared as values.
|
|
23
|
+
# Comments and the order of keys do not count, and the short and long
|
|
24
|
+
# forms of an intrinsic (`!Ref Name`, `{"Ref": "Name"}`) are the same
|
|
25
|
+
# thing. A change to a resource's `Type` is marked as a replacement.
|
|
26
|
+
# Differences that are only in how a value is written are marked
|
|
27
|
+
# cosmetic and do not count as a difference unless `strict` is set.
|
|
28
|
+
#
|
|
29
|
+
# ## What it does not do
|
|
30
|
+
#
|
|
31
|
+
# It does not know which property changes CloudFormation applies in
|
|
32
|
+
# place and which replace the resource beyond a change of `Type`, so a
|
|
33
|
+
# changed immutable property is listed as a change, not flagged.
|
|
34
|
+
module TemplateDiff
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
Report = Struct.new(:sections, :strict, keyword_init: true) do
|
|
38
|
+
# Tells whether the two templates differ in a way that counts.
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean] false when every difference is cosmetic and the report is not strict
|
|
41
|
+
def different?
|
|
42
|
+
sections.any? do |name, diff|
|
|
43
|
+
if name == "Template"
|
|
44
|
+
counted(diff).any?
|
|
45
|
+
else
|
|
46
|
+
!diff.added.empty? || !diff.removed.empty? || diff.changed.any? { |entity| counted(entity.changes).any? }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Keeps the changes that count towards a difference.
|
|
52
|
+
#
|
|
53
|
+
# @param changes [Array<Comparer::Change>] the changes to filter
|
|
54
|
+
# @return [Array<Comparer::Change>] every change, or only the non-cosmetic ones unless
|
|
55
|
+
# strict
|
|
56
|
+
def counted(changes) = strict ? changes : changes.reject(&:cosmetic)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Compares two template texts.
|
|
60
|
+
#
|
|
61
|
+
# @param before [String] the first template, YAML
|
|
62
|
+
# @param after [String] the second template, YAML
|
|
63
|
+
# @param strict [Boolean] whether cosmetic differences count as differences
|
|
64
|
+
# @return [Report] the differences, by section
|
|
65
|
+
# @raise [ArgumentError] if either text is not a valid template
|
|
66
|
+
def diff(before, after, strict: false)
|
|
67
|
+
Report.new(sections: Comparer.compare(Loader.load(before), Loader.load(after)), strict: strict)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Compares two template files.
|
|
71
|
+
#
|
|
72
|
+
# @param before_path [String] the path of the first template
|
|
73
|
+
# @param after_path [String] the path of the template to compare it with
|
|
74
|
+
# @param strict [Boolean] whether cosmetic differences count as differences
|
|
75
|
+
# @return [Report] the differences, by section
|
|
76
|
+
# @raise [ArgumentError] if either file is missing or is not a valid template
|
|
77
|
+
def diff_files(before_path, after_path, strict: false)
|
|
78
|
+
Report.new(
|
|
79
|
+
sections: Comparer.compare(Loader.load_file(before_path), Loader.load_file(after_path)),
|
|
80
|
+
strict: strict
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Writes a report as text for a person to read.
|
|
85
|
+
#
|
|
86
|
+
# @param report [Report] the differences to describe
|
|
87
|
+
# @return [String] one section per kind of entity, each entity on a `+`, `-` or `~` line,
|
|
88
|
+
# or `no differences` when nothing differs
|
|
89
|
+
def render(report)
|
|
90
|
+
lines = report.sections.flat_map do |name, diff|
|
|
91
|
+
name == "Template" ? template_lines(report, diff) : section_lines(report, name, diff)
|
|
92
|
+
end
|
|
93
|
+
return "no differences\n" if lines.empty?
|
|
94
|
+
|
|
95
|
+
note = report.different? ? [] : ["", "only cosmetic differences; --strict counts them"]
|
|
96
|
+
"#{(lines + note).join("\n")}\n"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Writes a report as JSON for a program to read.
|
|
100
|
+
#
|
|
101
|
+
# @param report [Report] the differences to describe
|
|
102
|
+
# @return [String] a JSON document with `different` and one entry per section
|
|
103
|
+
def render_json(report)
|
|
104
|
+
sections = report.sections.to_h do |name, diff|
|
|
105
|
+
[name, name == "Template" ? diff.map { |change| change_hash(change) } : section_hash(diff)]
|
|
106
|
+
end
|
|
107
|
+
JSON.pretty_generate("different" => report.different?, "sections" => sections)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def template_lines(report, changes)
|
|
111
|
+
return [] if changes.empty?
|
|
112
|
+
|
|
113
|
+
["Template"] + changes.flat_map { |change| change_lines(change, report, " ") }
|
|
114
|
+
end
|
|
115
|
+
private_class_method :template_lines
|
|
116
|
+
|
|
117
|
+
def section_lines(report, name, diff)
|
|
118
|
+
return [] if diff.empty?
|
|
119
|
+
|
|
120
|
+
[name] +
|
|
121
|
+
diff.added.map { |id, type| " + #{id}#{" (#{type})" if type}" } +
|
|
122
|
+
diff.removed.map { |id, type| " - #{id}#{" (#{type})" if type}" } +
|
|
123
|
+
diff.changed.flat_map { |entity| entity_lines(entity, report) }
|
|
124
|
+
end
|
|
125
|
+
private_class_method :section_lines
|
|
126
|
+
|
|
127
|
+
def entity_lines(entity, report)
|
|
128
|
+
head = " ~ #{entity.name}#{" (#{entity.type})" if entity.type}#{' REPLACEMENT: the type changed' if entity.replaced}"
|
|
129
|
+
[head] + entity.changes.flat_map { |change| change_lines(change, report, " ") }
|
|
130
|
+
end
|
|
131
|
+
private_class_method :entity_lines
|
|
132
|
+
|
|
133
|
+
def change_lines(change, report, indent)
|
|
134
|
+
marker = change.cosmetic && !report.strict ? " (cosmetic)" : ""
|
|
135
|
+
case change.kind
|
|
136
|
+
when :added then ["#{indent}+ #{change.path}: #{show(change.after)}"]
|
|
137
|
+
when :removed then ["#{indent}- #{change.path}: #{show(change.before)}"]
|
|
138
|
+
when :reordered then ["#{indent}~ #{change.path}: order #{show(change.before)} -> #{show(change.after)}#{marker}"]
|
|
139
|
+
else ["#{indent}~ #{change.path}: #{show(change.before)} -> #{show(change.after)}#{marker}"]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
private_class_method :change_lines
|
|
143
|
+
|
|
144
|
+
def show(value)
|
|
145
|
+
text = value.is_a?(String) ? value.inspect : JSON.generate(value)
|
|
146
|
+
text.length > 140 ? "#{text[0, 137]}..." : text
|
|
147
|
+
end
|
|
148
|
+
private_class_method :show
|
|
149
|
+
|
|
150
|
+
def section_hash(diff)
|
|
151
|
+
{
|
|
152
|
+
"added" => diff.added.map { |id, type| { "id" => id, "type" => type }.compact },
|
|
153
|
+
"removed" => diff.removed.map { |id, type| { "id" => id, "type" => type }.compact },
|
|
154
|
+
"changed" => diff.changed.map { |entity| entity_hash(entity) }
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
private_class_method :section_hash
|
|
158
|
+
|
|
159
|
+
def entity_hash(entity)
|
|
160
|
+
changes = entity.changes.map { |change| change_hash(change) }
|
|
161
|
+
{ "id" => entity.name, "type" => entity.type, "replaced" => entity.replaced, "changes" => changes }.compact
|
|
162
|
+
end
|
|
163
|
+
private_class_method :entity_hash
|
|
164
|
+
|
|
165
|
+
def change_hash(change)
|
|
166
|
+
{ "path" => change.path, "kind" => change.kind.to_s, "before" => change.before, "after" => change.after,
|
|
167
|
+
"cosmetic" => change.cosmetic }.compact
|
|
168
|
+
end
|
|
169
|
+
private_class_method :change_hash
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -7,7 +7,7 @@ module Hecks
|
|
|
7
7
|
# deployment declares apart from the domain (`Privacy::Marking`, or a
|
|
8
8
|
# `has_phi(readable_by:)` chain in a `.hecksagon`) and handed in as
|
|
9
9
|
# `options[:markings]`. A marking names an aggregate by its qualified
|
|
10
|
-
# name (`"
|
|
10
|
+
# name (`"Site::Registration"`) and a field by its dotted path
|
|
11
11
|
# (`"attendee.medications"`). Its category is spoken as the acronym it
|
|
12
12
|
# is, uppercased and never reworded, because the category vocabulary is
|
|
13
13
|
# the deployment's own and open-ended.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
require_relative "../doc/reference"
|
|
2
1
|
require_relative "../projector"
|
|
3
2
|
|
|
4
3
|
module Hecks
|
|
4
|
+
# Repository-only tooling: `Doc::Reference` reads the committed pages under
|
|
5
|
+
# `docs/`, which only a checkout has, so it loads on first use and the
|
|
6
|
+
# packaged gem leaves it out (ADR 0066).
|
|
7
|
+
module Doc
|
|
8
|
+
autoload :Reference, File.expand_path("../doc/reference", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
module Projections
|
|
6
12
|
# The DSL reference pages, projected from the chapter's own Syntax
|
|
7
13
|
# aggregate — the tables come from the declaration, the prose is
|
data/lib/hecks/projections.rb
CHANGED
|
@@ -36,3 +36,5 @@ require_relative "projections/glossary"
|
|
|
36
36
|
require_relative "projections/deploy/shared"
|
|
37
37
|
require_relative "projections/deploy/lambda"
|
|
38
38
|
require_relative "projections/deploy/fargate"
|
|
39
|
+
require_relative "projections/deploy/smoke"
|
|
40
|
+
require_relative "projections/deploy/template_diff"
|
|
@@ -263,10 +263,10 @@ module Hecks
|
|
|
263
263
|
# nothing this domain attaches provides payment_connection.
|
|
264
264
|
# @param registry [Runtime::Registry] the booted registry `domain_name` is loaded in
|
|
265
265
|
# @param domain_name [String] the domain to export the payment-connection binding for
|
|
266
|
-
# @return [Hash{Symbol => String, nil}] `:provider` (name), one qualified verb per
|
|
267
|
-
# (`:connect`, `:reconnect`, `:disconnect`, `:suspend`, `:resume`,
|
|
268
|
-
# `:aggregate` (`:connect`'s own leading qualified
|
|
269
|
-
# domain attaches provides payment_connection
|
|
266
|
+
# @return [Hash{Symbol => String, nil}] `:provider` (name), one qualified verb per
|
|
267
|
+
# contract key (`:connect`, `:reconnect`, `:disconnect`, `:suspend`, `:resume`,
|
|
268
|
+
# `:enable`, `:disable`) and `:aggregate` (`:connect`'s own leading qualified
|
|
269
|
+
# aggregate name); `{}` if nothing this domain attaches provides payment_connection
|
|
270
270
|
def payment_connection(registry, domain_name)
|
|
271
271
|
provider = registry.payment_connection_provider_for(domain_name)
|
|
272
272
|
return {} unless provider
|
|
@@ -59,7 +59,7 @@ module Hecks
|
|
|
59
59
|
# rather than as a `raise` in the projection's own body, so the
|
|
60
60
|
# requirement is stated instead of written as behaviour. The
|
|
61
61
|
# registry refuses before the projection runs, so the projection
|
|
62
|
-
# itself
|
|
62
|
+
# itself carries no guard about its own admission.
|
|
63
63
|
# `emits:` says what kind of artifact comes back.
|
|
64
64
|
#
|
|
65
65
|
# `:artifact` (the default) is one thing — a Hash, or a String.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Release
|
|
7
|
+
class GemPin
|
|
8
|
+
# Raised when a pin cannot be resolved or is refused.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# The hecks releases RubyGems publishes, fetched on first use.
|
|
12
|
+
#
|
|
13
|
+
# `GemPin` takes any object answering `include?` and `newest_satisfying`,
|
|
14
|
+
# so a caller with no network, or a spec, hands it a stand-in and this
|
|
15
|
+
# class is never asked to dial out.
|
|
16
|
+
class PublishedVersions
|
|
17
|
+
URL = URI("https://rubygems.org/api/v1/versions/hecks.json")
|
|
18
|
+
|
|
19
|
+
# Says whether RubyGems publishes a version.
|
|
20
|
+
#
|
|
21
|
+
# @param version [String, Gem::Version] the version to look for
|
|
22
|
+
# @return [Boolean] true when it is in the published list, prereleases included
|
|
23
|
+
# @raise [GemPin::Error] if RubyGems cannot be reached or does not answer with the list
|
|
24
|
+
def include?(version)
|
|
25
|
+
versions.include?(Gem::Version.new(version))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Finds the newest stable release a requirement allows.
|
|
29
|
+
#
|
|
30
|
+
# @param requirement [Gem::Requirement] the constraint to satisfy
|
|
31
|
+
# @return [Gem::Version, nil] the newest non-prerelease version that satisfies it, or nil
|
|
32
|
+
# @raise [GemPin::Error] if RubyGems cannot be reached or does not answer with the list
|
|
33
|
+
def newest_satisfying(requirement)
|
|
34
|
+
versions.reject(&:prerelease?).select { |version| requirement.satisfied_by?(version) }.max
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def versions
|
|
40
|
+
@versions ||= fetch
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetch
|
|
44
|
+
response = Net::HTTP.start(URL.host, URL.port, use_ssl: true, open_timeout: 10, read_timeout: 20) do |http|
|
|
45
|
+
http.get(URL.request_uri)
|
|
46
|
+
end
|
|
47
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
48
|
+
raise Error, "could not list published hecks versions (RubyGems answered #{response.code})"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
JSON.parse(response.body).map { |entry| Gem::Version.new(entry.fetch("number")) }
|
|
52
|
+
rescue SocketError, Timeout::Error, SystemCallError => e
|
|
53
|
+
raise Error, "could not reach RubyGems to check the hecks version (#{e.class})"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|