jekyll-relationships 0.1.0.alpha → 0.2.0.alpha
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/lib/jekyll-relationships/configuration/debug_setting.rb +72 -12
- data/lib/jekyll-relationships/configuration/defaults.rb +3 -1
- data/lib/jekyll-relationships/configuration/frontmatter.rb +28 -1
- data/lib/jekyll-relationships/configuration/parser.rb +20 -3
- data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +33 -5
- data/lib/jekyll-relationships/configuration/tree_frontmatter.rb +8 -2
- data/lib/jekyll-relationships/configuration.rb +43 -8
- data/lib/jekyll-relationships/debug_logger.rb +94 -3
- data/lib/jekyll-relationships/definitions/normal_relationship.rb +13 -2
- data/lib/jekyll-relationships/definitions/prune_rule.rb +19 -3
- data/lib/jekyll-relationships/definitions/tree_relationship.rb +8 -2
- data/lib/jekyll-relationships/documents/registry.rb +182 -33
- data/lib/jekyll-relationships/engine/normal_seed.rb +222 -0
- data/lib/jekyll-relationships/engine/persisted_links.rb +365 -0
- data/lib/jekyll-relationships/engine/raw_path_state.rb +46 -116
- data/lib/jekyll-relationships/engine/relationship_state.rb +67 -56
- data/lib/jekyll-relationships/engine/session.rb +159 -19
- data/lib/jekyll-relationships/engine/write_back.rb +3 -42
- data/lib/jekyll-relationships/engine.rb +168 -46
- data/lib/jekyll-relationships/pruning/rule_pruner.rb +7 -7
- data/lib/jekyll-relationships/pruning/tree_phase.rb +128 -124
- data/lib/jekyll-relationships/pruning/tree_provenance.rb +1 -1
- data/lib/jekyll-relationships/references/accumulator.rb +87 -9
- data/lib/jekyll-relationships/references/template.rb +46 -11
- data/lib/jekyll-relationships/resolvers/base.rb +59 -2
- data/lib/jekyll-relationships/support/frontmatter_matcher.rb +76 -0
- data/lib/jekyll-relationships/support/placeholders.rb +1 -0
- data/lib/jekyll-relationships/trees/edge_builder.rb +43 -11
- data/lib/jekyll-relationships/trees/graph.rb +135 -17
- data/lib/jekyll-relationships/trees/root_distances.rb +44 -0
- data/lib/jekyll-relationships/version.rb +1 -1
- data/lib/jekyll-relationships.rb +1 -0
- data/readme.md +185 -26
- metadata +6 -2
|
@@ -12,13 +12,14 @@ module Definitions
|
|
|
12
12
|
# and which frontmatter, maximum, URL, and primary-key rules should be used to
|
|
13
13
|
# resolve references for this concrete relationship definition.
|
|
14
14
|
class TreeRelationship
|
|
15
|
-
attr_reader :from_collection, :to_collection, :primary_path, :tree_settings, :sequence
|
|
15
|
+
attr_reader :from_collection, :to_collection, :primary_path, :scope_fields, :tree_settings, :sequence
|
|
16
16
|
|
|
17
17
|
# Captures the allowed parent-child directions for one tree definition.
|
|
18
|
-
def initialize(from_collection:, to_collection:, primary_path:, parent_child_pairs:, tree_settings:, debug:, sequence:)
|
|
18
|
+
def initialize(from_collection:, to_collection:, primary_path:, scope_fields: [], parent_child_pairs:, tree_settings:, debug:, sequence:)
|
|
19
19
|
@from_collection = from_collection
|
|
20
20
|
@to_collection = to_collection
|
|
21
21
|
@primary_path = primary_path
|
|
22
|
+
@scope_fields = scope_fields.freeze
|
|
22
23
|
@parent_child_pairs = parent_child_pairs
|
|
23
24
|
@tree_settings = tree_settings
|
|
24
25
|
@debug = debug
|
|
@@ -35,6 +36,11 @@ class TreeRelationship
|
|
|
35
36
|
@debug.enabled?(area)
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Returns true when one event's related IDs satisfy this definition's debug filter.
|
|
40
|
+
def debug_ids_match?(ids)
|
|
41
|
+
@debug.matches_ids?(ids)
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
# Returns true when one parent-child direction is permitted.
|
|
39
45
|
def allows_parent_child?(parent_collection:, child_collection:)
|
|
40
46
|
@parent_child_pairs.include?([parent_collection, child_collection])
|
|
@@ -6,17 +6,18 @@ module Plugins
|
|
|
6
6
|
module Relationships
|
|
7
7
|
module Documents
|
|
8
8
|
|
|
9
|
-
# Indexes participating documents by collection and
|
|
9
|
+
# Indexes participating documents by collection, primary key, and optional complete scope.
|
|
10
10
|
#
|
|
11
|
-
# Indices are built lazily per
|
|
12
|
-
# use different
|
|
11
|
+
# Indices are built lazily per complete identity scheme so relationship definitions can
|
|
12
|
+
# use different primary and scope paths without rebuilding unrelated lookups.
|
|
13
13
|
class Registry
|
|
14
14
|
|
|
15
15
|
# Builds the registry over the collections used by the configuration.
|
|
16
16
|
def initialize(site:, collections:)
|
|
17
17
|
@site = site
|
|
18
18
|
@collections = collections
|
|
19
|
-
@
|
|
19
|
+
@indices_by_identity = {}
|
|
20
|
+
@data_path = Jekyll::Plugins::Relationships::Support::DataPath.new
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# Returns true when the document belongs to one of the tracked collections.
|
|
@@ -48,7 +49,19 @@ class Registry
|
|
|
48
49
|
# Builds every requested primary-key index so duplicates fail eagerly.
|
|
49
50
|
def validate_primary_paths!(primary_paths:)
|
|
50
51
|
Array(primary_paths).uniq.each do |primary_path|
|
|
51
|
-
index_for(primary_path)
|
|
52
|
+
index_for(primary_path: primary_path, scope_fields: [])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Builds every requested identity index so missing scope values and duplicate identities fail eagerly.
|
|
57
|
+
def validate_identity_schemes!(identity_schemes:)
|
|
58
|
+
Array(identity_schemes).each do |scheme|
|
|
59
|
+
index_for(
|
|
60
|
+
primary_path: scheme.fetch(:primary_path),
|
|
61
|
+
scope_fields: scheme.fetch(:scope_fields),
|
|
62
|
+
collections: scheme.fetch(:collections),
|
|
63
|
+
relationship: scheme.fetch(:relationships).join(', ')
|
|
64
|
+
)
|
|
52
65
|
end
|
|
53
66
|
end
|
|
54
67
|
|
|
@@ -57,61 +70,157 @@ class Registry
|
|
|
57
70
|
if primary_path.nil?
|
|
58
71
|
default_relative_key(document)
|
|
59
72
|
else
|
|
60
|
-
value =
|
|
73
|
+
value = @data_path.read(document.data, primary_path)
|
|
61
74
|
raise ResolutionError, "Document `#{document.relative_path}` is missing primary key path `#{primary_path}`." if value.nil?
|
|
62
75
|
|
|
63
76
|
value.to_s
|
|
64
77
|
end
|
|
65
78
|
end
|
|
66
79
|
|
|
67
|
-
#
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
# Reads and validates a document's complete scope for one configured identity scheme.
|
|
81
|
+
def scope_for(document, scope_fields:, relationship: nil)
|
|
82
|
+
return nil if scope_fields.empty?
|
|
83
|
+
|
|
84
|
+
scope_fields.each_with_object({}) do |field, scope|
|
|
85
|
+
value = @data_path.read(document.data, field.path)
|
|
86
|
+
if value.nil?
|
|
87
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}document `#{document.relative_path}` is missing required scope field `#{field.name}` at frontmatter path `#{field.path}`."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
scope[field.name] = value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Resolves reference overrides over the referring document's scope and validates every configured field.
|
|
95
|
+
def effective_scope_for(reference_scope:, referring_document:, scope_fields:, relationship: nil)
|
|
96
|
+
return nil if scope_fields.empty? && (reference_scope.nil? || reference_scope.empty?)
|
|
97
|
+
|
|
98
|
+
reference_scope ||= {}
|
|
99
|
+
unless reference_scope.is_a?(Hash)
|
|
100
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}reference on document `#{referring_document.relative_path}` must provide scope as a hash."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
configured_names = scope_fields.map(&:name)
|
|
104
|
+
unknown_names = reference_scope.keys.map(&:to_s) - configured_names
|
|
105
|
+
unless unknown_names.empty?
|
|
106
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}reference on document `#{referring_document.relative_path}` uses unconfigured scope field `#{unknown_names.first}`."
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
scope_fields.each_with_object({}) do |field, effective_scope|
|
|
110
|
+
value = if hash_property?(reference_scope, field.name)
|
|
111
|
+
Jekyll::Plugins::Relationships::Support::FrontmatterPath.read_hash(reference_scope, field.name)
|
|
112
|
+
else
|
|
113
|
+
@data_path.read(referring_document.data, field.path)
|
|
114
|
+
end
|
|
115
|
+
if value.nil?
|
|
116
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}reference on document `#{referring_document.relative_path}` has no effective value for scope field `#{field.name}`."
|
|
117
|
+
end
|
|
73
118
|
|
|
74
|
-
|
|
119
|
+
effective_scope[field.name] = value
|
|
75
120
|
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Ensures one already-selected target contains and exactly matches an effective scope.
|
|
124
|
+
def validate_scope_match!(document:, scope:, scope_fields:, relationship: nil, referring_document: nil)
|
|
125
|
+
return nil if scope_fields.empty?
|
|
76
126
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return matches.first if matches.length == 1
|
|
127
|
+
target_scope = scope_for(document, scope_fields: scope_fields, relationship: relationship)
|
|
128
|
+
scope_fields.each do |field|
|
|
129
|
+
next if target_scope.fetch(field.name) == scope.fetch(field.name)
|
|
81
130
|
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
raise ResolutionError, scope_mismatch_message(
|
|
132
|
+
document: document,
|
|
133
|
+
field: field,
|
|
134
|
+
expected_value: scope.fetch(field.name),
|
|
135
|
+
actual_value: target_scope.fetch(field.name),
|
|
136
|
+
relationship: relationship,
|
|
137
|
+
referring_document: referring_document
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
target_scope
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Resolves one key and complete scope to a document, optionally constrained to a collection.
|
|
145
|
+
def lookup(key:, primary_path:, collection: nil, scope_fields: [], scope: nil, relationship: nil, referring_document: nil)
|
|
146
|
+
index = index_for(primary_path: primary_path, scope_fields: scope_fields, relationship: relationship)
|
|
147
|
+
candidates = if collection
|
|
148
|
+
fetch_nested(index.fetch(:by_collection), collection, key.to_s) || []
|
|
149
|
+
else
|
|
150
|
+
index.fetch(:by_key).fetch(key.to_s, [])
|
|
151
|
+
end
|
|
152
|
+
if candidates.empty?
|
|
153
|
+
if collection
|
|
154
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}could not resolve key `#{key}` in collection `#{collection}`#{source_suffix(referring_document)}."
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}could not resolve key-only reference `#{key}`#{source_suffix(referring_document)}."
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
matches = if scope_fields.empty?
|
|
161
|
+
candidates
|
|
162
|
+
else
|
|
163
|
+
candidates.select { |entry| entry.fetch(:scope) == scope }
|
|
164
|
+
end
|
|
165
|
+
if matches.empty?
|
|
166
|
+
raise_scope_mismatch!(
|
|
167
|
+
candidates: candidates,
|
|
168
|
+
scope: scope,
|
|
169
|
+
scope_fields: scope_fields,
|
|
170
|
+
relationship: relationship,
|
|
171
|
+
referring_document: referring_document
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
matched_documents = matches.map { |entry| entry.fetch(:document) }
|
|
176
|
+
return matched_documents.first if collection
|
|
177
|
+
|
|
178
|
+
return matched_documents.first if matched_documents.length == 1
|
|
179
|
+
|
|
180
|
+
collections = matched_documents.map { |document| collection_label(document) }.uniq.sort.join(', ')
|
|
181
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}key-only reference `#{key}` is ambiguous across collections: #{collections}#{source_suffix(referring_document)}."
|
|
84
182
|
end
|
|
85
183
|
|
|
86
184
|
private
|
|
87
185
|
|
|
88
|
-
# Returns or builds the index for one
|
|
89
|
-
def index_for(primary_path)
|
|
90
|
-
signature = primary_path.nil? ? '__relative_path__' : primary_path
|
|
91
|
-
@
|
|
186
|
+
# Returns or builds the index for one complete identity scheme.
|
|
187
|
+
def index_for(primary_path:, scope_fields:, collections: nil, relationship: nil)
|
|
188
|
+
signature = [primary_path.nil? ? '__relative_path__' : primary_path, scope_fields.map(&:signature)]
|
|
189
|
+
@indices_by_identity[signature] ||= build_index(
|
|
190
|
+
primary_path: primary_path,
|
|
191
|
+
scope_fields: scope_fields,
|
|
192
|
+
collections: collections || @collections,
|
|
193
|
+
relationship: relationship
|
|
194
|
+
)
|
|
92
195
|
end
|
|
93
196
|
|
|
94
|
-
# Builds the collection and global indices for one scheme.
|
|
95
|
-
def build_index(primary_path)
|
|
96
|
-
by_collection = Hash.new { |hash, key| hash[key] = {} }
|
|
197
|
+
# Builds the collection and global indices for one complete identity scheme.
|
|
198
|
+
def build_index(primary_path:, scope_fields:, collections:, relationship: nil)
|
|
199
|
+
by_collection = Hash.new { |hash, key| hash[key] = Hash.new { |nested_hash, nested_key| nested_hash[nested_key] = [] } }
|
|
97
200
|
by_key = Hash.new { |hash, key| hash[key] = [] }
|
|
98
201
|
|
|
99
|
-
|
|
202
|
+
collections.each do |collection|
|
|
100
203
|
documents_for(collection).each do |document|
|
|
101
204
|
key = key_for(document, primary_path: primary_path)
|
|
102
|
-
|
|
103
|
-
|
|
205
|
+
scope = scope_for(document, scope_fields: scope_fields, relationship: relationship)
|
|
206
|
+
existing_entry = by_collection[collection][key].find { |entry| entry.fetch(:scope) == scope }
|
|
207
|
+
if existing_entry
|
|
208
|
+
if scope_fields.empty?
|
|
209
|
+
raise ResolutionError, "Primary key `#{key}` is duplicated within collection `#{collection}`."
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
raise ResolutionError, "#{relationship_prefix(relationship)}primary key `#{key}` is duplicated within collection `#{collection}` and scope #{scope.inspect} by documents `#{existing_entry.fetch(:document).relative_path}` and `#{document.relative_path}`."
|
|
104
213
|
end
|
|
105
214
|
|
|
106
|
-
|
|
107
|
-
|
|
215
|
+
entry = { document: document, scope: scope }
|
|
216
|
+
by_collection[collection][key] << entry
|
|
217
|
+
by_key[key] << entry
|
|
108
218
|
end
|
|
109
219
|
end
|
|
110
220
|
|
|
111
221
|
{
|
|
112
222
|
by_collection: by_collection,
|
|
113
|
-
by_key: by_key
|
|
114
|
-
site_wide_unique: by_key.values.all? { |documents| documents.length <= 1 }
|
|
223
|
+
by_key: by_key
|
|
115
224
|
}
|
|
116
225
|
end
|
|
117
226
|
|
|
@@ -123,6 +232,46 @@ class Registry
|
|
|
123
232
|
first_hash[second_key]
|
|
124
233
|
end
|
|
125
234
|
|
|
235
|
+
# Raises a field-specific error when key candidates exist but none match the requested complete scope.
|
|
236
|
+
def raise_scope_mismatch!(candidates:, scope:, scope_fields:, relationship:, referring_document:)
|
|
237
|
+
candidate = candidates.first
|
|
238
|
+
field = scope_fields.find do |configured_field|
|
|
239
|
+
candidate.fetch(:scope).fetch(configured_field.name) != scope.fetch(configured_field.name)
|
|
240
|
+
end
|
|
241
|
+
raise ResolutionError, scope_mismatch_message(
|
|
242
|
+
document: candidate.fetch(:document),
|
|
243
|
+
field: field,
|
|
244
|
+
expected_value: scope.fetch(field.name),
|
|
245
|
+
actual_value: candidate.fetch(:scope).fetch(field.name),
|
|
246
|
+
relationship: relationship,
|
|
247
|
+
referring_document: referring_document
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Builds one detailed exact-match failure for a configured scope field.
|
|
252
|
+
def scope_mismatch_message(document:, field:, expected_value:, actual_value:, relationship:, referring_document:)
|
|
253
|
+
"#{relationship_prefix(relationship)}target document `#{document.relative_path}` has scope field `#{field.name}` value `#{actual_value.inspect}`, not the required `#{expected_value.inspect}`#{source_suffix(referring_document)}."
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Returns true when one literal scope key exists in string or symbol form.
|
|
257
|
+
def hash_property?(hash, property)
|
|
258
|
+
hash.key?(property) || hash.key?(property.to_s) || hash.key?(property.to_s.to_sym)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Prefixes an error with its concrete relationship where available.
|
|
262
|
+
def relationship_prefix(relationship)
|
|
263
|
+
return '' if relationship.nil? || relationship.to_s.empty?
|
|
264
|
+
|
|
265
|
+
"Relationship `#{relationship}`: "
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Adds the referring document to lookup diagnostics where available.
|
|
269
|
+
def source_suffix(document)
|
|
270
|
+
return '' unless document
|
|
271
|
+
|
|
272
|
+
" from document `#{document.relative_path}`"
|
|
273
|
+
end
|
|
274
|
+
|
|
126
275
|
# Implements the README's default relative-path key rule.
|
|
127
276
|
def default_relative_key(document)
|
|
128
277
|
document.relative_path.sub(/\A_/, '').sub(/#{Regexp.escape(document.extname)}\z/, '')
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
module Plugins
|
|
5
|
+
|
|
6
|
+
module Relationships
|
|
7
|
+
|
|
8
|
+
class Engine
|
|
9
|
+
|
|
10
|
+
# Stores the source-derived normal relationship graph used to seed each round.
|
|
11
|
+
#
|
|
12
|
+
# The seed graph is built once from frontmatter, including deterministic
|
|
13
|
+
# bidirectional mirrors, and is later reduced only by document removals.
|
|
14
|
+
# Resolver output never flows back into this structure.
|
|
15
|
+
class NormalSeed
|
|
16
|
+
# Builds one normal seed graph for the current active document set.
|
|
17
|
+
def initialize(engine:, active_document_ids:)
|
|
18
|
+
@engine = engine
|
|
19
|
+
@configuration = engine.configuration
|
|
20
|
+
@registry = engine.registry
|
|
21
|
+
@data_path = engine.data_path
|
|
22
|
+
@debug_logger = engine.debug_logger
|
|
23
|
+
@active_document_ids = active_document_ids.each_with_object({}) do |document_id, active_ids|
|
|
24
|
+
active_ids[document_id] = true
|
|
25
|
+
end
|
|
26
|
+
@string_array = Jekyll::Plugins::Relationships::Support::StringArray.new
|
|
27
|
+
@raw_path_states = {}
|
|
28
|
+
@entries_by_state = {}
|
|
29
|
+
build!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the seed entries for one concrete document-to-collection pair.
|
|
33
|
+
def entries_for(document, to_collection)
|
|
34
|
+
duplicate_entries(@entries_by_state[[document.object_id, to_collection]])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Permanently removes documents from the seed graph in both source and target positions.
|
|
38
|
+
def remove_documents!(documents)
|
|
39
|
+
removed_document_ids = Array(documents).each_with_object({}) do |document, lookup|
|
|
40
|
+
lookup[document.object_id] = true
|
|
41
|
+
end
|
|
42
|
+
return if removed_document_ids.empty?
|
|
43
|
+
|
|
44
|
+
@entries_by_state.delete_if do |(source_document_id, _to_collection), _entries|
|
|
45
|
+
removed_document_ids.key?(source_document_id)
|
|
46
|
+
end
|
|
47
|
+
@entries_by_state.each_value do |entries|
|
|
48
|
+
entries.reject! do |entry|
|
|
49
|
+
removed_document_ids.key?(entry.fetch(:document).object_id)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Builds the full source-derived seed graph once.
|
|
57
|
+
def build!
|
|
58
|
+
accumulators = {}
|
|
59
|
+
|
|
60
|
+
@configuration.collections.each do |collection|
|
|
61
|
+
@configuration.normal_relationships_for(collection).each do |definition|
|
|
62
|
+
next unless definition.reads_frontmatter
|
|
63
|
+
|
|
64
|
+
documents_for(collection).each do |document|
|
|
65
|
+
seed_definition_from_frontmatter(
|
|
66
|
+
accumulators: accumulators,
|
|
67
|
+
document: document,
|
|
68
|
+
definition: definition
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@entries_by_state = accumulators.each_with_object({}) do |(state_key, accumulator), entries_by_state|
|
|
75
|
+
entries_by_state[state_key] = accumulator.encounter_entries
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Reads and stores one concrete direct relationship definition from frontmatter.
|
|
80
|
+
def seed_definition_from_frontmatter(accumulators:, document:, definition:)
|
|
81
|
+
definition.foreign_paths.each do |path|
|
|
82
|
+
raw_state = raw_path_state(document, path, definition)
|
|
83
|
+
debug_relationship_event(
|
|
84
|
+
document: document,
|
|
85
|
+
definition: definition,
|
|
86
|
+
event: 'raw_path',
|
|
87
|
+
details: {
|
|
88
|
+
path: path,
|
|
89
|
+
present: raw_state.present?,
|
|
90
|
+
value: raw_state.raw_value
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
next unless raw_state.present?
|
|
94
|
+
|
|
95
|
+
resolved_entries = raw_state.resolved_entries_for(
|
|
96
|
+
primary_path: definition.primary_path,
|
|
97
|
+
scope_fields: definition.scope_fields,
|
|
98
|
+
registry: @registry,
|
|
99
|
+
relationship: "#{definition.from_collection} -> #{definition.to_collection}",
|
|
100
|
+
active_document_checker: proc { |resolved_document| active_document?(resolved_document) }
|
|
101
|
+
)
|
|
102
|
+
debug_relationship_event(
|
|
103
|
+
document: document,
|
|
104
|
+
definition: definition,
|
|
105
|
+
event: 'raw_path_resolved',
|
|
106
|
+
details: {
|
|
107
|
+
path: path,
|
|
108
|
+
entries: resolved_entries.compact
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
resolved_entries.each do |entry|
|
|
113
|
+
next unless entry
|
|
114
|
+
if entry.fetch(:document).collection.label != definition.to_collection
|
|
115
|
+
debug_relationship_event(
|
|
116
|
+
document: document,
|
|
117
|
+
definition: definition,
|
|
118
|
+
event: 'raw_path_skipped',
|
|
119
|
+
details: {
|
|
120
|
+
path: path,
|
|
121
|
+
target: entry.fetch(:document),
|
|
122
|
+
target_key: entry.fetch(:key),
|
|
123
|
+
actual_collection: entry.fetch(:document).collection.label,
|
|
124
|
+
expected_collection: definition.to_collection
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
add_direct_entry(
|
|
131
|
+
accumulators: accumulators,
|
|
132
|
+
document: document,
|
|
133
|
+
definition: definition,
|
|
134
|
+
entry: entry
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Adds one direct link and its deterministic bidirectional mirror when configured.
|
|
141
|
+
def add_direct_entry(accumulators:, document:, definition:, entry:)
|
|
142
|
+
accumulator_for(
|
|
143
|
+
accumulators: accumulators,
|
|
144
|
+
document: document,
|
|
145
|
+
to_collection: definition.to_collection
|
|
146
|
+
).add(
|
|
147
|
+
document: entry.fetch(:document),
|
|
148
|
+
key: entry.fetch(:key),
|
|
149
|
+
scope: entry.fetch(:scope),
|
|
150
|
+
metadata: entry.fetch(:metadata),
|
|
151
|
+
count: entry.fetch(:count)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return unless definition.bidirectional
|
|
155
|
+
|
|
156
|
+
accumulator_for(
|
|
157
|
+
accumulators: accumulators,
|
|
158
|
+
document: entry.fetch(:document),
|
|
159
|
+
to_collection: definition.from_collection
|
|
160
|
+
).add(
|
|
161
|
+
document: document,
|
|
162
|
+
key: @registry.key_for(document, primary_path: definition.primary_path),
|
|
163
|
+
scope: @registry.scope_for(document, scope_fields: definition.scope_fields, relationship: "#{definition.from_collection} -> #{definition.to_collection}"),
|
|
164
|
+
metadata: entry.fetch(:metadata),
|
|
165
|
+
count: entry.fetch(:count)
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Returns one reusable accumulator for one concrete source pair.
|
|
170
|
+
def accumulator_for(accumulators:, document:, to_collection:)
|
|
171
|
+
accumulators[[document.object_id, to_collection]] ||= Jekyll::Plugins::Relationships::References::Accumulator.new(
|
|
172
|
+
reference_template: @configuration.reference_template,
|
|
173
|
+
multiple_settings: @configuration.multiple_settings
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns every active document for one collection.
|
|
178
|
+
def documents_for(collection)
|
|
179
|
+
@registry.documents_for(collection).select do |document|
|
|
180
|
+
active_document?(document)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns true when one document is still active in the seed graph.
|
|
185
|
+
def active_document?(document)
|
|
186
|
+
@active_document_ids.key?(document.object_id)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Returns the cached raw-path parser for one document/path pair.
|
|
190
|
+
def raw_path_state(document, path, definition)
|
|
191
|
+
@raw_path_states[[document.object_id, path, definition.object_id]] ||= RawPathState.new(
|
|
192
|
+
document: document,
|
|
193
|
+
path: path,
|
|
194
|
+
data_path: @data_path,
|
|
195
|
+
string_array: @string_array,
|
|
196
|
+
reference_template: @configuration.reference_template,
|
|
197
|
+
reference_context: "Relationship `#{definition.from_collection} -> #{definition.to_collection}` reference on document `#{document.relative_path}`"
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Duplicates one stored seed-entry array defensively.
|
|
202
|
+
def duplicate_entries(entries)
|
|
203
|
+
Array(entries).map(&:dup)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Emits one debug event while the seed graph is being built.
|
|
207
|
+
def debug_relationship_event(document:, definition:, event:, details:)
|
|
208
|
+
@debug_logger.relationship_event(
|
|
209
|
+
document: document,
|
|
210
|
+
definition: definition,
|
|
211
|
+
area: 'upgrading',
|
|
212
|
+
event: event,
|
|
213
|
+
details: details
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
end
|
|
222
|
+
end
|