textus 0.55.1 → 0.56.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/CHANGELOG.md +175 -2768
- data/README.md +17 -18
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +2 -39
- data/lib/textus/human.rb +7 -0
- data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
- data/lib/textus/infra/clock.rb +7 -0
- data/lib/textus/infra/database.rb +127 -0
- data/lib/textus/infra/file_system.rb +54 -0
- data/lib/textus/infra/locks.rb +50 -0
- data/lib/textus/infra/port/clock.rb +18 -0
- data/lib/textus/infra/port/publisher.rb +71 -0
- data/lib/textus/infra/port/sentinel.rb +25 -0
- data/lib/textus/infra/port/sentinel_store.rb +93 -0
- data/lib/textus/infra/store/audit_log.rb +42 -0
- data/lib/textus/infra/store/base.rb +25 -0
- data/lib/textus/infra/store/cursor_store.rb +27 -0
- data/lib/textus/infra/store/entry_index.rb +31 -0
- data/lib/textus/infra/store/envelope/meta.rb +63 -0
- data/lib/textus/infra/store/file_store.rb +23 -0
- data/lib/textus/infra/store/index/builder.rb +28 -0
- data/lib/textus/infra/store/index/lookup.rb +23 -0
- data/lib/textus/infra/store/link_store.rb +43 -0
- data/lib/textus/infra/store/workflow_queue.rb +54 -0
- data/lib/textus/infra/store.rb +29 -0
- data/lib/textus/ingest.rb +8 -0
- data/lib/textus/lanes/artifact/cache.rb +35 -0
- data/lib/textus/lanes/artifact/handlers.rb +29 -0
- data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
- data/lib/textus/lanes/artifact/verbs.rb +18 -0
- data/lib/textus/lanes/ingest/dedup.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
- data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
- data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
- data/lib/textus/lanes/ingest/entry_types.rb +25 -0
- data/lib/textus/lanes/ingest/handlers.rb +54 -0
- data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
- data/lib/textus/lanes/ingest/key_builder.rb +33 -0
- data/lib/textus/lanes/ingest/resolver.rb +21 -0
- data/lib/textus/lanes/knowledge/freeform.rb +15 -0
- data/lib/textus/lanes/knowledge/handlers.rb +53 -0
- data/lib/textus/lanes/knowledge/runner.rb +45 -0
- data/lib/textus/lanes/knowledge/verbs.rb +16 -0
- data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
- data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
- data/lib/textus/{gate → protocol}/binder.rb +2 -10
- data/lib/textus/protocol/boot/catalog.rb +34 -0
- data/lib/textus/protocol/boot/context.rb +124 -0
- data/lib/textus/protocol/boot/protocol.rb +33 -0
- data/lib/textus/protocol/boot.rb +42 -0
- data/lib/textus/protocol/builder/domain.rb +17 -0
- data/lib/textus/protocol/builder/infrastructure.rb +17 -0
- data/lib/textus/protocol/builder/runtime.rb +46 -0
- data/lib/textus/protocol/builder.rb +11 -0
- data/lib/textus/protocol/container.rb +22 -0
- data/lib/textus/protocol/diff.rb +170 -0
- data/lib/textus/protocol/entry_constraint.rb +54 -0
- data/lib/textus/protocol/envelope.rb +70 -0
- data/lib/textus/protocol/format/base.rb +80 -0
- data/lib/textus/protocol/format/bash.rb +10 -0
- data/lib/textus/protocol/format/javascript.rb +10 -0
- data/lib/textus/protocol/format/json.rb +82 -0
- data/lib/textus/protocol/format/markdown.rb +53 -0
- data/lib/textus/protocol/format/python.rb +10 -0
- data/lib/textus/protocol/format/ruby.rb +10 -0
- data/lib/textus/protocol/format/script.rb +55 -0
- data/lib/textus/protocol/format/text.rb +48 -0
- data/lib/textus/protocol/format/yaml.rb +105 -0
- data/lib/textus/protocol/format.rb +73 -0
- data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
- data/lib/textus/protocol/gate.rb +61 -0
- data/lib/textus/protocol/handlers/command_context.rb +19 -0
- data/lib/textus/protocol/handlers/concern.rb +15 -0
- data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
- data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
- data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
- data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
- data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
- data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
- data/lib/textus/protocol/handlers/query_context.rb +41 -0
- data/lib/textus/protocol/handlers/read.rb +67 -0
- data/lib/textus/protocol/handlers/system.rb +93 -0
- data/lib/textus/protocol/handlers/write.rb +57 -0
- data/lib/textus/protocol/init.rb +143 -0
- data/lib/textus/protocol/key/grammar.rb +35 -0
- data/lib/textus/protocol/key/matching.rb +23 -0
- data/lib/textus/protocol/key/path.rb +23 -0
- data/lib/textus/protocol/key.rb +25 -0
- data/lib/textus/protocol/lane.rb +12 -0
- data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
- data/lib/textus/protocol/links/resolver.rb +33 -0
- data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
- data/lib/textus/protocol/links.rb +10 -0
- data/lib/textus/protocol/manifest/data.rb +90 -0
- data/lib/textus/protocol/manifest/entry/base.rb +95 -0
- data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
- data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
- data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
- data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
- data/lib/textus/protocol/manifest/entry.rb +10 -0
- data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
- data/lib/textus/protocol/manifest/policy.rb +69 -0
- data/lib/textus/protocol/manifest/resolver.rb +156 -0
- data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
- data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
- data/lib/textus/protocol/manifest/schema.rb +40 -0
- data/lib/textus/protocol/manifest.rb +62 -0
- data/lib/textus/protocol/produce/render/context.rb +43 -0
- data/lib/textus/protocol/produce/render.rb +44 -0
- data/lib/textus/protocol/schema/registry.rb +44 -0
- data/lib/textus/protocol/schema/tools.rb +104 -0
- data/lib/textus/protocol/schema.rb +117 -0
- data/lib/textus/protocol/session/bridge.rb +11 -0
- data/lib/textus/protocol/session/context.rb +5 -0
- data/lib/textus/protocol/session.rb +87 -0
- data/lib/textus/protocol/store_engine/delete.rb +37 -0
- data/lib/textus/protocol/store_engine/move.rb +109 -0
- data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
- data/lib/textus/protocol/store_engine/put.rb +104 -0
- data/lib/textus/protocol/store_engine.rb +86 -0
- data/lib/textus/protocol/v4.rb +25 -0
- data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
- data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
- data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
- data/lib/textus/protocol/verb_registry.rb +42 -0
- data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
- data/lib/textus/surface/cli/runner.rb +25 -25
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +3 -6
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +8 -6
- data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
- data/lib/textus/surface/cli.rb +25 -6
- data/lib/textus/surface/dispatch.rb +17 -0
- data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
- data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/server.rb +46 -46
- data/lib/textus/surface/watcher.rb +18 -12
- data/lib/textus/surface/web.rb +50 -0
- data/lib/textus/value/call.rb +1 -1
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/etag.rb +7 -7
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/consumer.rb +39 -0
- data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
- data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
- data/lib/textus/workflow/dsl/step_builder.rb +56 -0
- data/lib/textus/workflow/dsl.rb +111 -21
- data/lib/textus/workflow/event_emitter.rb +21 -0
- data/lib/textus/workflow/publisher.rb +59 -0
- data/lib/textus/workflow/registry.rb +31 -8
- data/lib/textus/workflow/runner.rb +25 -61
- data/lib/textus/workflow/scheduler.rb +45 -0
- data/lib/textus/workflow/step_executor.rb +78 -0
- data/lib/textus/workflow/step_helpers.rb +102 -0
- data/lib/textus/workflow.rb +2 -5
- data/lib/textus.rb +16 -80
- metadata +167 -183
- data/SPEC.md +0 -908
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -138
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/boot.rb +0 -202
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness/verdict.rb +0 -34
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/core/sentinel.rb +0 -30
- data/lib/textus/doctor/check/audit_log.rb +0 -34
- data/lib/textus/doctor/check/generator_drift.rb +0 -29
- data/lib/textus/doctor/check/illegal_keys.rb +0 -60
- data/lib/textus/doctor/check/manifest_files.rb +0 -25
- data/lib/textus/doctor/check/notebook_sources.rb +0 -50
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
- data/lib/textus/doctor/check/proposal_targets.rb +0 -45
- data/lib/textus/doctor/check/protocol_version.rb +0 -47
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
- data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
- data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
- data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
- data/lib/textus/doctor/check/schema_violations.rb +0 -28
- data/lib/textus/doctor/check/schemas.rb +0 -26
- data/lib/textus/doctor/check/sentinels.rb +0 -60
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
- data/lib/textus/doctor/check/templates.rb +0 -29
- data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
- data/lib/textus/doctor/check.rb +0 -41
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -49
- data/lib/textus/format/json.rb +0 -102
- data/lib/textus/format/markdown.rb +0 -80
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -125
- data/lib/textus/format.rb +0 -46
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/init.rb +0 -157
- data/lib/textus/jobs.rb +0 -9
- data/lib/textus/key/distance.rb +0 -55
- data/lib/textus/key/grammar.rb +0 -33
- data/lib/textus/key/matching.rb +0 -24
- data/lib/textus/key/path.rb +0 -28
- data/lib/textus/manifest/capabilities.rb +0 -29
- data/lib/textus/manifest/data.rb +0 -102
- data/lib/textus/manifest/entry/base.rb +0 -105
- data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
- data/lib/textus/manifest/entry/leaf.rb +0 -17
- data/lib/textus/manifest/entry/nested.rb +0 -37
- data/lib/textus/manifest/entry/parser.rb +0 -99
- data/lib/textus/manifest/entry/produced.rb +0 -36
- data/lib/textus/manifest/entry/publish/mode.rb +0 -45
- data/lib/textus/manifest/entry/publish/none.rb +0 -14
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
- data/lib/textus/manifest/entry/publish/template.rb +0 -16
- data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
- data/lib/textus/manifest/entry/publish/tree.rb +0 -50
- data/lib/textus/manifest/entry/publish.rb +0 -45
- data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
- data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
- data/lib/textus/manifest/entry/validators/publish.rb +0 -30
- data/lib/textus/manifest/entry/validators.rb +0 -18
- data/lib/textus/manifest/entry.rb +0 -8
- data/lib/textus/manifest/policy/matcher.rb +0 -51
- data/lib/textus/manifest/policy/publish_target.rb +0 -34
- data/lib/textus/manifest/policy/react.rb +0 -30
- data/lib/textus/manifest/policy/retention.rb +0 -29
- data/lib/textus/manifest/policy/source.rb +0 -30
- data/lib/textus/manifest/policy.rb +0 -95
- data/lib/textus/manifest/resolver.rb +0 -118
- data/lib/textus/manifest/rules.rb +0 -86
- data/lib/textus/manifest/schema/contract.rb +0 -61
- data/lib/textus/manifest/schema/keys.rb +0 -90
- data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
- data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
- data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
- data/lib/textus/manifest/schema/semantics.rb +0 -27
- data/lib/textus/manifest/schema/validator.rb +0 -48
- data/lib/textus/manifest/schema/vocabulary.rb +0 -25
- data/lib/textus/manifest/schema.rb +0 -30
- data/lib/textus/manifest.rb +0 -65
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/port/audit_log.rb +0 -268
- data/lib/textus/port/build_lock.rb +0 -64
- data/lib/textus/port/clock.rb +0 -10
- data/lib/textus/port/publisher.rb +0 -60
- data/lib/textus/port/sentinel_store.rb +0 -83
- data/lib/textus/port/storage/file_stat.rb +0 -19
- data/lib/textus/port/storage/file_store.rb +0 -26
- data/lib/textus/port/store.rb +0 -93
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -50
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/tools.rb +0 -115
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/container.rb +0 -43
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/envelope/reader.rb +0 -43
- data/lib/textus/store/envelope/writer.rb +0 -195
- data/lib/textus/store/index/builder.rb +0 -74
- data/lib/textus/store/index/lookup.rb +0 -60
- data/lib/textus/store/jobs/base.rb +0 -13
- data/lib/textus/store/jobs/index.rb +0 -15
- data/lib/textus/store/jobs/materialize.rb +0 -15
- data/lib/textus/store/jobs/plan.rb +0 -11
- data/lib/textus/store/jobs/planner.rb +0 -104
- data/lib/textus/store/jobs/queue.rb +0 -154
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention.rb +0 -50
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/store.rb +0 -99
- data/lib/textus/surface/cli/verb/doctor.rb +0 -19
- data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
- data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
- data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -89
- data/lib/textus/value/result.rb +0 -26
- data/lib/textus/workflow/collector.rb +0 -27
- data/lib/textus/workflow/context.rb +0 -5
- data/lib/textus/workflow/loader.rb +0 -17
- data/lib/textus/workflow/pattern.rb +0 -18
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
class Base
|
|
5
|
+
def self.parse(_raw, path: nil)
|
|
6
|
+
_ = path
|
|
7
|
+
raise NotImplementedError.new("#{name}.parse not implemented")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.serialize(meta: {}, body: "", content: nil)
|
|
11
|
+
_ = meta
|
|
12
|
+
_ = body
|
|
13
|
+
_ = content
|
|
14
|
+
raise NotImplementedError.new("#{name}.serialize not implemented")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.extensions
|
|
18
|
+
raise NotImplementedError.new("#{name}.extensions not implemented")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.validate_against(schema, parsed)
|
|
22
|
+
schema.validate!(parsed["_meta"] || {})
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.nested_glob
|
|
26
|
+
raise NotImplementedError.new("#{name}.nested_glob not implemented")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.validate_path_extension(path, nested)
|
|
30
|
+
ext = File.extname(path)
|
|
31
|
+
if nested
|
|
32
|
+
return if ext == ""
|
|
33
|
+
|
|
34
|
+
raise UsageError.new("#{format_name} nested path must not have an extension")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
return if extensions.include?(ext)
|
|
38
|
+
|
|
39
|
+
raise UsageError.new("#{format_name} format requires '#{extensions.join("' or '")}' path (got #{ext.inspect})")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.enforce_name_match!(path, meta)
|
|
43
|
+
return unless meta.is_a?(Hash) && meta["name"]
|
|
44
|
+
|
|
45
|
+
ext = extensions.first
|
|
46
|
+
basename = File.basename(path, ext)
|
|
47
|
+
return if meta["name"] == basename
|
|
48
|
+
|
|
49
|
+
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
53
|
+
raw = File.binread(path)
|
|
54
|
+
parsed = parse(raw, path: path)
|
|
55
|
+
meta = parsed["_meta"] || {}
|
|
56
|
+
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
57
|
+
|
|
58
|
+
new_meta = meta.merge("name" => basename)
|
|
59
|
+
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"] || "", content: parsed["content"]))
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.format_name
|
|
64
|
+
name.split("::").last.downcase
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.validate_raw_entry!(_parsed, _lane); end
|
|
68
|
+
|
|
69
|
+
def self.nested_ext(_segment = nil) = extensions.first
|
|
70
|
+
|
|
71
|
+
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
72
|
+
_ = path
|
|
73
|
+
_ = content
|
|
74
|
+
bytes = serialize(meta: meta || {}, body: body.to_s)
|
|
75
|
+
[bytes, meta, body.to_s, nil]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Format
|
|
6
|
+
class Json < Base
|
|
7
|
+
def self.parse(raw, path: nil)
|
|
8
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
9
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
parsed = ::JSON.parse(raw)
|
|
13
|
+
rescue ::JSON::ParserError => e
|
|
14
|
+
raise BadFrontmatter.new(path, "JSON parse failed: #{e.message}")
|
|
15
|
+
end
|
|
16
|
+
raise BadFrontmatter.new(path, "JSON top-level must be an object") unless parsed.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
meta = parsed["_meta"]
|
|
19
|
+
fm = meta.is_a?(Hash) ? meta : {}
|
|
20
|
+
content_without_meta = parsed.except("_meta")
|
|
21
|
+
{ "_meta" => fm, "body" => raw, "content" => content_without_meta }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.serialize(meta:, body:, content: nil)
|
|
25
|
+
if content.is_a?(Hash)
|
|
26
|
+
on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
|
|
27
|
+
out = ::JSON.pretty_generate(on_disk)
|
|
28
|
+
out += "\n" unless out.end_with?("\n")
|
|
29
|
+
out
|
|
30
|
+
elsif body && !body.to_s.empty?
|
|
31
|
+
b = body.to_s
|
|
32
|
+
b += "\n" unless b.end_with?("\n")
|
|
33
|
+
b
|
|
34
|
+
else
|
|
35
|
+
raise UsageError.new("json serialize requires :content or :body")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.validate_against(schema, parsed)
|
|
40
|
+
schema.validate!(parsed["content"] || {})
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.extensions = [".json"]
|
|
44
|
+
def self.nested_glob = "**/*.json"
|
|
45
|
+
|
|
46
|
+
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
47
|
+
raise UsageError.new("put for json requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
48
|
+
|
|
49
|
+
if content.nil?
|
|
50
|
+
begin
|
|
51
|
+
parsed = parse(body.to_s, path: path)
|
|
52
|
+
rescue BadFrontmatter => e
|
|
53
|
+
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
[body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
|
|
56
|
+
else
|
|
57
|
+
bytes = serialize(meta: meta, body: "", content: content)
|
|
58
|
+
[bytes, meta, bytes, content]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.validate_path_extension(path, nested)
|
|
63
|
+
ext = File.extname(path)
|
|
64
|
+
if nested
|
|
65
|
+
return if ext == ""
|
|
66
|
+
|
|
67
|
+
raise UsageError.new("nested json path must not have an extension")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return if ext == ".json"
|
|
71
|
+
|
|
72
|
+
raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.data_to_payload(data)
|
|
76
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
77
|
+
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Format
|
|
6
|
+
class Markdown < Base
|
|
7
|
+
def self.parse(raw, path: nil)
|
|
8
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
9
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
10
|
+
return { "_meta" => {}, "body" => raw, "content" => nil } unless raw.start_with?("---\n") || raw.start_with?("---\r\n")
|
|
11
|
+
|
|
12
|
+
lines = raw.split(/\r?\n/, -1)
|
|
13
|
+
close_idx = lines[1..].index("---")
|
|
14
|
+
raise BadFrontmatter.new(path, "frontmatter not terminated") unless close_idx
|
|
15
|
+
|
|
16
|
+
close_idx += 1
|
|
17
|
+
fm_yaml = lines[1...close_idx].join("\n")
|
|
18
|
+
body = lines[(close_idx + 1)..].join("\n")
|
|
19
|
+
begin
|
|
20
|
+
fm = fm_yaml.strip.empty? ? {} : ::YAML.safe_load(fm_yaml, permitted_classes: [Date, Time], aliases: false)
|
|
21
|
+
rescue Psych::SyntaxError => e
|
|
22
|
+
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
23
|
+
end
|
|
24
|
+
fm = {} unless fm.is_a?(Hash)
|
|
25
|
+
{ "_meta" => fm, "body" => body, "content" => nil }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.serialize(meta:, body:, content: nil)
|
|
29
|
+
_ = content
|
|
30
|
+
fm_yaml = meta.empty? ? "" : ::YAML.dump(meta).sub(/\A---\n/, "")
|
|
31
|
+
body = body.to_s
|
|
32
|
+
body += "\n" unless body.empty? || body.end_with?("\n")
|
|
33
|
+
"---\n#{fm_yaml}---\n#{body}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.extensions = [".md"]
|
|
37
|
+
def self.nested_glob = "**/*.md"
|
|
38
|
+
|
|
39
|
+
def self.validate_path_extension(path, _nested)
|
|
40
|
+
ext = File.extname(path)
|
|
41
|
+
return if ["", ".md"].include?(ext)
|
|
42
|
+
|
|
43
|
+
raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.data_to_payload(data)
|
|
47
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
48
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
class Script < Base
|
|
5
|
+
EXTENSIONS = %w[.rb .py .sh .js .pl .rs .exs].freeze
|
|
6
|
+
|
|
7
|
+
def self.parse(raw, path: nil)
|
|
8
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
9
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
10
|
+
|
|
11
|
+
{ "_meta" => {}, "body" => raw, "content" => nil }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.serialize(meta:, body:, content: nil)
|
|
15
|
+
_ = meta
|
|
16
|
+
_ = content
|
|
17
|
+
b = body.to_s
|
|
18
|
+
b += "\n" unless b.empty? || b.end_with?("\n")
|
|
19
|
+
b
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.extensions = EXTENSIONS
|
|
23
|
+
def self.nested_glob = "**/*"
|
|
24
|
+
def self.nested_ext(*) = ""
|
|
25
|
+
def self.enforce_name_match!(_path, _meta); end
|
|
26
|
+
|
|
27
|
+
def self.rewrite_name(_path, _basename)
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.validate_path_extension(path, nested)
|
|
32
|
+
ext = File.extname(path)
|
|
33
|
+
if nested
|
|
34
|
+
return if ext == ""
|
|
35
|
+
|
|
36
|
+
raise UsageError.new("nested script path must not have an extension")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return if EXTENSIONS.include?(ext) || ext == ""
|
|
40
|
+
|
|
41
|
+
raise UsageError.new("script format requires #{EXTENSIONS.join(" | ")} extension (got #{ext.inspect})")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.validate_raw_entry!(_parsed, lane)
|
|
45
|
+
nil unless lane == "scratchpad"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.data_to_payload(data)
|
|
49
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
50
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
class Text < Base
|
|
5
|
+
def self.parse(raw, path: nil)
|
|
6
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
7
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
8
|
+
|
|
9
|
+
{ "_meta" => {}, "body" => raw, "content" => nil }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.serialize(meta:, body:, content: nil)
|
|
13
|
+
_ = meta
|
|
14
|
+
_ = content
|
|
15
|
+
b = body.to_s
|
|
16
|
+
b += "\n" unless b.empty? || b.end_with?("\n")
|
|
17
|
+
b
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.extensions = [".txt"]
|
|
21
|
+
def self.nested_glob = "**/*.txt"
|
|
22
|
+
def self.enforce_name_match!(_path, _meta); end
|
|
23
|
+
|
|
24
|
+
def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.validate_path_extension(path, nested)
|
|
29
|
+
ext = File.extname(path)
|
|
30
|
+
if nested
|
|
31
|
+
return if ext == ""
|
|
32
|
+
|
|
33
|
+
raise UsageError.new("nested text path must not have an extension")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return if [".txt", ""].include?(ext)
|
|
37
|
+
|
|
38
|
+
raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.data_to_payload(data)
|
|
42
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
43
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Format
|
|
6
|
+
class Yaml < Base
|
|
7
|
+
def self.parse(raw, path: nil)
|
|
8
|
+
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
9
|
+
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
|
|
13
|
+
rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
|
|
14
|
+
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
15
|
+
end
|
|
16
|
+
raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
meta = parsed["_meta"]
|
|
19
|
+
fm = meta.is_a?(Hash) ? meta : {}
|
|
20
|
+
content_without_meta = parsed.except("_meta")
|
|
21
|
+
{ "_meta" => fm, "body" => raw, "content" => content_without_meta }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.serialize(meta:, body:, content: nil)
|
|
25
|
+
if content.is_a?(Hash)
|
|
26
|
+
on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
|
|
27
|
+
::YAML.dump(on_disk).sub(/\A---\n/, "")
|
|
28
|
+
elsif body && !body.to_s.empty?
|
|
29
|
+
b = body.to_s
|
|
30
|
+
b += "\n" unless b.end_with?("\n")
|
|
31
|
+
b
|
|
32
|
+
else
|
|
33
|
+
raise UsageError.new("yaml serialize requires :content or :body")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.validate_against(schema, parsed)
|
|
38
|
+
schema.validate!(parsed["content"] || {})
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
RAW_REQUIRED = %w[ingested_at content_hash].freeze
|
|
42
|
+
RAW_SOURCE_KINDS = %w[url file asset].freeze
|
|
43
|
+
|
|
44
|
+
def self.validate_raw_entry!(parsed, lane)
|
|
45
|
+
return unless lane == "raw"
|
|
46
|
+
|
|
47
|
+
content = parsed["content"] || {}
|
|
48
|
+
return if content["superseded_by"]
|
|
49
|
+
|
|
50
|
+
missing = RAW_REQUIRED.reject { |f| content[f] }
|
|
51
|
+
raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
|
|
52
|
+
|
|
53
|
+
source = content["source"] || {}
|
|
54
|
+
kind = source["kind"]
|
|
55
|
+
unless RAW_SOURCE_KINDS.include?(kind)
|
|
56
|
+
raise Textus::BadContent.new(
|
|
57
|
+
nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return unless kind == "url" && !source["url"]
|
|
62
|
+
|
|
63
|
+
raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.extensions = [".yaml", ".yml"]
|
|
67
|
+
def self.nested_glob = "**/*.{yaml,yml}"
|
|
68
|
+
|
|
69
|
+
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
70
|
+
raise UsageError.new("put for yaml requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
|
|
71
|
+
|
|
72
|
+
if content.nil?
|
|
73
|
+
begin
|
|
74
|
+
parsed = parse(body.to_s, path: path)
|
|
75
|
+
rescue BadFrontmatter => e
|
|
76
|
+
raise BadContent.new(path, "bad_content: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
[body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
|
|
79
|
+
else
|
|
80
|
+
bytes = serialize(meta: meta, body: "", content: content)
|
|
81
|
+
[bytes, meta, bytes, content]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.validate_path_extension(path, nested)
|
|
86
|
+
ext = File.extname(path)
|
|
87
|
+
if nested
|
|
88
|
+
return if ext == ""
|
|
89
|
+
|
|
90
|
+
raise UsageError.new("nested yaml path must not have an extension")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
return if [".yaml", ".yml"].include?(ext)
|
|
94
|
+
|
|
95
|
+
raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.data_to_payload(data)
|
|
99
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
100
|
+
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Format
|
|
4
|
+
SEP = "---".freeze
|
|
5
|
+
|
|
6
|
+
STRATEGIES = {
|
|
7
|
+
"markdown" => -> { Protocol::Format::Markdown },
|
|
8
|
+
"json" => -> { Protocol::Format::Json },
|
|
9
|
+
"yaml" => -> { Protocol::Format::Yaml },
|
|
10
|
+
"text" => -> { Protocol::Format::Text },
|
|
11
|
+
"script" => -> { Protocol::Format::Script },
|
|
12
|
+
"bash" => -> { Protocol::Format::Bash },
|
|
13
|
+
"ruby" => -> { Protocol::Format::Ruby },
|
|
14
|
+
"python" => -> { Protocol::Format::Python },
|
|
15
|
+
"javascript" => -> { Protocol::Format::Javascript },
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
@registry = nil
|
|
19
|
+
|
|
20
|
+
def self.registry=(reg)
|
|
21
|
+
@registry = reg
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.registry
|
|
25
|
+
@registry
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
EXT_TO_FORMAT = {
|
|
29
|
+
".md" => "markdown",
|
|
30
|
+
".json" => "json",
|
|
31
|
+
".yaml" => "yaml",
|
|
32
|
+
".yml" => "yaml",
|
|
33
|
+
".txt" => "text",
|
|
34
|
+
".rb" => "ruby",
|
|
35
|
+
".py" => "python",
|
|
36
|
+
".sh" => "bash",
|
|
37
|
+
".js" => "javascript",
|
|
38
|
+
".pl" => "script",
|
|
39
|
+
".rs" => "script",
|
|
40
|
+
".exs" => "script",
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def self.for(format)
|
|
44
|
+
key = format.to_s
|
|
45
|
+
return registry.fetch(key).call if registry&.key?(key)
|
|
46
|
+
|
|
47
|
+
STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.infer_from_extension(ext)
|
|
51
|
+
EXT_TO_FORMAT[ext]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.formats
|
|
55
|
+
EXT_TO_FORMAT.values.uniq
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.parse(raw, path: nil, format: "markdown")
|
|
59
|
+
Protocol::Format.for(format).parse(raw, path: path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
|
|
63
|
+
Protocol::Format.for(format).serialize(meta: meta, body: body, content: content)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.data_to_payload(data, format)
|
|
67
|
+
return { meta: {}, body: "", content: nil } if data.nil?
|
|
68
|
+
|
|
69
|
+
Protocol::Format.for(format).data_to_payload(data)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
module Freshness
|
|
6
|
+
class TtlEvaluator
|
|
7
|
+
def initialize(manifest:, file_stat:, clock:)
|
|
8
|
+
@manifest = manifest
|
|
9
|
+
@file_stat = file_stat
|
|
10
|
+
@clock = clock
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def verdict(mentry)
|
|
14
|
+
ttl = lane_ttl(mentry.lane)
|
|
15
|
+
return fresh if ttl.nil?
|
|
16
|
+
|
|
17
|
+
stale = age_stale?(file_basis(mentry), ttl)
|
|
18
|
+
Verdict.build(stale: stale, reason: stale ? "ttl exceeded" : nil, fetching: false)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stale_keys(prefix: nil, lane: nil)
|
|
22
|
+
@manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
|
|
28
|
+
|
|
29
|
+
def file_basis(mentry)
|
|
30
|
+
path = @manifest.resolver.resolve(mentry.key).path
|
|
31
|
+
return nil unless @file_stat.exists?(path)
|
|
32
|
+
|
|
33
|
+
@file_stat.mtime(path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def lane_ttl(lane)
|
|
37
|
+
Textus::Protocol::V4::LANES.dig(lane, :retention_ttl)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def due?(mentry, prefix:, lane:)
|
|
41
|
+
return false if lane && mentry.lane != lane
|
|
42
|
+
return false if prefix && !mentry.key.start_with?(prefix)
|
|
43
|
+
|
|
44
|
+
ttl = lane_ttl(mentry.lane)
|
|
45
|
+
return false if ttl.nil?
|
|
46
|
+
|
|
47
|
+
path = @manifest.resolver.resolve(mentry.key).path
|
|
48
|
+
return true unless @file_stat.exists?(path)
|
|
49
|
+
|
|
50
|
+
age_stale?(file_basis(mentry), ttl)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def age_stale?(basis, ttl)
|
|
54
|
+
return true if basis.nil?
|
|
55
|
+
|
|
56
|
+
(@clock.now - basis).to_i > ttl
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Gate
|
|
4
|
+
HANDLER_GROUPS = {
|
|
5
|
+
read: Handlers::Read,
|
|
6
|
+
write: Handlers::Write,
|
|
7
|
+
maintenance: Handlers::System,
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(manifest:, resolver:, container:, store_engine:, policy: nil, async: nil)
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
@resolver = resolver
|
|
13
|
+
@container = container
|
|
14
|
+
@store_engine = store_engine
|
|
15
|
+
@async = async
|
|
16
|
+
@policy = policy || manifest.policy
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dispatch(role, verb, params = {}, call: nil)
|
|
20
|
+
spec = VerbRegistry.for(verb)
|
|
21
|
+
return error(:unknown_verb, "Unknown verb: #{verb}") unless spec
|
|
22
|
+
|
|
23
|
+
key = params[:key]
|
|
24
|
+
authorize!(role, verb, key, spec)
|
|
25
|
+
|
|
26
|
+
call_obj = call || Value::Call.build(role:, correlation_id: SecureRandom.uuid)
|
|
27
|
+
bound = Protocol::Binder.bind(spec, params)
|
|
28
|
+
ctx_class = spec.read? && !spec.system? ? Handlers::QueryContext : Handlers::CommandContext
|
|
29
|
+
ctx = ctx_class.new(@store_engine, @container, call_obj)
|
|
30
|
+
|
|
31
|
+
handler = spec.lane ? Lane.handler_for(spec.lane) : handler_group(spec)
|
|
32
|
+
handler.public_send(verb, **bound, ctx:, call: call_obj)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def handler_group(spec)
|
|
38
|
+
return Handlers::System if spec.system?
|
|
39
|
+
|
|
40
|
+
HANDLER_GROUPS[spec.category] || Handlers::System
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def authorize!(role, _verb, key, spec)
|
|
44
|
+
return true unless spec&.write?
|
|
45
|
+
|
|
46
|
+
key_lane = key&.split(".")&.first
|
|
47
|
+
lane = key_lane || spec.lane&.to_s
|
|
48
|
+
return true unless lane
|
|
49
|
+
|
|
50
|
+
lane_map = Textus::Protocol::V4::LANES[lane]
|
|
51
|
+
return true unless lane_map
|
|
52
|
+
|
|
53
|
+
@policy.allowed!(role:, lane:, key:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def error(code, message)
|
|
57
|
+
{ error: { code: code.to_s, message: } }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Handlers
|
|
4
|
+
class CommandContext < QueryContext
|
|
5
|
+
# StoreEngine delegation (write side)
|
|
6
|
+
def put(...) = @store_engine.put(...)
|
|
7
|
+
def delete(...) = @store_engine.delete(...)
|
|
8
|
+
def move(...) = @store_engine.move(...)
|
|
9
|
+
|
|
10
|
+
# Infrastructure (ops, drain, pulse)
|
|
11
|
+
def audit_log = @container.store.audit_log
|
|
12
|
+
def workflow_queue = @container.store.workflow_queue
|
|
13
|
+
def file_system = @container.store.file_system
|
|
14
|
+
def workflows = @container.workflows
|
|
15
|
+
def workflow_registry = @container.workflow_registry
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|