metanorma-release 0.2.2 → 0.2.3
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/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +250 -319
- data/README.adoc +120 -233
- data/Rakefile +2 -2
- data/exe/metanorma-release +2 -2
- data/lib/metanorma/release/aggregation_pipeline.rb +59 -45
- data/lib/metanorma/release/asset_processor.rb +10 -8
- data/lib/metanorma/release/cache_store.rb +6 -6
- data/lib/metanorma/release/change_detector.rb +7 -3
- data/lib/metanorma/release/channel.rb +13 -39
- data/lib/metanorma/release/channel_filter.rb +26 -10
- data/lib/metanorma/release/cli.rb +129 -100
- data/lib/metanorma/release/commands/aggregate.rb +39 -54
- data/lib/metanorma/release/commands/package.rb +20 -12
- data/lib/metanorma/release/commands/{publish.rb → release_command.rb} +20 -12
- data/lib/metanorma/release/config.rb +104 -0
- data/lib/metanorma/release/content_hash.rb +11 -3
- data/lib/metanorma/release/delta_state.rb +55 -18
- data/lib/metanorma/release/file_routing.rb +8 -5
- data/lib/metanorma/release/index.rb +132 -0
- data/lib/metanorma/release/interfaces.rb +15 -15
- data/lib/metanorma/release/platform/github/manifest_reader.rb +4 -4
- data/lib/metanorma/release/platform/github/publisher.rb +23 -11
- data/lib/metanorma/release/platform/github/release_fetcher.rb +12 -3
- data/lib/metanorma/release/platform/github.rb +10 -7
- data/lib/metanorma/release/platform/local/directory_discoverer.rb +1 -1
- data/lib/metanorma/release/platform/local/fetcher.rb +17 -12
- data/lib/metanorma/release/platform/local/publisher.rb +9 -7
- data/lib/metanorma/release/platform/local.rb +4 -4
- data/lib/metanorma/release/platform/null/publisher.rb +3 -2
- data/lib/metanorma/release/platform/null.rb +1 -1
- data/lib/metanorma/release/platform.rb +3 -3
- data/lib/metanorma/release/platform_factory.rb +48 -29
- data/lib/metanorma/release/publication.rb +335 -0
- data/lib/metanorma/release/release_pipeline.rb +85 -52
- data/lib/metanorma/release/repo_ref.rb +5 -2
- data/lib/metanorma/release/site.rb +66 -0
- data/lib/metanorma/release/slug_strategy.rb +163 -0
- data/lib/metanorma/release/version.rb +1 -1
- data/lib/metanorma/release/zip_packager.rb +31 -8
- data/lib/metanorma/release.rb +68 -94
- metadata +22 -26
- data/lib/metanorma/release/aggregation_interfaces.rb +0 -27
- data/lib/metanorma/release/channel_audience.rb +0 -24
- data/lib/metanorma/release/channel_config.rb +0 -55
- data/lib/metanorma/release/channel_manifest.rb +0 -192
- data/lib/metanorma/release/channel_registry.rb +0 -60
- data/lib/metanorma/release/config_fetcher.rb +0 -11
- data/lib/metanorma/release/config_locator.rb +0 -37
- data/lib/metanorma/release/config_resolver.rb +0 -37
- data/lib/metanorma/release/document_id.rb +0 -45
- data/lib/metanorma/release/document_index.rb +0 -183
- data/lib/metanorma/release/document_metadata.rb +0 -39
- data/lib/metanorma/release/document_stage.rb +0 -86
- data/lib/metanorma/release/document_type.rb +0 -55
- data/lib/metanorma/release/document_version.rb +0 -50
- data/lib/metanorma/release/naming_strategy.rb +0 -158
- data/lib/metanorma/release/platform/github/config_fetcher.rb +0 -40
- data/lib/metanorma/release/platform/local/config_fetcher.rb +0 -20
- data/lib/metanorma/release/rake_tasks.rb +0 -71
- data/lib/metanorma/release/relaton_enricher.rb +0 -138
- data/lib/metanorma/release/release_metadata.rb +0 -79
- data/lib/metanorma/release/release_tag.rb +0 -49
- data/lib/metanorma/release/rxl_extractor.rb +0 -115
- data/lib/metanorma/release/stage_filter.rb +0 -18
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'yaml'
|
|
4
|
-
|
|
5
|
-
module Metanorma
|
|
6
|
-
module Release
|
|
7
|
-
class ChannelConfig
|
|
8
|
-
def self.from_yaml(yaml_string)
|
|
9
|
-
data = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
|
|
10
|
-
raise ArgumentError, 'Invalid channel config YAML' unless data.is_a?(Hash)
|
|
11
|
-
|
|
12
|
-
registry = ChannelRegistry.from_yaml(yaml_string)
|
|
13
|
-
defaults = data['defaults'] || {}
|
|
14
|
-
default_visibility = defaults['visibility'] || 'public'
|
|
15
|
-
default_channels = parse_channels(defaults['channels'])
|
|
16
|
-
|
|
17
|
-
new(registry: registry, default_visibility: default_visibility,
|
|
18
|
-
default_channels: default_channels)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.from_file(path)
|
|
22
|
-
if File.directory?(path)
|
|
23
|
-
channels_yml = File.join(path, 'channels.yml')
|
|
24
|
-
raise ArgumentError, "Channel config file not found: #{path}" unless File.exist?(channels_yml)
|
|
25
|
-
|
|
26
|
-
return from_file(channels_yml)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
raise ArgumentError, "Channel config file not found: #{path}" unless File.exist?(path)
|
|
30
|
-
|
|
31
|
-
from_yaml(File.read(path))
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.empty
|
|
35
|
-
new(registry: ChannelRegistry.all_allowed,
|
|
36
|
-
default_visibility: 'public', default_channels: [])
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def initialize(registry:, default_visibility:, default_channels:)
|
|
40
|
-
@registry = registry
|
|
41
|
-
@default_visibility = default_visibility
|
|
42
|
-
@default_channels = default_channels.freeze
|
|
43
|
-
freeze
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
attr_reader :registry, :default_visibility, :default_channels
|
|
47
|
-
|
|
48
|
-
def self.parse_channels(list)
|
|
49
|
-
return [] unless list
|
|
50
|
-
|
|
51
|
-
list.map { |c| Channel.parse(c.to_s) }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'yaml'
|
|
4
|
-
|
|
5
|
-
module Metanorma
|
|
6
|
-
module Release
|
|
7
|
-
class DocumentReleasePolicy
|
|
8
|
-
def self.from_defaults(visibility, channels)
|
|
9
|
-
ch = build_channels(visibility, channels)
|
|
10
|
-
is_released = visibility != 'private' || !ch.empty?
|
|
11
|
-
new(release: is_released, channels: ch, stage_allow_list: nil)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def self.from_entry(entry)
|
|
15
|
-
ch = build_channels(entry.visibility, entry.channels)
|
|
16
|
-
new(release: true, channels: ch, stage_allow_list: entry.stages_set)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.not_released
|
|
20
|
-
new(release: false, channels: [].freeze, stage_allow_list: nil)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def initialize(release:, channels:, stage_allow_list:)
|
|
24
|
-
@release = release
|
|
25
|
-
@channels = channels.freeze
|
|
26
|
-
@stage_allow_list = stage_allow_list
|
|
27
|
-
freeze
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def release?
|
|
31
|
-
@release
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
attr_reader :channels, :stage_allow_list
|
|
35
|
-
|
|
36
|
-
def self.build_channels(visibility, explicit_channels)
|
|
37
|
-
return explicit_channels if explicit_channels && !explicit_channels.empty?
|
|
38
|
-
|
|
39
|
-
case visibility
|
|
40
|
-
when 'public' then [Channel.public('default')]
|
|
41
|
-
when 'members' then [Channel.members('default')]
|
|
42
|
-
else [].freeze
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
class ManifestEntry
|
|
48
|
-
attr_reader :source, :pattern, :visibility, :channels, :stages
|
|
49
|
-
|
|
50
|
-
def initialize(source:, pattern:, visibility:, channels:, stages:)
|
|
51
|
-
@source = source
|
|
52
|
-
@pattern = pattern
|
|
53
|
-
@visibility = visibility
|
|
54
|
-
@channels = channels
|
|
55
|
-
@stages = stages
|
|
56
|
-
freeze
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def match_priority
|
|
60
|
-
return 100 if source
|
|
61
|
-
return 50 + pattern.to_s.length if pattern
|
|
62
|
-
|
|
63
|
-
0
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def stages_set
|
|
67
|
-
return nil if stages.nil? || stages.empty?
|
|
68
|
-
|
|
69
|
-
Set.new(stages.map(&:downcase))
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
class ChannelManifest
|
|
74
|
-
def self.parse(yaml_hash)
|
|
75
|
-
defaults = yaml_hash['defaults'] || {}
|
|
76
|
-
default_visibility = defaults['visibility'] || 'public'
|
|
77
|
-
default_channels = parse_channels(defaults['channels'])
|
|
78
|
-
entries = parse_entries(yaml_hash['documents'] || [])
|
|
79
|
-
config_source = yaml_hash['config']
|
|
80
|
-
|
|
81
|
-
new(entries: entries, default_visibility: default_visibility,
|
|
82
|
-
default_channels: default_channels, explicit: true, config_source: config_source)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def self.from_yaml(yaml_string)
|
|
86
|
-
yaml = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
|
|
87
|
-
raise ArgumentError, 'Manifest YAML is empty' unless yaml.is_a?(Hash)
|
|
88
|
-
|
|
89
|
-
parse(yaml)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def self.from_file(path)
|
|
93
|
-
raise ArgumentError, "Manifest file not found: #{path}" unless File.exist?(path)
|
|
94
|
-
|
|
95
|
-
from_yaml(File.read(path))
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def self.all_public
|
|
99
|
-
new(entries: [], default_visibility: 'public',
|
|
100
|
-
default_channels: [Channel.public('default')], explicit: false, config_source: nil)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def self.all_private
|
|
104
|
-
new(entries: [], default_visibility: 'private',
|
|
105
|
-
default_channels: [], explicit: false, config_source: nil)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def initialize(entries:, default_visibility:, default_channels:, explicit:, config_source: nil)
|
|
109
|
-
@entries = entries
|
|
110
|
-
@default_visibility = default_visibility
|
|
111
|
-
@default_channels = default_channels.freeze
|
|
112
|
-
@explicit = explicit
|
|
113
|
-
@config_source = config_source
|
|
114
|
-
freeze
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def resolve(document)
|
|
118
|
-
return default_policy unless @explicit
|
|
119
|
-
|
|
120
|
-
entry = find_best_match(document)
|
|
121
|
-
return DocumentReleasePolicy.from_defaults(@default_visibility, @default_channels) unless entry
|
|
122
|
-
|
|
123
|
-
DocumentReleasePolicy.from_entry(entry)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def list_all
|
|
127
|
-
@entries
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def all_channels
|
|
131
|
-
(@default_channels + @entries.flat_map(&:channels)).uniq
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def explicit?
|
|
135
|
-
@explicit
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
attr_reader :config_source
|
|
139
|
-
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
def default_policy
|
|
143
|
-
DocumentReleasePolicy.from_defaults(@default_visibility, @default_channels)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def find_best_match(document)
|
|
147
|
-
source = extract_source(document)
|
|
148
|
-
matches = @entries.select { |e| entry_matches?(e, source) }
|
|
149
|
-
return nil if matches.empty?
|
|
150
|
-
|
|
151
|
-
matches.max_by(&:match_priority)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def entry_matches?(entry, source)
|
|
155
|
-
return false unless source
|
|
156
|
-
return true if entry.source && entry.source == source
|
|
157
|
-
return true if entry.pattern && File.fnmatch?(entry.pattern, source)
|
|
158
|
-
|
|
159
|
-
false
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def extract_source(document)
|
|
163
|
-
document['source_path']
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def self.parse_channels(channel_list)
|
|
167
|
-
return [] unless channel_list
|
|
168
|
-
|
|
169
|
-
channel_list.map { |c| Channel.parse(c.to_s) }
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def self.parse_entries(documents)
|
|
173
|
-
documents.map do |doc|
|
|
174
|
-
validate_entry!(doc)
|
|
175
|
-
ManifestEntry.new(
|
|
176
|
-
source: doc['source'],
|
|
177
|
-
pattern: doc['pattern'],
|
|
178
|
-
visibility: doc['visibility'],
|
|
179
|
-
channels: parse_channels(doc['channels']),
|
|
180
|
-
stages: doc['stages']
|
|
181
|
-
)
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def self.validate_entry!(doc)
|
|
186
|
-
return unless doc['source']&.include?('..')
|
|
187
|
-
|
|
188
|
-
raise ArgumentError, "Path traversal detected in manifest source: #{doc['source']}"
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'yaml'
|
|
4
|
-
|
|
5
|
-
module Metanorma
|
|
6
|
-
module Release
|
|
7
|
-
class ChannelRegistry
|
|
8
|
-
def self.from_yaml(yaml_string)
|
|
9
|
-
data = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
|
|
10
|
-
raise ArgumentError, 'Invalid channel registry YAML' unless data.is_a?(Hash)
|
|
11
|
-
|
|
12
|
-
channels = parse_channel_list(data['channels'])
|
|
13
|
-
new(channels: channels)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.from_file(path)
|
|
17
|
-
raise ArgumentError, "Channel registry file not found: #{path}" unless File.exist?(path)
|
|
18
|
-
|
|
19
|
-
from_yaml(File.read(path))
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.all_allowed
|
|
23
|
-
new(channels: [])
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def initialize(channels:)
|
|
27
|
-
@channels = channels.freeze
|
|
28
|
-
@channel_set = channels.to_set
|
|
29
|
-
freeze
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def valid?(channel)
|
|
33
|
-
return true if @channels.empty?
|
|
34
|
-
|
|
35
|
-
@channel_set.include?(channel)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def include?(channel_or_string)
|
|
39
|
-
valid?(Channel.parse(channel_or_string.to_s))
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
attr_reader :channels
|
|
43
|
-
|
|
44
|
-
def empty?
|
|
45
|
-
@channels.empty?
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.parse_channel_list(list)
|
|
49
|
-
return [] unless list
|
|
50
|
-
|
|
51
|
-
list.filter_map do |entry|
|
|
52
|
-
case entry
|
|
53
|
-
when Hash then Channel.parse(entry['name'].to_s)
|
|
54
|
-
when String then Channel.parse(entry)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Metanorma
|
|
4
|
-
module Release
|
|
5
|
-
class ConfigLocator
|
|
6
|
-
CONFIG_FILES = ['.metanorma.yml', '.metanorma.yaml'].freeze
|
|
7
|
-
CONFIG_DIRS = ['.metanorma'].freeze
|
|
8
|
-
|
|
9
|
-
def self.find(start_dir = Dir.pwd)
|
|
10
|
-
new.find(start_dir)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def find(start_dir)
|
|
14
|
-
dir = File.expand_path(start_dir)
|
|
15
|
-
loop do
|
|
16
|
-
CONFIG_FILES.each do |name|
|
|
17
|
-
path = File.join(dir, name)
|
|
18
|
-
return ChannelConfig.from_file(path) if File.exist?(path)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
CONFIG_DIRS.each do |name|
|
|
22
|
-
path = File.join(dir, name)
|
|
23
|
-
next unless File.directory?(path)
|
|
24
|
-
|
|
25
|
-
channels = File.join(path, 'channels.yml')
|
|
26
|
-
return ChannelConfig.from_file(channels) if File.exist?(channels)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
parent = File.dirname(dir)
|
|
30
|
-
return nil if parent == dir
|
|
31
|
-
|
|
32
|
-
dir = parent
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Metanorma
|
|
4
|
-
module Release
|
|
5
|
-
module ConfigResolver
|
|
6
|
-
def resolve_channel_config(cli_source, manifest)
|
|
7
|
-
return fetch_config(cli_source) if cli_source
|
|
8
|
-
return fetch_config(manifest.config_source) if manifest&.config_source
|
|
9
|
-
|
|
10
|
-
found = ConfigLocator.find
|
|
11
|
-
return found if found
|
|
12
|
-
|
|
13
|
-
ChannelConfig.empty
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def load_manifest(path)
|
|
17
|
-
return nil unless path && File.exist?(path)
|
|
18
|
-
|
|
19
|
-
ChannelManifest.from_file(path)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def fetch_config(source)
|
|
25
|
-
if source.start_with?('local:')
|
|
26
|
-
Platform::Local::ConfigFetcher.new.fetch(source)
|
|
27
|
-
elsif source.include?('/')
|
|
28
|
-
Platform::Local::ConfigFetcher.new.fetch("local:#{source}")
|
|
29
|
-
else
|
|
30
|
-
require 'octokit'
|
|
31
|
-
client = PlatformFactory.build_github_client(nil)
|
|
32
|
-
Platform::GitHub::ConfigFetcher.new(client: client).fetch(source)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Metanorma
|
|
4
|
-
module Release
|
|
5
|
-
class DocumentId
|
|
6
|
-
def self.from_raw(raw_identifier)
|
|
7
|
-
normalized = raw_identifier.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
|
|
8
|
-
raise ArgumentError, 'Document ID cannot be empty' if normalized.empty?
|
|
9
|
-
|
|
10
|
-
new(normalized)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.from_normalized(value)
|
|
14
|
-
raise ArgumentError, 'Document ID cannot be empty' if value.nil? || value.strip.empty?
|
|
15
|
-
|
|
16
|
-
new(value.to_s.strip)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def initialize(value)
|
|
20
|
-
@value = value
|
|
21
|
-
freeze
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def to_s
|
|
25
|
-
@value
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def tag_prefix
|
|
29
|
-
@value
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def file_name
|
|
33
|
-
@value
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def eql?(other)
|
|
37
|
-
other.is_a?(self.class) && @value == other.to_s
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def hash
|
|
41
|
-
@value.hash
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
module Metanorma
|
|
6
|
-
module Release
|
|
7
|
-
DocumentFile = Struct.new(:name, :path, keyword_init: true) do
|
|
8
|
-
def extension
|
|
9
|
-
File.extname(name).delete_prefix('.')
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
DocumentSource = Struct.new(
|
|
14
|
-
:owner, :repo, :tag, :release_url, :release_date,
|
|
15
|
-
keyword_init: true
|
|
16
|
-
) do
|
|
17
|
-
def repo_key
|
|
18
|
-
"#{owner}/#{repo}"
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
IndexParameters = Struct.new(
|
|
23
|
-
:organizations, :channels, :topic, :repo_count,
|
|
24
|
-
keyword_init: true
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
IndexSummary = Struct.new(
|
|
28
|
-
:repo_count, :document_count, :channels_found,
|
|
29
|
-
keyword_init: true
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
class AggregatedDocument
|
|
33
|
-
def self.from_h(hash)
|
|
34
|
-
files = (hash['files'] || []).map do |f|
|
|
35
|
-
DocumentFile.new(name: f['name'], path: f['path'])
|
|
36
|
-
end
|
|
37
|
-
source_data = hash['source'] || {}
|
|
38
|
-
source = DocumentSource.new(
|
|
39
|
-
owner: source_data['owner'], repo: source_data['repo'],
|
|
40
|
-
tag: source_data['tag'], release_url: source_data['releaseUrl'],
|
|
41
|
-
release_date: source_data['releaseDate']
|
|
42
|
-
)
|
|
43
|
-
new(
|
|
44
|
-
id: hash['id'], title: hash['title'], edition: hash['edition'],
|
|
45
|
-
stage: hash['stage'], doctype: hash.fetch('doctype', ''),
|
|
46
|
-
channels: hash['channels'] || [], formats: hash['formats'] || [],
|
|
47
|
-
flavor: hash['flavor'], content_hash: hash['contentHash'],
|
|
48
|
-
source: source, files: files
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
attr_reader :id, :title, :edition, :stage, :doctype, :channels,
|
|
53
|
-
:formats, :flavor, :content_hash, :source, :files
|
|
54
|
-
|
|
55
|
-
def initialize(id:, title:, edition:, stage:, doctype:, channels:,
|
|
56
|
-
formats:, flavor:, content_hash:, source:, files:)
|
|
57
|
-
@id = id
|
|
58
|
-
@title = title
|
|
59
|
-
@edition = edition
|
|
60
|
-
@stage = stage
|
|
61
|
-
@doctype = doctype
|
|
62
|
-
@channels = channels.freeze
|
|
63
|
-
@formats = formats.freeze
|
|
64
|
-
@flavor = flavor
|
|
65
|
-
@content_hash = content_hash
|
|
66
|
-
@source = source
|
|
67
|
-
@files = files.freeze
|
|
68
|
-
freeze
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def to_h
|
|
72
|
-
{
|
|
73
|
-
'id' => id, 'title' => title, 'edition' => edition,
|
|
74
|
-
'stage' => stage, 'doctype' => doctype,
|
|
75
|
-
'channels' => channels, 'formats' => formats,
|
|
76
|
-
'flavor' => flavor, 'contentHash' => content_hash,
|
|
77
|
-
'source' => {
|
|
78
|
-
'owner' => source.owner, 'repo' => source.repo,
|
|
79
|
-
'tag' => source.tag, 'releaseUrl' => source.release_url,
|
|
80
|
-
'releaseDate' => source.release_date
|
|
81
|
-
},
|
|
82
|
-
'files' => files.map { |f| { 'name' => f.name, 'path' => f.path } }
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
class DocumentIndex
|
|
88
|
-
SCHEMA_VERSION = 1
|
|
89
|
-
|
|
90
|
-
SchemaError = Class.new(StandardError)
|
|
91
|
-
|
|
92
|
-
def self.from_json(json_string)
|
|
93
|
-
data = JSON.parse(json_string)
|
|
94
|
-
validate!(data)
|
|
95
|
-
new(
|
|
96
|
-
documents: (data['documents'] || []).map { |d| AggregatedDocument.from_h(d) },
|
|
97
|
-
parameters: IndexParameters.new(
|
|
98
|
-
organizations: data.dig('parameters', 'organizations') || [],
|
|
99
|
-
channels: data.dig('parameters', 'channels') || [],
|
|
100
|
-
topic: data.dig('parameters', 'topic'),
|
|
101
|
-
repo_count: data.dig('parameters', 'repoCount') || 0
|
|
102
|
-
),
|
|
103
|
-
generated_at: data['generatedAt']
|
|
104
|
-
)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def self.from_documents(documents, parameters:)
|
|
108
|
-
new(documents: documents, parameters: parameters)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def initialize(documents:, parameters:, generated_at: nil)
|
|
112
|
-
@documents = documents.freeze
|
|
113
|
-
@parameters = parameters
|
|
114
|
-
@generated_at = generated_at || Time.now.utc.iso8601
|
|
115
|
-
freeze
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
attr_reader :documents, :parameters
|
|
119
|
-
|
|
120
|
-
def summary
|
|
121
|
-
IndexSummary.new(
|
|
122
|
-
repo_count: @parameters.repo_count,
|
|
123
|
-
document_count: @documents.length,
|
|
124
|
-
channels_found: channels
|
|
125
|
-
)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def channels
|
|
129
|
-
@documents.flat_map(&:channels).uniq.sort
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def document_count
|
|
133
|
-
@documents.length
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def empty?
|
|
137
|
-
@documents.empty?
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def to_h
|
|
141
|
-
{
|
|
142
|
-
'version' => SCHEMA_VERSION,
|
|
143
|
-
'generatedAt' => @generated_at,
|
|
144
|
-
'parameters' => {
|
|
145
|
-
'organizations' => @parameters.organizations,
|
|
146
|
-
'channels' => @parameters.channels,
|
|
147
|
-
'topic' => @parameters.topic,
|
|
148
|
-
'repoCount' => @parameters.repo_count
|
|
149
|
-
},
|
|
150
|
-
'summary' => {
|
|
151
|
-
'repoCount' => summary.repo_count,
|
|
152
|
-
'documentCount' => summary.document_count,
|
|
153
|
-
'channelsFound' => summary.channels_found
|
|
154
|
-
},
|
|
155
|
-
'documents' => @documents.map(&:to_h)
|
|
156
|
-
}
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def to_json(*_args)
|
|
160
|
-
JSON.generate(to_h)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def write(path)
|
|
164
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
165
|
-
File.write(path, to_json)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def self.validate!(data)
|
|
169
|
-
raise SchemaError, "Missing 'version' field" unless data.key?('version')
|
|
170
|
-
unless data['version'] == SCHEMA_VERSION
|
|
171
|
-
raise SchemaError,
|
|
172
|
-
"Unsupported schema version: #{data['version']}. Expected #{SCHEMA_VERSION}"
|
|
173
|
-
end
|
|
174
|
-
raise SchemaError, "Missing 'documents' field" unless data.key?('documents')
|
|
175
|
-
|
|
176
|
-
data['documents'].each do |doc|
|
|
177
|
-
raise SchemaError, "Document missing required field 'id'" unless doc.key?('id')
|
|
178
|
-
raise SchemaError, "Document missing required field 'title'" unless doc.key?('title')
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Metanorma
|
|
4
|
-
module Release
|
|
5
|
-
class DocumentMetadata
|
|
6
|
-
attr_reader :id, :title, :version, :doctype, :document_type,
|
|
7
|
-
:flavor, :revdate, :source_path, :output_dir,
|
|
8
|
-
:formats, :file_base_name
|
|
9
|
-
|
|
10
|
-
def initialize(id:, title:, version:, doctype:, document_type:,
|
|
11
|
-
flavor:, revdate:, source_path:, output_dir:,
|
|
12
|
-
formats:, file_base_name:)
|
|
13
|
-
@id = id
|
|
14
|
-
@title = title
|
|
15
|
-
@version = version
|
|
16
|
-
@doctype = doctype
|
|
17
|
-
@document_type = document_type
|
|
18
|
-
@flavor = flavor
|
|
19
|
-
@revdate = revdate
|
|
20
|
-
@source_path = source_path
|
|
21
|
-
@output_dir = output_dir
|
|
22
|
-
@formats = formats.freeze
|
|
23
|
-
@file_base_name = file_base_name
|
|
24
|
-
@lookup = {
|
|
25
|
-
'id' => @id, 'title' => @title, 'doctype' => @doctype,
|
|
26
|
-
'document_type' => @document_type, 'flavor' => @flavor,
|
|
27
|
-
'revdate' => @revdate, 'source_path' => @source_path,
|
|
28
|
-
'output_dir' => @output_dir, 'formats' => @formats,
|
|
29
|
-
'file_base_name' => @file_base_name
|
|
30
|
-
}.freeze
|
|
31
|
-
freeze
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def [](key)
|
|
35
|
-
@lookup[key]
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|