coradoc 2.0.28 → 2.0.29

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/cli.rb +4 -1
  3. data/lib/coradoc/coradoc.rb +3 -0
  4. data/lib/coradoc/errors.rb +19 -0
  5. data/lib/coradoc/format_module.rb +10 -0
  6. data/lib/coradoc/pipeline.rb +60 -1
  7. data/lib/coradoc/reference/address/anchor.rb +48 -0
  8. data/lib/coradoc/reference/address/doi.rb +40 -0
  9. data/lib/coradoc/reference/address/isbn.rb +39 -0
  10. data/lib/coradoc/reference/address/path.rb +66 -0
  11. data/lib/coradoc/reference/address/scoped_path.rb +47 -0
  12. data/lib/coradoc/reference/address/url.rb +41 -0
  13. data/lib/coradoc/reference/address.rb +153 -0
  14. data/lib/coradoc/reference/catalog/composite.rb +59 -0
  15. data/lib/coradoc/reference/catalog/local.rb +97 -0
  16. data/lib/coradoc/reference/catalog/memory_index.rb +59 -0
  17. data/lib/coradoc/reference/catalog.rb +33 -0
  18. data/lib/coradoc/reference/edge/citation_options.rb +16 -0
  19. data/lib/coradoc/reference/edge/footnote_ref_options.rb +13 -0
  20. data/lib/coradoc/reference/edge/image_ref_options.rb +15 -0
  21. data/lib/coradoc/reference/edge/include_options.rb +15 -0
  22. data/lib/coradoc/reference/edge/kind.rb +72 -0
  23. data/lib/coradoc/reference/edge/link_options.rb +12 -0
  24. data/lib/coradoc/reference/edge/navigation_options.rb +12 -0
  25. data/lib/coradoc/reference/edge/options.rb +13 -0
  26. data/lib/coradoc/reference/edge.rb +80 -0
  27. data/lib/coradoc/reference/edge_search.rb +128 -0
  28. data/lib/coradoc/reference/materializer/base.rb +44 -0
  29. data/lib/coradoc/reference/materializer/passthrough.rb +31 -0
  30. data/lib/coradoc/reference/materializer/registry.rb +116 -0
  31. data/lib/coradoc/reference/materializer.rb +14 -0
  32. data/lib/coradoc/reference/presentation/base.rb +47 -0
  33. data/lib/coradoc/reference/presentation/custom_hierarchy.rb +104 -0
  34. data/lib/coradoc/reference/presentation/page.rb +20 -0
  35. data/lib/coradoc/reference/presentation/single_document.rb +35 -0
  36. data/lib/coradoc/reference/presentation/split_pages.rb +118 -0
  37. data/lib/coradoc/reference/presentation.rb +16 -0
  38. data/lib/coradoc/reference/resolution.rb +184 -0
  39. data/lib/coradoc/reference/resolver/base.rb +15 -0
  40. data/lib/coradoc/reference/resolver/caching.rb +37 -0
  41. data/lib/coradoc/reference/resolver/catalog.rb +69 -0
  42. data/lib/coradoc/reference/resolver/chain.rb +33 -0
  43. data/lib/coradoc/reference/resolver.rb +20 -0
  44. data/lib/coradoc/reference/result/ambiguous.rb +23 -0
  45. data/lib/coradoc/reference/result/base.rb +46 -0
  46. data/lib/coradoc/reference/result/missing.rb +14 -0
  47. data/lib/coradoc/reference/result/resolved.rb +22 -0
  48. data/lib/coradoc/reference/result.rb +20 -0
  49. data/lib/coradoc/reference.rb +65 -0
  50. data/lib/coradoc/validation.rb +40 -0
  51. data/lib/coradoc/version.rb +1 -1
  52. metadata +44 -1
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ module Resolver
6
+ # Wraps another resolver with an address-keyed memoization cache.
7
+ # Addresses are value types, so cache keys are stable across calls.
8
+ # Use to avoid re-asking the catalog for the same Edge.address.
9
+ class Caching < Resolver::Base
10
+ attr_reader :inner
11
+
12
+ def initialize(inner:)
13
+ super()
14
+ @inner = inner
15
+ @cache = {}
16
+ end
17
+
18
+ def resolve(edge)
19
+ cached = if @cache.key?(edge.address)
20
+ @cache[edge.address]
21
+ else
22
+ @cache[edge.address] = @inner.resolve(edge)
23
+ end
24
+ cached.for_edge(edge)
25
+ end
26
+
27
+ def clear!
28
+ @cache.clear
29
+ end
30
+
31
+ def size
32
+ @cache.size
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ module Resolver
6
+ # Asks one catalog. Applies the +ambiguous:+ policy in-line.
7
+ # Does NOT apply the +missing:+ policy — that's the caller's
8
+ # call (Resolution orchestrator) — but it does surface the
9
+ # Missing Result so the caller can decide.
10
+ class Catalog < Resolver::Base
11
+ attr_reader :catalog, :ambiguous_policy, :missing_policy
12
+
13
+ def initialize(catalog:, ambiguous: :disambiguate, missing: :warn)
14
+ super()
15
+ @catalog = catalog
16
+ @ambiguous_policy = ambiguous
17
+ @missing_policy = missing
18
+ end
19
+
20
+ def resolve(edge)
21
+ address = edge.address
22
+ return missing_for(edge, address) unless catalog.recognizes_scheme?(address.scheme)
23
+
24
+ result = catalog.lookup(address)
25
+ return missing_for(edge, address) if result.nil?
26
+
27
+ return resolved_for(edge, address, result) unless result.is_a?(Array)
28
+
29
+ resolve_ambiguous(edge, address, result)
30
+ end
31
+
32
+ private
33
+
34
+ def resolved_for(edge, address, target)
35
+ Coradoc::Reference::Result::Resolved.build(
36
+ edge: edge, address: address, target: target
37
+ )
38
+ end
39
+
40
+ def resolve_ambiguous(edge, address, candidates)
41
+ return ambiguous_error(address, candidates) if ambiguous_policy == :error
42
+ return resolved_for(edge, address, candidates.first) if ambiguous_policy == :first
43
+
44
+ # :disambiguate — warn, then deterministically take the first
45
+ Coradoc::Logger.warn(
46
+ "Reference #{address} is ambiguous " \
47
+ "(#{candidates.size} candidates); using first"
48
+ )
49
+ resolved_for(edge, address, candidates.first)
50
+ end
51
+
52
+ def ambiguous_error(address, candidates)
53
+ raise Coradoc::Reference::AmbiguousReferenceError,
54
+ "Address #{address} matched #{candidates.size} candidates"
55
+ end
56
+
57
+ def missing_for(edge, address)
58
+ case missing_policy
59
+ when :error
60
+ raise Coradoc::Reference::MissingReferenceError,
61
+ "Address #{address} not found"
62
+ else
63
+ Coradoc::Reference::Result::Missing.build(edge: edge, address: address)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ module Resolver
6
+ # Tries each child resolver in order. First non-Missing result
7
+ # wins (Resolved or Ambiguous short-circuit). Use to combine a
8
+ # local catalog with a remote one without composite catalog
9
+ # indirection.
10
+ class Chain < Resolver::Base
11
+ attr_reader :resolvers
12
+
13
+ def initialize(*resolvers)
14
+ super()
15
+ @resolvers = resolvers.flatten
16
+ end
17
+
18
+ def resolve(edge)
19
+ last = nil
20
+ @resolvers.each do |resolver|
21
+ result = resolver.resolve(edge)
22
+ return result unless result.is_a?(Coradoc::Reference::Result::Missing)
23
+
24
+ last = result
25
+ end
26
+ last || Coradoc::Reference::Result::Missing.build(
27
+ edge: edge, address: edge.address
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ # Kind-agnostic resolver: given an Edge, ask the catalog, return a
6
+ # Result. Never materializes, never slices, never orders.
7
+ #
8
+ # resolver = Resolver::Catalog.new(catalog: catalog, ambiguous: :first, missing: :warn)
9
+ # case resolver.resolve(edge)
10
+ # in Result::Resolved => r ; use_target(r.target)
11
+ # in Result::Missing ; warn("could not resolve #{edge.address}")
12
+ # end
13
+ module Resolver
14
+ autoload :Base, "#{__dir__}/resolver/base"
15
+ autoload :Catalog, "#{__dir__}/resolver/catalog"
16
+ autoload :Chain, "#{__dir__}/resolver/chain"
17
+ autoload :Caching, "#{__dir__}/resolver/caching"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ module Result
6
+ # Multiple candidates matched. The catalog returned an Array.
7
+ # Callers' +ambiguous:+ policy decides what to do.
8
+ class Ambiguous < Base
9
+ attribute :candidates, Coradoc::CoreModel::Base, collection: true
10
+
11
+ def self.build(edge:, address:, candidates:)
12
+ new(edge: edge, address: address, candidates: candidates)
13
+ end
14
+
15
+ private
16
+
17
+ def result_data
18
+ { candidates: candidates }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Coradoc
6
+ module Reference
7
+ module Result
8
+ # Base class for resolution outcomes. Subclasses are value types.
9
+ # Every Result carries the Edge that asked — so callers can log,
10
+ # trace, and re-route without re-resolving.
11
+ class Base < Lutaml::Model::Serializable
12
+ attribute :edge, Coradoc::Reference::Edge
13
+ attribute :address, Coradoc::Reference::Address
14
+
15
+ def resolved?
16
+ is_a?(Result::Resolved)
17
+ end
18
+
19
+ def ambiguous?
20
+ is_a?(Result::Ambiguous)
21
+ end
22
+
23
+ def missing?
24
+ is_a?(Result::Missing)
25
+ end
26
+
27
+ # This Result as seen by +edge+. Returns self when the edge is
28
+ # value-equal to the one embedded; otherwise rebuilds the same
29
+ # outcome (target/candidates preserved) for the asking edge.
30
+ # Used by caching resolvers so two edges sharing an Address
31
+ # never see each other's identity.
32
+ def for_edge(edge)
33
+ return self if self.edge == edge
34
+
35
+ self.class.build(edge: edge, address: address, **result_data)
36
+ end
37
+
38
+ private
39
+
40
+ def result_data
41
+ {}
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ module Result
6
+ # No catalog knows this address. Caller's +missing:+ policy decides.
7
+ class Missing < Base
8
+ def self.build(edge:, address:)
9
+ new(edge: edge, address: address)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ module Result
6
+ # Catalog knew the address; exactly one Content was returned.
7
+ class Resolved < Base
8
+ attribute :target, Coradoc::CoreModel::Base
9
+
10
+ def self.build(edge:, address:, target:)
11
+ new(edge: edge, address: address, target: target)
12
+ end
13
+
14
+ private
15
+
16
+ def result_data
17
+ { target: target }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Reference
5
+ # Sum type for resolution outcomes. Callers pattern-match on the
6
+ # concrete subclass — never check +nil+.
7
+ #
8
+ # case resolver.resolve(edge)
9
+ # in Result::Resolved => r # have the target
10
+ # in Result::Ambiguous => a # multiple candidates
11
+ # in Result::Missing => m # catalog does not know
12
+ # end
13
+ module Result
14
+ autoload :Base, "#{__dir__}/result/base"
15
+ autoload :Resolved, "#{__dir__}/result/resolved"
16
+ autoload :Ambiguous, "#{__dir__}/result/ambiguous"
17
+ autoload :Missing, "#{__dir__}/result/missing"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'coradoc/errors'
4
+
5
+ module Coradoc
6
+ # Unified content-graph reference resolution.
7
+ #
8
+ # Every referenceable thing in a document is a Content node; every
9
+ # reference is a directed Edge with a kind. The four primitives:
10
+ #
11
+ # Content — CoreModel::Base with an id (already on Base).
12
+ # Document — Content that contains Content (composite).
13
+ # Reference::Edge — directed {source Content, target Address, kind}.
14
+ # Reference::Address — scheme-aware locator (anchor, path, url, ...).
15
+ #
16
+ # Same Content graph → N Presentations → M Materializers. The public
17
+ # entry point is +Coradoc.resolve_references+.
18
+ module Reference
19
+ # Base class for reference-resolution errors. All subclasses inherit
20
+ # from here so callers can rescue the family with one +rescue+ clause.
21
+ class Error < Coradoc::Error; end
22
+
23
+ # Raised when no catalog knew the requested Address and the
24
+ # +missing:+ policy is :error.
25
+ class MissingReferenceError < Error
26
+ attr_reader :address
27
+
28
+ def initialize(message = nil, address: nil)
29
+ @address = address
30
+ super(message || "Reference not found: #{address}")
31
+ end
32
+ end
33
+
34
+ # Raised when multiple catalogs matched an Address and the
35
+ # +ambiguous:+ policy is :error.
36
+ class AmbiguousReferenceError < Error
37
+ attr_reader :address, :candidates
38
+
39
+ def initialize(message = nil, address: nil, candidates: nil)
40
+ @address = address
41
+ @candidates = candidates
42
+ super(message || "Reference is ambiguous: #{address}")
43
+ end
44
+ end
45
+
46
+ # Raised when the catalog index is malformed — a programmer
47
+ # error, not a runtime condition. Surfaces as a clear message
48
+ # rather than a vague NoMethodError.
49
+ class InvalidCatalogError < Error; end
50
+
51
+ # Raised when building an Edge whose kind was never registered.
52
+ # External kinds register via +Edge.register_kind+ (OCP).
53
+ class UnknownKindError < Error; end
54
+
55
+ autoload :Address, "#{__dir__}/reference/address"
56
+ autoload :Edge, "#{__dir__}/reference/edge"
57
+ autoload :Catalog, "#{__dir__}/reference/catalog"
58
+ autoload :Result, "#{__dir__}/reference/result"
59
+ autoload :Resolver, "#{__dir__}/reference/resolver"
60
+ autoload :Presentation, "#{__dir__}/reference/presentation"
61
+ autoload :Materializer, "#{__dir__}/reference/materializer"
62
+ autoload :EdgeSearch, "#{__dir__}/reference/edge_search"
63
+ autoload :Resolution, "#{__dir__}/reference/resolution"
64
+ end
65
+ end
@@ -28,6 +28,46 @@ module Coradoc
28
28
  # end
29
29
  #
30
30
  module Validation
31
+ module_function
32
+
33
+ # Targets of every unresolved +include::+ edge in the tree
34
+ # (deduplicated, in document order). A document is "unresolved"
35
+ # when it was parsed in graph mode and never went through
36
+ # +Coradoc.resolve_includes+.
37
+ def unresolved_include_targets(model)
38
+ targets = []
39
+ walk_nodes(model) do |node|
40
+ targets << node.target if node.is_a?(Coradoc::CoreModel::Include)
41
+ end
42
+ targets.uniq
43
+ end
44
+
45
+ # Serialization boundary check: formats that cannot represent an
46
+ # unresolved include edge (FormatModule::Interface
47
+ # #preserves_unresolved_includes? == false) must not silently drop
48
+ # the content — raise UnresolvedIncludesError instead, pointing at
49
+ # the hydration step (resolve_includes).
50
+ def guard_unresolved_includes!(model, format_module)
51
+ return if format_module.preserves_unresolved_includes?
52
+
53
+ targets = unresolved_include_targets(model)
54
+ return if targets.empty?
55
+
56
+ raise Coradoc::UnresolvedIncludesError, targets
57
+ end
58
+
59
+ def walk_nodes(node, &block)
60
+ return unless node.is_a?(Coradoc::CoreModel::Base)
61
+
62
+ yield node
63
+ return unless node.is_a?(Coradoc::CoreModel::HasChildren)
64
+
65
+ children = node.children
66
+ return unless children
67
+
68
+ children.each { |child| walk_nodes(child, &block) }
69
+ end
70
+
31
71
  # A single validation error
32
72
  class Error
33
73
  attr_reader :path, :message, :code, :element
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coradoc
4
- VERSION = '2.0.28'
4
+ VERSION = '2.0.29'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coradoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.28
4
+ version: 2.0.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -131,6 +131,49 @@ files:
131
131
  - lib/coradoc/performance_regression.rb
132
132
  - lib/coradoc/pipeline.rb
133
133
  - lib/coradoc/query.rb
134
+ - lib/coradoc/reference.rb
135
+ - lib/coradoc/reference/address.rb
136
+ - lib/coradoc/reference/address/anchor.rb
137
+ - lib/coradoc/reference/address/doi.rb
138
+ - lib/coradoc/reference/address/isbn.rb
139
+ - lib/coradoc/reference/address/path.rb
140
+ - lib/coradoc/reference/address/scoped_path.rb
141
+ - lib/coradoc/reference/address/url.rb
142
+ - lib/coradoc/reference/catalog.rb
143
+ - lib/coradoc/reference/catalog/composite.rb
144
+ - lib/coradoc/reference/catalog/local.rb
145
+ - lib/coradoc/reference/catalog/memory_index.rb
146
+ - lib/coradoc/reference/edge.rb
147
+ - lib/coradoc/reference/edge/citation_options.rb
148
+ - lib/coradoc/reference/edge/footnote_ref_options.rb
149
+ - lib/coradoc/reference/edge/image_ref_options.rb
150
+ - lib/coradoc/reference/edge/include_options.rb
151
+ - lib/coradoc/reference/edge/kind.rb
152
+ - lib/coradoc/reference/edge/link_options.rb
153
+ - lib/coradoc/reference/edge/navigation_options.rb
154
+ - lib/coradoc/reference/edge/options.rb
155
+ - lib/coradoc/reference/edge_search.rb
156
+ - lib/coradoc/reference/materializer.rb
157
+ - lib/coradoc/reference/materializer/base.rb
158
+ - lib/coradoc/reference/materializer/passthrough.rb
159
+ - lib/coradoc/reference/materializer/registry.rb
160
+ - lib/coradoc/reference/presentation.rb
161
+ - lib/coradoc/reference/presentation/base.rb
162
+ - lib/coradoc/reference/presentation/custom_hierarchy.rb
163
+ - lib/coradoc/reference/presentation/page.rb
164
+ - lib/coradoc/reference/presentation/single_document.rb
165
+ - lib/coradoc/reference/presentation/split_pages.rb
166
+ - lib/coradoc/reference/resolution.rb
167
+ - lib/coradoc/reference/resolver.rb
168
+ - lib/coradoc/reference/resolver/base.rb
169
+ - lib/coradoc/reference/resolver/caching.rb
170
+ - lib/coradoc/reference/resolver/catalog.rb
171
+ - lib/coradoc/reference/resolver/chain.rb
172
+ - lib/coradoc/reference/result.rb
173
+ - lib/coradoc/reference/result/ambiguous.rb
174
+ - lib/coradoc/reference/result/base.rb
175
+ - lib/coradoc/reference/result/missing.rb
176
+ - lib/coradoc/reference/result/resolved.rb
134
177
  - lib/coradoc/registry.rb
135
178
  - lib/coradoc/relative_path.rb
136
179
  - lib/coradoc/resolve_includes.rb