textus 0.55.2 → 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 +8 -9
- data/lib/textus/artifact.rb +8 -0
- data/lib/textus/errors.rb +0 -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/{dispatch → protocol}/binder.rb +1 -7
- 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/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
- 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 → protocol}/layout.rb +16 -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 +22 -21
- data/lib/textus/surface/cli/sources.rb +1 -3
- data/lib/textus/surface/cli/verb/get.rb +2 -7
- data/lib/textus/surface/cli/verb/init.rb +1 -1
- data/lib/textus/surface/cli/verb/put.rb +7 -7
- 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 +12 -12
- 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/server.rb +35 -24
- 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/value/etag.rb +7 -7
- 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 -53
- 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 -16
- metadata +169 -185
- data/SPEC.md +0 -909
- data/docs/architecture/README.md +0 -273
- data/docs/reference/conventions.md +0 -141
- data/lib/textus/boot.rb +0 -181
- data/lib/textus/dispatch/contracts.rb +0 -63
- data/lib/textus/dispatch/handler_registry.rb +0 -21
- data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
- data/lib/textus/dispatch/middleware/auth.rb +0 -40
- data/lib/textus/dispatch/middleware/base.rb +0 -26
- data/lib/textus/dispatch/middleware/binder.rb +0 -20
- data/lib/textus/dispatch/middleware/cascade.rb +0 -53
- data/lib/textus/dispatch/pipeline.rb +0 -35
- 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/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/scratchpad_sources.rb +0 -55
- 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 -43
- data/lib/textus/doctor/validator.rb +0 -93
- data/lib/textus/doctor.rb +0 -54
- data/lib/textus/format/base.rb +0 -77
- data/lib/textus/format/json.rb +0 -81
- data/lib/textus/format/markdown.rb +0 -59
- data/lib/textus/format/text.rb +0 -55
- data/lib/textus/format/yaml.rb +0 -104
- data/lib/textus/format.rb +0 -61
- data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
- data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
- data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
- data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
- data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
- data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
- data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
- data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
- data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
- data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
- data/lib/textus/handlers/read/audit_entries.rb +0 -48
- data/lib/textus/handlers/read/blame_entry.rb +0 -71
- data/lib/textus/handlers/read/deps_entry.rb +0 -17
- data/lib/textus/handlers/read/get_entry.rb +0 -68
- data/lib/textus/handlers/read/list_keys.rb +0 -36
- data/lib/textus/handlers/read/pulse_entries.rb +0 -66
- data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
- data/lib/textus/handlers/read/uid_entry.rb +0 -18
- data/lib/textus/handlers/read/where_entry.rb +0 -18
- data/lib/textus/handlers/write/accept_proposal.rb +0 -39
- data/lib/textus/handlers/write/data_mv.rb +0 -55
- data/lib/textus/handlers/write/delete_key.rb +0 -17
- data/lib/textus/handlers/write/enqueue_job.rb +0 -27
- data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
- data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
- data/lib/textus/handlers/write/move_key.rb +0 -80
- data/lib/textus/handlers/write/propose_entry.rb +0 -29
- data/lib/textus/handlers/write/put_entry.rb +0 -29
- data/lib/textus/handlers/write/reject_proposal.rb +0 -29
- 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/predicates/author_held.rb +0 -22
- data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
- data/lib/textus/manifest/policy/predicates.rb +0 -54
- 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/orchestration.rb +0 -55
- 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.rb +0 -25
- 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 -49
- data/lib/textus/port/storage/interface.rb +0 -17
- data/lib/textus/port/store.rb +0 -149
- data/lib/textus/port/watcher_lock.rb +0 -48
- data/lib/textus/produce/engine.rb +0 -40
- data/lib/textus/produce/publisher.rb +0 -21
- data/lib/textus/produce/render.rb +0 -20
- data/lib/textus/schema/registry.rb +0 -42
- data/lib/textus/schema/tools.rb +0 -108
- data/lib/textus/schema.rb +0 -100
- data/lib/textus/store/container.rb +0 -173
- data/lib/textus/store/cursor.rb +0 -26
- data/lib/textus/store/entry/reader.rb +0 -47
- data/lib/textus/store/entry/writer.rb +0 -219
- data/lib/textus/store/envelope/meta.rb +0 -61
- data/lib/textus/store/freshness/drift_detector.rb +0 -93
- data/lib/textus/store/freshness/evaluator.rb +0 -20
- data/lib/textus/store/freshness/verdict.rb +0 -24
- data/lib/textus/store/freshness.rb +0 -8
- data/lib/textus/store/index/builder.rb +0 -76
- 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 -124
- data/lib/textus/store/jobs/queue.rb +0 -162
- data/lib/textus/store/jobs/registry.rb +0 -19
- data/lib/textus/store/jobs/retention/base.rb +0 -52
- data/lib/textus/store/jobs/retention/sweep.rb +0 -55
- data/lib/textus/store/jobs/retention.rb +0 -8
- data/lib/textus/store/jobs/sweep.rb +0 -21
- data/lib/textus/store/jobs/worker.rb +0 -64
- data/lib/textus/store.rb +0 -122
- data/lib/textus/surface/cli/verb/doctor.rb +0 -20
- 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/projector.rb +0 -20
- data/lib/textus/surface/mcp.rb +0 -8
- data/lib/textus/value/command.rb +0 -16
- data/lib/textus/value/envelope.rb +0 -90
- data/lib/textus/value/result.rb +0 -46
- data/lib/textus/verb_registry.rb +0 -417
- 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,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Builder
|
|
4
|
+
class Infrastructure
|
|
5
|
+
Components = Data.define(:database, :file_system, :store, :layout)
|
|
6
|
+
|
|
7
|
+
def call(root)
|
|
8
|
+
layout = Layout.new(root)
|
|
9
|
+
database = Infra::Database.new(layout:).setup!
|
|
10
|
+
file_system = Infra::FileSystem.new
|
|
11
|
+
store = Infra::Store.new(database:, file_system:)
|
|
12
|
+
Components.new(database:, file_system:, store:, layout:)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Builder
|
|
4
|
+
class Runtime
|
|
5
|
+
Components = Data.define(:store_engine, :gate, :ttl_evaluator, :container)
|
|
6
|
+
|
|
7
|
+
def call(infra:, domain:, root:)
|
|
8
|
+
event_emitter = Textus::Workflow::EventEmitter.new(
|
|
9
|
+
infra.store.workflow_queue,
|
|
10
|
+
registry: domain.workflow_registry,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
store_engine = StoreEngine.new(
|
|
14
|
+
file_system: infra.file_system, manifest: domain.manifest,
|
|
15
|
+
layout: infra.layout, store: infra.store,
|
|
16
|
+
event_emitter:, schemas: domain.schemas
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ttl_evaluator = Freshness::TtlEvaluator.new(
|
|
20
|
+
manifest: domain.manifest,
|
|
21
|
+
file_stat: infra.file_system,
|
|
22
|
+
clock: Infra::Clock.new,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
container = Container.new(
|
|
26
|
+
store: infra.store, layout: infra.layout,
|
|
27
|
+
manifest: domain.manifest, schemas: domain.schemas,
|
|
28
|
+
workflows: domain.workflows,
|
|
29
|
+
store_engine: store_engine,
|
|
30
|
+
root:, ttl_evaluator:
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
gate = Gate.new(
|
|
34
|
+
manifest: domain.manifest,
|
|
35
|
+
resolver: domain.manifest.resolver,
|
|
36
|
+
container:,
|
|
37
|
+
store_engine:,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
container.gate = gate
|
|
41
|
+
Components.new(store_engine:, gate:, ttl_evaluator:, container:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
class Container
|
|
4
|
+
attr_accessor :gate
|
|
5
|
+
attr_reader :store, :layout, :manifest, :schemas, :workflows, :workflow_registry, :root, :ttl_evaluator, :store_engine
|
|
6
|
+
|
|
7
|
+
def initialize(store:, layout:, manifest:, schemas:, workflows:,
|
|
8
|
+
root: nil, gate: nil, ttl_evaluator: nil, store_engine: nil)
|
|
9
|
+
@store = store
|
|
10
|
+
@layout = layout
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
@schemas = schemas
|
|
13
|
+
@workflows = workflows
|
|
14
|
+
@workflow_registry = workflows ? Textus::Workflow::Registry.new(workflows) : nil
|
|
15
|
+
@root = root
|
|
16
|
+
@gate = gate
|
|
17
|
+
@ttl_evaluator = ttl_evaluator
|
|
18
|
+
@store_engine = store_engine
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module Diff
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def body(a_text, b_text)
|
|
7
|
+
a_lines = (a_text || "").lines.map(&:chomp)
|
|
8
|
+
b_lines = (b_text || "").lines.map(&:chomp)
|
|
9
|
+
hunks = build_hunks(myers_diff(a_lines, b_lines), a_lines, b_lines)
|
|
10
|
+
{ "type" => "body", "hunks" => hunks, "stats" => stats(hunks) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def meta(a_hash, b_hash)
|
|
14
|
+
a = a_hash || {}
|
|
15
|
+
b = b_hash || {}
|
|
16
|
+
all_keys = (a.keys + b.keys).uniq.sort
|
|
17
|
+
changes = all_keys.filter_map do |k|
|
|
18
|
+
av = a[k]
|
|
19
|
+
bv = b[k]
|
|
20
|
+
next if av == bv
|
|
21
|
+
|
|
22
|
+
{ "key" => k.to_s, "old" => av, "new" => bv }
|
|
23
|
+
end
|
|
24
|
+
{ "type" => "meta", "changes" => changes, "stats" => { "changed" => changes.size } }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def schema(a_schema, b_schema)
|
|
28
|
+
meta(a_schema, b_schema).merge("type" => "schema")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def summary(diff_result)
|
|
32
|
+
parts = []
|
|
33
|
+
parts << "#{diff_result.dig("body", "stats", "additions") || 0}+" if diff_result["body"]
|
|
34
|
+
parts << "#{diff_result.dig("body", "stats", "deletions") || 0}-" if diff_result["body"]
|
|
35
|
+
parts << "#{diff_result.dig("meta", "stats", "changed") || 0}~" if diff_result["meta"]
|
|
36
|
+
parts.join(" ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def myers_diff(a_lines, b_lines)
|
|
40
|
+
a_count = a_lines.size
|
|
41
|
+
b_count = b_lines.size
|
|
42
|
+
max_d = a_count + b_count
|
|
43
|
+
v = Hash.new(0)
|
|
44
|
+
v[1] = 0
|
|
45
|
+
trace = []
|
|
46
|
+
|
|
47
|
+
(0..max_d).each do |d|
|
|
48
|
+
(-d..d).step(2).each do |k|
|
|
49
|
+
x = if k == -d || (k != d && v[k - 1] < v[k + 1])
|
|
50
|
+
v[k + 1]
|
|
51
|
+
else
|
|
52
|
+
v[k - 1] + 1
|
|
53
|
+
end
|
|
54
|
+
y = x - k
|
|
55
|
+
while x < a_count && y < b_count && a_lines[x] == b_lines[y]
|
|
56
|
+
x += 1
|
|
57
|
+
y += 1
|
|
58
|
+
end
|
|
59
|
+
v[k] = x
|
|
60
|
+
return backtrack(trace, x, y) if x >= a_count && y >= b_count
|
|
61
|
+
end
|
|
62
|
+
trace << v.dup
|
|
63
|
+
end
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def backtrack(trace, x_pos, y_pos)
|
|
68
|
+
script = []
|
|
69
|
+
d = trace.size - 1
|
|
70
|
+
return script if d.negative?
|
|
71
|
+
|
|
72
|
+
k = x_pos - y_pos
|
|
73
|
+
while d.positive?
|
|
74
|
+
prev_v = trace[d - 1]
|
|
75
|
+
prev_k = if k == -d || (k != d && (prev_v[k - 1] || -1) < (prev_v[k + 1] || -1))
|
|
76
|
+
k + 1
|
|
77
|
+
else
|
|
78
|
+
k - 1
|
|
79
|
+
end
|
|
80
|
+
prev_x = prev_v[prev_k] || 0
|
|
81
|
+
prev_y = prev_x - prev_k
|
|
82
|
+
|
|
83
|
+
while x_pos > prev_x && y_pos > prev_y
|
|
84
|
+
script.unshift([:eq, x_pos - 1, y_pos - 1])
|
|
85
|
+
x_pos -= 1
|
|
86
|
+
y_pos -= 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if x_pos > prev_x
|
|
90
|
+
script.unshift([:del, x_pos - 1, nil])
|
|
91
|
+
x_pos -= 1
|
|
92
|
+
elsif y_pos > prev_y
|
|
93
|
+
script.unshift([:ins, nil, y_pos - 1])
|
|
94
|
+
y_pos -= 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
d -= 1
|
|
98
|
+
k = prev_k
|
|
99
|
+
x_pos = prev_x
|
|
100
|
+
y_pos = prev_y
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
script
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_hunks(script, a_lines, b_lines)
|
|
107
|
+
return [] if script.empty?
|
|
108
|
+
|
|
109
|
+
hunks = []
|
|
110
|
+
i = 0
|
|
111
|
+
while i < script.size
|
|
112
|
+
next_context = find_next_change(script, i)
|
|
113
|
+
break unless next_context
|
|
114
|
+
|
|
115
|
+
start = [next_context - 3, 0].max
|
|
116
|
+
hunk_end_script = find_hunk_end(script, next_context)
|
|
117
|
+
hunk_end = [hunk_end_script + 3, script.size - 1].min
|
|
118
|
+
|
|
119
|
+
a_start = nil
|
|
120
|
+
b_start = nil
|
|
121
|
+
a_lines_hunk = []
|
|
122
|
+
b_lines_hunk = []
|
|
123
|
+
|
|
124
|
+
(start..hunk_end).each do |j|
|
|
125
|
+
op, ai, bi = script[j]
|
|
126
|
+
a_start ||= ai if ai
|
|
127
|
+
b_start ||= bi if bi
|
|
128
|
+
|
|
129
|
+
case op
|
|
130
|
+
when :eq
|
|
131
|
+
a_lines_hunk << { "type" => "context", "line" => a_lines[ai] }
|
|
132
|
+
b_lines_hunk << { "type" => "context", "line" => b_lines[bi] }
|
|
133
|
+
when :del
|
|
134
|
+
a_lines_hunk << { "type" => "deletion", "line" => a_lines[ai] }
|
|
135
|
+
when :ins
|
|
136
|
+
b_lines_hunk << { "type" => "addition", "line" => b_lines[bi] }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
hunks << {
|
|
141
|
+
"a_start" => a_start, "b_start" => b_start,
|
|
142
|
+
"a_lines" => a_lines_hunk, "b_lines" => b_lines_hunk
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
i = hunk_end + 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
hunks
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def find_next_change(script, from)
|
|
152
|
+
(from...script.size).find { |i| script[i][0] != :eq }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def find_hunk_end(script, from)
|
|
156
|
+
last_change = from
|
|
157
|
+
(from...[script.size, from + 6].min).each do |i|
|
|
158
|
+
last_change = i if script[i][0] != :eq
|
|
159
|
+
end
|
|
160
|
+
last_change
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def stats(hunks)
|
|
164
|
+
additions = hunks.sum { |h| h["b_lines"].count { |l| l["type"] == "addition" } }
|
|
165
|
+
deletions = hunks.sum { |h| h["a_lines"].count { |l| l["type"] == "deletion" } }
|
|
166
|
+
{ "additions" => additions, "deletions" => deletions, "hunks" => hunks.size }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Protocol
|
|
3
|
+
module EntryConstraint
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def validate_naming!(key, mentry)
|
|
7
|
+
naming = mentry.naming
|
|
8
|
+
return unless naming
|
|
9
|
+
return unless key.start_with?("#{mentry.key}.")
|
|
10
|
+
|
|
11
|
+
last = key.split(".").last
|
|
12
|
+
|
|
13
|
+
case naming
|
|
14
|
+
when "sequential"
|
|
15
|
+
return if last.match?(/\A\d{4}-[a-z][a-z0-9-]*\z/)
|
|
16
|
+
|
|
17
|
+
raise UsageError.new(
|
|
18
|
+
"naming violation: '#{key}' last segment must match NNNN-slug " \
|
|
19
|
+
"(entry '#{mentry.key}' has naming: sequential)",
|
|
20
|
+
)
|
|
21
|
+
when "dated"
|
|
22
|
+
return if last.match?(/\A\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*\z/)
|
|
23
|
+
|
|
24
|
+
raise UsageError.new(
|
|
25
|
+
"naming violation: '#{key}' last segment must match YYYY-MM-DD-slug " \
|
|
26
|
+
"(entry '#{mentry.key}' has naming: dated)",
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_schema!(payload, mentry, schemas:)
|
|
32
|
+
schema_name = mentry.schema
|
|
33
|
+
return unless schema_name && schemas
|
|
34
|
+
|
|
35
|
+
schema = schemas.fetch_or_nil(schema_name)
|
|
36
|
+
return unless schema
|
|
37
|
+
return if mentry.lane == "scratchpad"
|
|
38
|
+
|
|
39
|
+
schema.validate!(payload.meta || {})
|
|
40
|
+
rescue Textus::SchemaViolation
|
|
41
|
+
raise if mentry.lane != "scratchpad"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_format!(payload, mentry, format: nil)
|
|
45
|
+
fmt_name = format || mentry.format
|
|
46
|
+
fmt = Textus::Protocol::Format.for(fmt_name)
|
|
47
|
+
fmt.validate_raw_entry!(
|
|
48
|
+
{ "_meta" => payload.meta, "content" => payload.content },
|
|
49
|
+
mentry.lane,
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "dry-struct"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Protocol
|
|
5
|
+
class Envelope < Dry::Struct
|
|
6
|
+
attribute :protocol, Textus::Value::Types::String
|
|
7
|
+
attribute :key, Textus::Value::Types::String
|
|
8
|
+
attribute :lane, Textus::Value::Types::String
|
|
9
|
+
attribute :path, Textus::Value::Types::String
|
|
10
|
+
attribute :format, Textus::Value::Types::FormatName
|
|
11
|
+
attribute :etag, Textus::Value::Types::String
|
|
12
|
+
attribute :uid, Textus::Value::Types::String.optional
|
|
13
|
+
attribute :sources, Textus::Value::Types::Array.of(Textus::Value::Types::Any).optional
|
|
14
|
+
attribute :schema_ref, Textus::Value::Types::String.optional
|
|
15
|
+
attribute :meta, Textus::Value::Types::Hash.default({}.freeze)
|
|
16
|
+
attribute :body, Textus::Value::Types::String.optional
|
|
17
|
+
attribute :content, Textus::Value::Types::Any.optional
|
|
18
|
+
attribute :freshness, Textus::Value::Types::Any.optional
|
|
19
|
+
|
|
20
|
+
def self.build(key:, mentry:, path:, meta:, body:, etag:, content: nil, freshness: nil)
|
|
21
|
+
new(
|
|
22
|
+
protocol: Textus::PROTOCOL,
|
|
23
|
+
key:,
|
|
24
|
+
lane: mentry.lane,
|
|
25
|
+
path:,
|
|
26
|
+
format: mentry.format,
|
|
27
|
+
uid: extract_uid(meta),
|
|
28
|
+
sources: extract_sources(meta),
|
|
29
|
+
etag:,
|
|
30
|
+
schema_ref: mentry.schema,
|
|
31
|
+
meta: meta || {},
|
|
32
|
+
body:,
|
|
33
|
+
content:,
|
|
34
|
+
freshness:,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def with(**attrs) = self.class.new(to_h.merge(attrs))
|
|
39
|
+
|
|
40
|
+
def to_h_for_wire
|
|
41
|
+
h = {
|
|
42
|
+
"protocol" => protocol, "key" => key, "lane" => lane,
|
|
43
|
+
"path" => path, "format" => format, "_meta" => meta,
|
|
44
|
+
"body" => body, "etag" => etag, "schema_ref" => schema_ref, "uid" => uid
|
|
45
|
+
}
|
|
46
|
+
h["sources"] = sources if sources
|
|
47
|
+
h["content"] = content unless content.nil?
|
|
48
|
+
freshness&.to_h_for_wire&.each { |k, v| h[k] = v }
|
|
49
|
+
h
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def stale? = freshness.is_a?(Hash) && freshness["stale"] == true
|
|
53
|
+
|
|
54
|
+
def fetching? = freshness.is_a?(Hash) && freshness["fetching"] == true
|
|
55
|
+
|
|
56
|
+
def self.extract_uid(meta)
|
|
57
|
+
v = meta.is_a?(Hash) ? meta["uid"] : nil
|
|
58
|
+
v.is_a?(String) ? v : nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.extract_sources(meta)
|
|
62
|
+
v = meta.is_a?(Hash) ? meta["sources"] : nil
|
|
63
|
+
return nil unless v.is_a?(Array) && !v.empty?
|
|
64
|
+
|
|
65
|
+
valid = v.select { |s| s.is_a?(String) || (s.is_a?(Hash) && s["key"].is_a?(String)) }
|
|
66
|
+
valid.empty? ? nil : valid
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -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
|