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
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
class Check
|
|
4
|
-
class UnownedSchemaFields < Check
|
|
5
|
-
def call
|
|
6
|
-
dir = geometry.schemas_dir
|
|
7
|
-
return [] unless File.directory?(dir)
|
|
8
|
-
|
|
9
|
-
Dir.glob(File.join(dir, "*.yaml")).flat_map do |path|
|
|
10
|
-
issues_for(path)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def issues_for(path)
|
|
17
|
-
schema = safe_load(path)
|
|
18
|
-
return [] if schema.nil?
|
|
19
|
-
|
|
20
|
-
unowned = schema.unowned_fields
|
|
21
|
-
return [] if unowned.empty?
|
|
22
|
-
|
|
23
|
-
[{
|
|
24
|
-
"code" => "schema.unowned_fields",
|
|
25
|
-
"level" => "info",
|
|
26
|
-
"subject" => schema.name || File.basename(path, ".yaml"),
|
|
27
|
-
"message" => "schema has fields without maintained_by: #{unowned.join(", ")}",
|
|
28
|
-
"fix" => "add 'maintained_by: <role>' to each field in #{path} (optional but recommended)",
|
|
29
|
-
}]
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def safe_load(path)
|
|
33
|
-
Schema.load(path)
|
|
34
|
-
rescue StandardError
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
data/lib/textus/doctor/check.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Doctor
|
|
3
|
-
# Abstract base for a single doctor check. Each concrete check inspects
|
|
4
|
-
# one slice of store health and returns an array of issue hashes:
|
|
5
|
-
# { "code" => String, "level" => "error"|"warning"|"info",
|
|
6
|
-
# "subject" => String, "message" => String, "fix" => String (optional) }
|
|
7
|
-
class Check
|
|
8
|
-
# Snake-case name used in --checks flag and ALL_CHECKS list. Default
|
|
9
|
-
# derives from the class name; override if the SPEC name diverges.
|
|
10
|
-
def self.name_key
|
|
11
|
-
@name_key ||= name.split("::").last
|
|
12
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
13
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
14
|
-
.downcase
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def initialize(container, role: Textus::Value::Role::DEFAULT)
|
|
18
|
-
@container = container
|
|
19
|
-
@role = role
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call
|
|
23
|
-
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
protected
|
|
27
|
-
|
|
28
|
-
def root = @container.root
|
|
29
|
-
def geometry = @container.geometry
|
|
30
|
-
def manifest = @container.manifest
|
|
31
|
-
|
|
32
|
-
# Dispatch a verb through Gate.
|
|
33
|
-
def dispatch(verb, *args, **kwargs)
|
|
34
|
-
klass = Textus::Action::VERBS[verb]
|
|
35
|
-
spec = klass.contract if klass.respond_to?(:contract?) && klass.contract?
|
|
36
|
-
inputs = spec ? Textus::Gate::Binder.inputs_from_ordered(spec, args, kwargs) : kwargs
|
|
37
|
-
@container.gate.dispatch(spec:, inputs:, role: @role)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Doctor
|
|
5
|
-
class Validator
|
|
6
|
-
def initialize(reader:, manifest:, audit_log:, schema_for:)
|
|
7
|
-
@reader = reader
|
|
8
|
-
@manifest = manifest
|
|
9
|
-
@audit_log = audit_log
|
|
10
|
-
@schema_for = schema_for
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def call(container:, call:)
|
|
14
|
-
@container = container
|
|
15
|
-
@call = call
|
|
16
|
-
violations = []
|
|
17
|
-
check_content_violations(violations)
|
|
18
|
-
check_role_authority_violations(violations)
|
|
19
|
-
{ "protocol" => Textus::PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def check_content_violations(violations)
|
|
25
|
-
@manifest.resolver.enumerate.each do |row|
|
|
26
|
-
key = row[:key]
|
|
27
|
-
mentry = row[:manifest_entry]
|
|
28
|
-
env = fetch_envelope(key, violations) or next
|
|
29
|
-
schema = mentry.schema && @schema_for.call(mentry.schema)
|
|
30
|
-
next unless schema
|
|
31
|
-
|
|
32
|
-
begin
|
|
33
|
-
validate_schema!(schema, env, mentry.format)
|
|
34
|
-
rescue Textus::Error => e
|
|
35
|
-
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def check_role_authority_violations(violations)
|
|
41
|
-
@manifest.resolver.enumerate.each do |row|
|
|
42
|
-
mentry = row[:manifest_entry]
|
|
43
|
-
next unless mentry.schema
|
|
44
|
-
|
|
45
|
-
schema = @schema_for.call(mentry.schema)
|
|
46
|
-
next unless schema
|
|
47
|
-
|
|
48
|
-
env = begin
|
|
49
|
-
@reader.call(row[:key], @container, @call)
|
|
50
|
-
rescue StandardError
|
|
51
|
-
next
|
|
52
|
-
end
|
|
53
|
-
append_authority_violations(violations, row[:key], env, schema)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def append_authority_violations(violations, key, env, schema)
|
|
58
|
-
last_writer = @audit_log.last_writer_for(key)
|
|
59
|
-
return if last_writer.nil?
|
|
60
|
-
|
|
61
|
-
last_writer_is_authority =
|
|
62
|
-
@manifest.policy.roles_with_capability("author").include?(last_writer)
|
|
63
|
-
env.meta.each_key do |field|
|
|
64
|
-
owner = schema.maintained_by(field)
|
|
65
|
-
next if owner.nil? || last_writer == owner || last_writer_is_authority
|
|
66
|
-
|
|
67
|
-
violations << {
|
|
68
|
-
"key" => key,
|
|
69
|
-
"code" => "role_authority",
|
|
70
|
-
"field" => field,
|
|
71
|
-
"expected" => owner,
|
|
72
|
-
"last_writer" => last_writer,
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def fetch_envelope(key, violations)
|
|
78
|
-
@reader.call(key, @container, @call)
|
|
79
|
-
rescue Textus::Error => e
|
|
80
|
-
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
81
|
-
nil
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def validate_schema!(schema, envelope, format)
|
|
85
|
-
payload = case format
|
|
86
|
-
when "json", "yaml" then envelope.content || {}
|
|
87
|
-
else envelope.meta || {}
|
|
88
|
-
end
|
|
89
|
-
schema.validate!(payload)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
data/lib/textus/doctor.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Health check for a Textus store. Returns a JSON-friendly Hash envelope
|
|
3
|
-
# with an `issues` array and a summary. Each issue is a Hash with
|
|
4
|
-
# `code`, `level`, `subject`, `message`, and optionally `fix`.
|
|
5
|
-
module Doctor
|
|
6
|
-
LEVELS = %w[error warning info].freeze
|
|
7
|
-
|
|
8
|
-
CHECKS = [
|
|
9
|
-
Check::ProtocolVersion,
|
|
10
|
-
Check::ManifestFiles,
|
|
11
|
-
Check::Schemas,
|
|
12
|
-
Check::SchemaParseError,
|
|
13
|
-
Check::Templates,
|
|
14
|
-
Check::IllegalKeys,
|
|
15
|
-
Check::Sentinels,
|
|
16
|
-
Check::AuditLog,
|
|
17
|
-
Check::UnownedSchemaFields,
|
|
18
|
-
Check::SchemaViolations,
|
|
19
|
-
Check::RuleAmbiguity,
|
|
20
|
-
Check::OrphanedPublishTargets,
|
|
21
|
-
Check::PublishTreeIndexOverlap,
|
|
22
|
-
Check::ProposalTargets,
|
|
23
|
-
Check::GeneratorDrift,
|
|
24
|
-
Check::RawAssetPaths,
|
|
25
|
-
Check::NotebookSources,
|
|
26
|
-
Check::StaleReviewedStamp,
|
|
27
|
-
].freeze
|
|
28
|
-
|
|
29
|
-
ALL_CHECKS = CHECKS.map(&:name_key).freeze
|
|
30
|
-
|
|
31
|
-
module_function
|
|
32
|
-
|
|
33
|
-
def build(container:, checks: nil, role: Textus::Value::Role::DEFAULT)
|
|
34
|
-
selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
|
|
35
|
-
unknown = selected_keys - ALL_CHECKS
|
|
36
|
-
unless unknown.empty?
|
|
37
|
-
raise UsageError.new(
|
|
38
|
-
"unknown doctor check: #{unknown.first}. Valid checks: #{ALL_CHECKS.join(", ")}",
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
|
|
43
|
-
issues = selected.flat_map { |c| c.new(container, role:).call }
|
|
44
|
-
|
|
45
|
-
summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
|
|
46
|
-
{
|
|
47
|
-
"protocol" => Textus::PROTOCOL,
|
|
48
|
-
"ok" => summary["error"].zero?,
|
|
49
|
-
"issues" => issues,
|
|
50
|
-
"summary" => summary,
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
data/lib/textus/format/base.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
class Base
|
|
4
|
-
def self.parse(_raw, path: nil)
|
|
5
|
-
_ = path
|
|
6
|
-
raise NotImplementedError.new("#{name}.parse not implemented")
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def self.serialize(meta: {}, body: "", content: nil)
|
|
10
|
-
_ = meta
|
|
11
|
-
_ = body
|
|
12
|
-
_ = content
|
|
13
|
-
raise NotImplementedError.new("#{name}.serialize not implemented")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.extensions
|
|
17
|
-
raise NotImplementedError.new("#{name}.extensions not implemented")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.validate_against(schema, parsed)
|
|
21
|
-
schema.validate!(parsed["_meta"] || {})
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.nested_glob
|
|
25
|
-
raise NotImplementedError.new("#{name}.nested_glob not implemented")
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.validate_path_extension(_path, _nested)
|
|
29
|
-
raise NotImplementedError.new("#{name}.validate_path_extension not implemented")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.enforce_name_match!(_path, _meta)
|
|
33
|
-
raise NotImplementedError.new("#{name}.enforce_name_match! not implemented")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
37
|
-
_ = meta
|
|
38
|
-
_ = body
|
|
39
|
-
_ = content
|
|
40
|
-
_ = path
|
|
41
|
-
raise NotImplementedError.new("#{name}.serialize_for_put not implemented")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def self.rewrite_name(_path, _basename)
|
|
45
|
-
raise NotImplementedError.new("#{name}.rewrite_name not implemented")
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
data/lib/textus/format/json.rb
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Format
|
|
5
|
-
class Json < Base
|
|
6
|
-
def self.parse(raw, path: nil)
|
|
7
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
8
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
9
|
-
|
|
10
|
-
begin
|
|
11
|
-
parsed = ::JSON.parse(raw)
|
|
12
|
-
rescue ::JSON::ParserError => e
|
|
13
|
-
raise BadFrontmatter.new(path, "JSON parse failed: #{e.message}")
|
|
14
|
-
end
|
|
15
|
-
raise BadFrontmatter.new(path, "JSON top-level must be an object") unless parsed.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
meta = parsed["_meta"]
|
|
18
|
-
fm = meta.is_a?(Hash) ? meta : {}
|
|
19
|
-
content_without_meta = parsed.except("_meta")
|
|
20
|
-
{ "_meta" => fm, "body" => raw, "content" => content_without_meta }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.serialize(meta:, body:, content: nil)
|
|
24
|
-
if content.is_a?(Hash)
|
|
25
|
-
on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
|
|
26
|
-
out = ::JSON.pretty_generate(on_disk)
|
|
27
|
-
out += "\n" unless out.end_with?("\n")
|
|
28
|
-
out
|
|
29
|
-
elsif body && !body.to_s.empty?
|
|
30
|
-
b = body.to_s
|
|
31
|
-
b += "\n" unless b.end_with?("\n")
|
|
32
|
-
b
|
|
33
|
-
else
|
|
34
|
-
raise UsageError.new("json serialize requires :content or :body")
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.validate_against(schema, parsed)
|
|
39
|
-
schema.validate!(parsed["content"] || {})
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.extensions = [".json"]
|
|
43
|
-
|
|
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.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
63
|
-
raw = File.binread(path)
|
|
64
|
-
parsed = parse(raw, path: path)
|
|
65
|
-
meta = parsed["_meta"]
|
|
66
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
67
|
-
|
|
68
|
-
new_meta = meta.merge("name" => basename)
|
|
69
|
-
File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
70
|
-
true
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def self.enforce_name_match!(path, meta)
|
|
74
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
75
|
-
|
|
76
|
-
ext = extensions.first
|
|
77
|
-
basename = File.basename(path, ext)
|
|
78
|
-
return if meta["name"] == basename
|
|
79
|
-
|
|
80
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.validate_path_extension(path, nested)
|
|
84
|
-
ext = File.extname(path)
|
|
85
|
-
if nested
|
|
86
|
-
return if ext == ""
|
|
87
|
-
|
|
88
|
-
raise UsageError.new("nested json path must not have an extension")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
return if ext == ".json"
|
|
92
|
-
|
|
93
|
-
raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def self.data_to_payload(data)
|
|
97
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
98
|
-
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Format
|
|
5
|
-
class Markdown < Base
|
|
6
|
-
def self.parse(raw, path: nil)
|
|
7
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
8
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
9
|
-
return { "_meta" => {}, "body" => raw, "content" => nil } unless raw.start_with?("---\n") || raw.start_with?("---\r\n")
|
|
10
|
-
|
|
11
|
-
lines = raw.split(/\r?\n/, -1)
|
|
12
|
-
close_idx = lines[1..].index("---")
|
|
13
|
-
raise BadFrontmatter.new(path, "frontmatter not terminated") unless close_idx
|
|
14
|
-
|
|
15
|
-
close_idx += 1
|
|
16
|
-
fm_yaml = lines[1...close_idx].join("\n")
|
|
17
|
-
body = lines[(close_idx + 1)..].join("\n")
|
|
18
|
-
begin
|
|
19
|
-
fm = fm_yaml.strip.empty? ? {} : ::YAML.safe_load(fm_yaml, permitted_classes: [Date, Time], aliases: false)
|
|
20
|
-
rescue Psych::SyntaxError => e
|
|
21
|
-
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
22
|
-
end
|
|
23
|
-
fm = {} unless fm.is_a?(Hash)
|
|
24
|
-
{ "_meta" => fm, "body" => body, "content" => nil }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.serialize(meta:, body:, content: nil)
|
|
28
|
-
_ = content
|
|
29
|
-
fm_yaml = meta.empty? ? "" : ::YAML.dump(meta).sub(/\A---\n/, "")
|
|
30
|
-
body = body.to_s
|
|
31
|
-
body += "\n" unless body.empty? || body.end_with?("\n")
|
|
32
|
-
"---\n#{fm_yaml}---\n#{body}"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.extensions = [".md"]
|
|
36
|
-
|
|
37
|
-
def self.nested_glob = "**/*.md"
|
|
38
|
-
|
|
39
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
40
|
-
_ = path
|
|
41
|
-
_ = content
|
|
42
|
-
bytes = serialize(meta: meta || {}, body: body.to_s)
|
|
43
|
-
[bytes, meta, body.to_s, nil]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
47
|
-
raw = File.binread(path)
|
|
48
|
-
parsed = parse(raw, path: path)
|
|
49
|
-
meta = parsed["_meta"] || {}
|
|
50
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
51
|
-
|
|
52
|
-
new_meta = meta.merge("name" => basename)
|
|
53
|
-
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"]))
|
|
54
|
-
true
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.enforce_name_match!(path, meta)
|
|
58
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
59
|
-
|
|
60
|
-
ext = extensions.first
|
|
61
|
-
basename = File.basename(path, ext)
|
|
62
|
-
return if meta["name"] == basename
|
|
63
|
-
|
|
64
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.validate_path_extension(path, _nested)
|
|
68
|
-
ext = File.extname(path)
|
|
69
|
-
return if ["", ".md"].include?(ext)
|
|
70
|
-
|
|
71
|
-
raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def self.data_to_payload(data)
|
|
75
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
76
|
-
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
data/lib/textus/format/shared.rb
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
module Shared
|
|
4
|
-
ENFORCE_NAME_RE = /\.(md|json|yaml|yml|txt)\z/i
|
|
5
|
-
|
|
6
|
-
def self.enforce_name_match!(path, meta, extensions)
|
|
7
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
8
|
-
|
|
9
|
-
ext = extensions.first
|
|
10
|
-
basename = File.basename(path, ext)
|
|
11
|
-
return if meta["name"] == basename
|
|
12
|
-
|
|
13
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/textus/format/text.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Format
|
|
3
|
-
class Text < Base
|
|
4
|
-
def self.parse(raw, path: nil)
|
|
5
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
6
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
7
|
-
|
|
8
|
-
{ "_meta" => {}, "body" => raw, "content" => nil }
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.serialize(meta:, body:, content: nil)
|
|
12
|
-
_ = meta
|
|
13
|
-
_ = content
|
|
14
|
-
b = body.to_s
|
|
15
|
-
b += "\n" unless b.empty? || b.end_with?("\n")
|
|
16
|
-
b
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.extensions = [".txt"]
|
|
20
|
-
|
|
21
|
-
def self.nested_glob = "**/*.txt"
|
|
22
|
-
|
|
23
|
-
def self.enforce_name_match!(_path, _meta); end
|
|
24
|
-
|
|
25
|
-
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
26
|
-
_ = path
|
|
27
|
-
_ = content
|
|
28
|
-
bytes = serialize(meta: meta || {}, body: body.to_s)
|
|
29
|
-
[bytes, meta, body.to_s, nil]
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
|
|
33
|
-
false
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.validate_path_extension(path, nested)
|
|
37
|
-
ext = File.extname(path)
|
|
38
|
-
if nested
|
|
39
|
-
return if ext == ""
|
|
40
|
-
|
|
41
|
-
raise UsageError.new("nested text path must not have an extension")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
return if [".txt", ""].include?(ext)
|
|
45
|
-
|
|
46
|
-
raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.data_to_payload(data)
|
|
50
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
51
|
-
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
data/lib/textus/format/yaml.rb
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Format
|
|
5
|
-
class Yaml < Base
|
|
6
|
-
def self.parse(raw, path: nil)
|
|
7
|
-
raw = raw.dup.force_encoding(Encoding::UTF_8)
|
|
8
|
-
raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
|
|
9
|
-
|
|
10
|
-
begin
|
|
11
|
-
parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
|
|
12
|
-
rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
|
|
13
|
-
raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
|
|
14
|
-
end
|
|
15
|
-
raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
meta = parsed["_meta"]
|
|
18
|
-
fm = meta.is_a?(Hash) ? meta : {}
|
|
19
|
-
content_without_meta = parsed.except("_meta")
|
|
20
|
-
{ "_meta" => fm, "body" => raw, "content" => content_without_meta }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.serialize(meta:, body:, content: nil)
|
|
24
|
-
if content.is_a?(Hash)
|
|
25
|
-
on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
|
|
26
|
-
::YAML.dump(on_disk).sub(/\A---\n/, "")
|
|
27
|
-
elsif body && !body.to_s.empty?
|
|
28
|
-
b = body.to_s
|
|
29
|
-
b += "\n" unless b.end_with?("\n")
|
|
30
|
-
b
|
|
31
|
-
else
|
|
32
|
-
raise UsageError.new("yaml serialize requires :content or :body")
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.validate_against(schema, parsed)
|
|
37
|
-
schema.validate!(parsed["content"] || {})
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
RAW_REQUIRED = %w[ingested_at content_hash].freeze
|
|
41
|
-
RAW_SOURCE_KINDS = %w[url file asset].freeze
|
|
42
|
-
|
|
43
|
-
def self.validate_raw_entry!(parsed, lane)
|
|
44
|
-
return unless lane == "raw"
|
|
45
|
-
|
|
46
|
-
content = parsed["content"] || {}
|
|
47
|
-
return if content["superseded_by"]
|
|
48
|
-
|
|
49
|
-
missing = RAW_REQUIRED.reject { |f| content[f] }
|
|
50
|
-
raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
|
|
51
|
-
|
|
52
|
-
source = content["source"] || {}
|
|
53
|
-
kind = source["kind"]
|
|
54
|
-
unless RAW_SOURCE_KINDS.include?(kind)
|
|
55
|
-
raise Textus::BadContent.new(
|
|
56
|
-
nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
return unless kind == "url" && !source["url"]
|
|
61
|
-
|
|
62
|
-
raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def self.extensions = [".yaml", ".yml"]
|
|
66
|
-
|
|
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.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
86
|
-
raw = File.binread(path)
|
|
87
|
-
parsed = parse(raw, path: path)
|
|
88
|
-
meta = parsed["_meta"]
|
|
89
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
90
|
-
|
|
91
|
-
new_meta = meta.merge("name" => basename)
|
|
92
|
-
File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
93
|
-
true
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def self.enforce_name_match!(path, meta)
|
|
97
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
98
|
-
|
|
99
|
-
ext = extensions.first
|
|
100
|
-
basename = File.basename(path, ext)
|
|
101
|
-
return if meta["name"] == basename
|
|
102
|
-
|
|
103
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def self.validate_path_extension(path, nested)
|
|
107
|
-
ext = File.extname(path)
|
|
108
|
-
if nested
|
|
109
|
-
return if ext == ""
|
|
110
|
-
|
|
111
|
-
raise UsageError.new("nested yaml path must not have an extension")
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
return if [".yaml", ".yml"].include?(ext)
|
|
115
|
-
|
|
116
|
-
raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def self.data_to_payload(data)
|
|
120
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
121
|
-
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|