metanorma-release 0.2.1

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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +1 -0
  4. data/.rubocop_todo.yml +504 -0
  5. data/CHANGELOG.md +15 -0
  6. data/PROMPT.md +282 -0
  7. data/README.adoc +430 -0
  8. data/Rakefile +8 -0
  9. data/exe/mn-release +6 -0
  10. data/lib/metanorma/release/aggregation_interfaces.rb +33 -0
  11. data/lib/metanorma/release/aggregation_pipeline.rb +155 -0
  12. data/lib/metanorma/release/asset_processor.rb +58 -0
  13. data/lib/metanorma/release/cache_store.rb +86 -0
  14. data/lib/metanorma/release/change_detector.rb +20 -0
  15. data/lib/metanorma/release/channel.rb +64 -0
  16. data/lib/metanorma/release/channel_audience.rb +24 -0
  17. data/lib/metanorma/release/channel_config.rb +55 -0
  18. data/lib/metanorma/release/channel_filter.rb +26 -0
  19. data/lib/metanorma/release/channel_manifest.rb +192 -0
  20. data/lib/metanorma/release/channel_registry.rb +60 -0
  21. data/lib/metanorma/release/cli.rb +129 -0
  22. data/lib/metanorma/release/commands/aggregate.rb +126 -0
  23. data/lib/metanorma/release/commands/package.rb +46 -0
  24. data/lib/metanorma/release/commands/publish.rb +51 -0
  25. data/lib/metanorma/release/config_fetcher.rb +11 -0
  26. data/lib/metanorma/release/config_locator.rb +37 -0
  27. data/lib/metanorma/release/config_resolver.rb +37 -0
  28. data/lib/metanorma/release/content_hash.rb +51 -0
  29. data/lib/metanorma/release/delta_state.rb +108 -0
  30. data/lib/metanorma/release/document_id.rb +45 -0
  31. data/lib/metanorma/release/document_index.rb +183 -0
  32. data/lib/metanorma/release/document_metadata.rb +39 -0
  33. data/lib/metanorma/release/document_stage.rb +86 -0
  34. data/lib/metanorma/release/document_type.rb +55 -0
  35. data/lib/metanorma/release/document_version.rb +50 -0
  36. data/lib/metanorma/release/file_routing.rb +51 -0
  37. data/lib/metanorma/release/interfaces.rb +47 -0
  38. data/lib/metanorma/release/naming_strategy.rb +158 -0
  39. data/lib/metanorma/release/platform/github/config_fetcher.rb +40 -0
  40. data/lib/metanorma/release/platform/github/manifest_reader.rb +32 -0
  41. data/lib/metanorma/release/platform/github/publisher.rb +73 -0
  42. data/lib/metanorma/release/platform/github/release_fetcher.rb +52 -0
  43. data/lib/metanorma/release/platform/github/topic_discoverer.rb +29 -0
  44. data/lib/metanorma/release/platform/github.rb +25 -0
  45. data/lib/metanorma/release/platform/local/config_fetcher.rb +20 -0
  46. data/lib/metanorma/release/platform/local/directory_discoverer.rb +26 -0
  47. data/lib/metanorma/release/platform/local/fetcher.rb +76 -0
  48. data/lib/metanorma/release/platform/local/publisher.rb +44 -0
  49. data/lib/metanorma/release/platform/local.rb +14 -0
  50. data/lib/metanorma/release/platform/null/publisher.rb +17 -0
  51. data/lib/metanorma/release/platform/null.rb +11 -0
  52. data/lib/metanorma/release/platform.rb +11 -0
  53. data/lib/metanorma/release/platform_factory.rb +78 -0
  54. data/lib/metanorma/release/rake_tasks.rb +71 -0
  55. data/lib/metanorma/release/relaton_enricher.rb +138 -0
  56. data/lib/metanorma/release/release_metadata.rb +79 -0
  57. data/lib/metanorma/release/release_pipeline.rb +115 -0
  58. data/lib/metanorma/release/release_tag.rb +49 -0
  59. data/lib/metanorma/release/repo_ref.rb +34 -0
  60. data/lib/metanorma/release/rxl_extractor.rb +115 -0
  61. data/lib/metanorma/release/stage_filter.rb +18 -0
  62. data/lib/metanorma/release/version.rb +7 -0
  63. data/lib/metanorma/release/zip_packager.rb +37 -0
  64. data/lib/metanorma/release.rb +116 -0
  65. metadata +156 -0
@@ -0,0 +1,39 @@
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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ class DocumentStage
6
+ PUBLISHED_NAMES = %w[published in-force approved standard].freeze
7
+
8
+ STAGE_ABBREVS = {
9
+ 'working-draft' => 'wd',
10
+ 'committee-draft' => 'cd',
11
+ 'draft-standard' => 'ds',
12
+ 'final-draft' => 'fd',
13
+ 'proposal' => 'proposal',
14
+ 'informational' => 'info',
15
+ 'withdrawn' => 'withdrawn',
16
+ 'cancelled' => 'cancelled'
17
+ }.freeze
18
+
19
+ ISO_STAGE_MAP = {
20
+ 20 => 'working-draft',
21
+ 30 => 'committee-draft',
22
+ 40 => 'draft-standard',
23
+ 50 => 'final-draft',
24
+ 60 => 'published',
25
+ 95 => 'withdrawn'
26
+ }.freeze
27
+
28
+ def self.from_status(status_string)
29
+ raise ArgumentError, 'Stage cannot be empty' if status_string.nil? || status_string.strip.empty?
30
+
31
+ normalized = status_string.to_s.downcase.strip.gsub(/\s+/, '-')
32
+ new(normalized)
33
+ end
34
+
35
+ def self.from_iso_stage(stage, _substage = nil)
36
+ name = ISO_STAGE_MAP[stage.to_i] || ISO_STAGE_MAP.values.first
37
+ new(name)
38
+ end
39
+
40
+ def self.published
41
+ new('published')
42
+ end
43
+
44
+ def self.working_draft
45
+ new('working-draft')
46
+ end
47
+
48
+ def initialize(name)
49
+ @name = name
50
+ freeze
51
+ end
52
+
53
+ def to_s
54
+ @name
55
+ end
56
+
57
+ def published?
58
+ PUBLISHED_NAMES.include?(@name)
59
+ end
60
+
61
+ def draft?
62
+ !published? && @name != 'withdrawn' && @name != 'cancelled'
63
+ end
64
+
65
+ def withdrawn?
66
+ @name == 'withdrawn'
67
+ end
68
+
69
+ def cancelled?
70
+ @name == 'cancelled'
71
+ end
72
+
73
+ def tag_suffix
74
+ STAGE_ABBREVS[@name].to_s
75
+ end
76
+
77
+ def eql?(other)
78
+ other.is_a?(self.class) && @name == other.to_s
79
+ end
80
+
81
+ def hash
82
+ @name.hash
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module DocumentType
6
+ STANDARD = 'standard'
7
+ IETF_DRAFT = 'ietf-draft'
8
+ IETF_RFC = 'ietf-rfc'
9
+ ISO = 'iso'
10
+ IEC = 'iec'
11
+ IEEE = 'ieee'
12
+ ITU = 'itu'
13
+ BIPM = 'bipm'
14
+ IHO = 'iho'
15
+ OGC = 'ogc'
16
+ OIML = 'oiml'
17
+ UN = 'un'
18
+ CSA = 'csa'
19
+ PDFA = 'pdfa'
20
+ MPFA = 'mpfa'
21
+ M3AAWG = 'm3aawg'
22
+ RIBOSE = 'ribose'
23
+
24
+ DETECTION_RULES = [
25
+ [/^RFC\s/i, IETF_RFC],
26
+ [/^draft-/i, IETF_DRAFT],
27
+ [/^ISO/i, ISO],
28
+ [/^IEC/i, IEC],
29
+ [/^IEEE/i, IEEE],
30
+ [/^ITU-/i, ITU],
31
+ [/^BIPM/i, BIPM],
32
+ [/^[A-Z]-\d/i, IHO],
33
+ [/^\d{2}-\d{2,3}/, OGC],
34
+ [/^OIML/i, OIML],
35
+ [/^GE\./i, UN],
36
+ [/^csa-/i, CSA],
37
+ [/^(AN|BPG|TN)\s/i, PDFA],
38
+ [%r{^SU/}i, MPFA],
39
+ [/^M3AAWG/i, M3AAWG],
40
+ [/^Ribose/i, RIBOSE]
41
+ ].freeze
42
+
43
+ def self.from_identifier(raw_id)
44
+ id = raw_id.to_s.strip
45
+ return STANDARD if id.empty?
46
+
47
+ DETECTION_RULES.each do |pattern, type|
48
+ return type if id.match?(pattern)
49
+ end
50
+
51
+ STANDARD
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ class DocumentVersion
6
+ attr_reader :edition, :stage
7
+
8
+ def self.from(edition, stage)
9
+ ed = edition.to_s.strip
10
+ ed = '0' if ed.empty?
11
+ new(edition: ed, stage: stage)
12
+ end
13
+
14
+ def self.published(edition:)
15
+ new(edition: edition.to_s.strip, stage: DocumentStage.published)
16
+ end
17
+
18
+ def initialize(edition:, stage:)
19
+ @edition = edition
20
+ @stage = stage
21
+ freeze
22
+ end
23
+
24
+ def tag_component
25
+ base = "ed#{edition}"
26
+ return base if stage.published?
27
+
28
+ suffix = stage.tag_suffix
29
+ suffix.empty? ? base : "#{base}-#{suffix}"
30
+ end
31
+
32
+ def pre_release?
33
+ stage.draft?
34
+ end
35
+
36
+ def file_name(doc_id)
37
+ base = "#{doc_id}-#{tag_component}"
38
+ "#{base}.zip"
39
+ end
40
+
41
+ def eql?(other)
42
+ other.is_a?(self.class) && edition == other.edition && stage.eql?(other.stage)
43
+ end
44
+
45
+ def hash
46
+ [edition, stage].hash
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module FileRouting
6
+ def compute_path(file_name, metadata)
7
+ raise NotImplementedError, "#{self.class} must implement #compute_path"
8
+ end
9
+ end
10
+
11
+ class ByDocument
12
+ include FileRouting
13
+
14
+ def compute_path(file_name, metadata)
15
+ "#{metadata['id']}/#{file_name}"
16
+ end
17
+ end
18
+
19
+ class Flat
20
+ include FileRouting
21
+
22
+ def compute_path(file_name, _metadata)
23
+ file_name
24
+ end
25
+ end
26
+
27
+ class ByFormat
28
+ include FileRouting
29
+
30
+ def compute_path(file_name, _metadata)
31
+ ext = File.extname(file_name).delete_prefix('.')
32
+ "#{ext}/#{file_name}"
33
+ end
34
+ end
35
+
36
+ module FileRoutingFactory
37
+ ROUTING_MAP = {
38
+ 'by-document' => ByDocument,
39
+ 'flat' => Flat,
40
+ 'by-format' => ByFormat
41
+ }.freeze
42
+
43
+ def self.from_name(name)
44
+ klass = ROUTING_MAP[name]
45
+ raise ArgumentError, "Unknown routing mode: #{name}. Available: #{ROUTING_MAP.keys.join(', ')}" unless klass
46
+
47
+ klass.new
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module Extractor
6
+ def discover(output_dir)
7
+ raise NotImplementedError, "#{self.class} must implement #discover"
8
+ end
9
+
10
+ def extract(rxl_path)
11
+ raise NotImplementedError, "#{self.class} must implement #extract"
12
+ end
13
+ end
14
+
15
+ module Filter
16
+ def apply(documents)
17
+ raise NotImplementedError, "#{self.class} must implement #apply"
18
+ end
19
+ end
20
+
21
+ module ChangeDetector
22
+ def detect(metadata, tag, force: false)
23
+ raise NotImplementedError, "#{self.class} must implement #detect"
24
+ end
25
+ end
26
+
27
+ module Packager
28
+ def package(metadata, canonical_base:)
29
+ raise NotImplementedError, "#{self.class} must implement #package"
30
+ end
31
+ end
32
+
33
+ module Publisher
34
+ def publish(tag, artifact, metadata, channels:, force_replace: false)
35
+ raise NotImplementedError, "#{self.class} must implement #publish"
36
+ end
37
+ end
38
+
39
+ ChangeResult = Struct.new(:changed?, :current_hash, :previous_hash, keyword_init: true)
40
+ Artifact = Struct.new(:zip_path, :asset_name, :size, keyword_init: true)
41
+ PublishResult = Struct.new(:tag, :url, :created?, keyword_init: true)
42
+
43
+ ReleasedArtifact = Struct.new(:id, :tag, :url, :channels, keyword_init: true)
44
+
45
+ ReleaseResult = Struct.new(:released, :skipped, :failed, :released_artifacts, keyword_init: true)
46
+ end
47
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module NamingStrategy
6
+ def compute_tag(id, version)
7
+ raise NotImplementedError, "#{self.class} must implement #compute_tag"
8
+ end
9
+
10
+ def compute_asset_name(id, version)
11
+ raise NotImplementedError, "#{self.class} must implement #compute_asset_name"
12
+ end
13
+
14
+ def compute_canonical_base(id, version)
15
+ raise NotImplementedError, "#{self.class} must implement #compute_canonical_base"
16
+ end
17
+ end
18
+
19
+ class EditionNaming
20
+ include NamingStrategy
21
+
22
+ def compute_tag(id, version)
23
+ tag = "#{id}/#{version.tag_component}"
24
+ ReleaseTag.create(tag, pre_release: version.pre_release?)
25
+ end
26
+
27
+ def compute_asset_name(id, version)
28
+ "#{id}-#{version.tag_component}.zip"
29
+ end
30
+
31
+ def compute_canonical_base(id, version)
32
+ "#{id}-#{version.tag_component}"
33
+ end
34
+ end
35
+
36
+ class VersionNaming
37
+ include NamingStrategy
38
+
39
+ def compute_tag(id, version)
40
+ tag = "#{id}/v#{version.edition}"
41
+ ReleaseTag.create(tag, pre_release: version.pre_release?)
42
+ end
43
+
44
+ def compute_asset_name(id, version)
45
+ "#{id}-v#{version.edition}.zip"
46
+ end
47
+
48
+ def compute_canonical_base(id, version)
49
+ "#{id}-v#{version.edition}"
50
+ end
51
+ end
52
+
53
+ class InternetDraftNaming
54
+ include NamingStrategy
55
+
56
+ DRAFT_PATTERN = /\Adraft-ietf-([a-z0-9-]+?)-(\d+)\z/i
57
+
58
+ def compute_tag(id, version)
59
+ match = id.match(DRAFT_PATTERN)
60
+ return fallback_tag(id, version) unless match
61
+
62
+ name = match[1]
63
+ num = match[2]
64
+ ReleaseTag.create("id-#{name}/#{num}", pre_release: true)
65
+ end
66
+
67
+ def compute_asset_name(id, _version)
68
+ "#{id}.zip"
69
+ end
70
+
71
+ def compute_canonical_base(id, _version)
72
+ id.to_s
73
+ end
74
+
75
+ private
76
+
77
+ def fallback_tag(id, _version)
78
+ tag = "#{id}/draft"
79
+ ReleaseTag.create(tag, pre_release: true)
80
+ end
81
+ end
82
+
83
+ class RfcNaming
84
+ include NamingStrategy
85
+
86
+ def compute_tag(id, version)
87
+ tag = "#{id}/ed#{version.edition}"
88
+ ReleaseTag.create(tag, pre_release: version.pre_release?)
89
+ end
90
+
91
+ def compute_asset_name(id, _version)
92
+ "#{id}.zip"
93
+ end
94
+
95
+ def compute_canonical_base(id, version)
96
+ "#{id}-ed#{version.edition}"
97
+ end
98
+ end
99
+
100
+ class DraftSuffixNaming
101
+ include NamingStrategy
102
+
103
+ DRAFT_SUFFIX = /-d(\d+)\z/
104
+
105
+ def compute_tag(id, version)
106
+ match = id.to_s.match(DRAFT_SUFFIX)
107
+ return @fallback.compute_tag(id, version) unless match
108
+
109
+ base = id.to_s.sub(DRAFT_SUFFIX, '')
110
+ num = match[1]
111
+ ReleaseTag.create("#{base}/#{num}", pre_release: true)
112
+ end
113
+
114
+ def compute_asset_name(id, version)
115
+ match = id.to_s.match(DRAFT_SUFFIX)
116
+ return @fallback.compute_asset_name(id, version) unless match
117
+
118
+ "#{id}.zip"
119
+ end
120
+
121
+ def compute_canonical_base(id, version)
122
+ match = id.to_s.match(DRAFT_SUFFIX)
123
+ return @fallback.compute_canonical_base(id, version) unless match
124
+
125
+ id.to_s
126
+ end
127
+
128
+ def initialize
129
+ @fallback = EditionNaming.new
130
+ end
131
+ end
132
+
133
+ class NamingRegistry
134
+ def initialize(default: EditionNaming.new)
135
+ @default = default
136
+ @strategies = {}
137
+ end
138
+
139
+ def register(document_type, strategy)
140
+ @strategies[document_type] = strategy
141
+ end
142
+
143
+ def resolve(document_type)
144
+ @strategies.fetch(document_type, @default)
145
+ end
146
+
147
+ def self.default_registry
148
+ registry = new
149
+ registry.register(DocumentType::IETF_DRAFT, InternetDraftNaming.new)
150
+ registry.register(DocumentType::IETF_RFC, RfcNaming.new)
151
+ registry.register(DocumentType::IEEE, DraftSuffixNaming.new)
152
+ registry.register(DocumentType::IHO, VersionNaming.new)
153
+ registry.register(DocumentType::OGC, VersionNaming.new)
154
+ registry
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Metanorma
6
+ module Release
7
+ module Platform
8
+ module GitHub
9
+ class ConfigFetcher
10
+ include Metanorma::Release::ConfigFetcher
11
+
12
+ def initialize(client:)
13
+ @client = client
14
+ end
15
+
16
+ def fetch(source)
17
+ repo, path = parse_source(source)
18
+ content = @client.contents(repo, path: path)
19
+ return nil unless content
20
+
21
+ ChannelConfig.from_yaml(content['content'].unpack1('m0'))
22
+ rescue StandardError
23
+ nil
24
+ end
25
+
26
+ private
27
+
28
+ def parse_source(source)
29
+ if source.include?('#')
30
+ parts = source.split('#', 2)
31
+ [parts[0], parts[1]]
32
+ else
33
+ [source, 'channels.yml']
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Metanorma
6
+ module Release
7
+ module Platform
8
+ module GitHub
9
+ class ManifestReader
10
+ include Metanorma::Release::ManifestReader
11
+
12
+ def initialize(client:)
13
+ @client = client
14
+ end
15
+
16
+ def read(repo)
17
+ content = @client.contents(repo.to_s, path: 'metanorma.release.yml')
18
+ return nil unless content
19
+
20
+ yaml = content['content'].unpack1('m0')
21
+ parsed = YAML.safe_load(yaml, permitted_classes: [Symbol])
22
+ return nil unless parsed.is_a?(Hash)
23
+
24
+ (parsed['channels'] || []).map(&:to_s)
25
+ rescue StandardError
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module Platform
6
+ module GitHub
7
+ class Publisher
8
+ include Metanorma::Release::Publisher
9
+
10
+ def initialize(client:, repo:)
11
+ @client = client
12
+ @repo = repo
13
+ end
14
+
15
+ def publish(tag, artifact, metadata, channels:, force_replace: false)
16
+ tag_name = tag.to_s
17
+
18
+ if force_replace
19
+ delete_existing_release(tag_name)
20
+ return create_release(tag_name, metadata, artifact)
21
+ end
22
+
23
+ existing = find_release(tag_name)
24
+
25
+ if existing
26
+ update_release(existing, metadata)
27
+ else
28
+ create_release(tag_name, metadata, artifact)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def find_release(tag_name)
35
+ @client.releases(@repo).find { |r| r['tag_name'] == tag_name }
36
+ rescue StandardError
37
+ nil
38
+ end
39
+
40
+ def create_release(tag_name, metadata, artifact)
41
+ release = @client.create_release(
42
+ @repo, tag_name,
43
+ name: tag_name,
44
+ body: metadata.to_release_body,
45
+ prerelease: tag_name.match?(/-(wd|cd|ds|fd|proposal)$/)
46
+ )
47
+ upload_asset(release['id'], artifact)
48
+ PublishResult.new(tag: tag_name, url: release['html_url'], created?: true)
49
+ end
50
+
51
+ def update_release(release, metadata)
52
+ @client.update_release(release['url'], body: metadata.to_release_body)
53
+ PublishResult.new(tag: release['tag_name'], url: release['html_url'], created?: false)
54
+ end
55
+
56
+ def upload_asset(release_id, artifact)
57
+ @client.upload_asset(release_id, artifact.zip_path, content_type: 'application/zip')
58
+ end
59
+
60
+ def delete_existing_release(tag_name)
61
+ release = find_release(tag_name)
62
+ @client.delete_release(release['url']) if release
63
+ begin
64
+ @client.delete_ref(@repo, "tags/#{tag_name}")
65
+ rescue StandardError
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module Platform
6
+ module GitHub
7
+ GitHubRelease = Struct.new(:tag_name, :body, :prerelease, :draft,
8
+ :html_url, :published_at, :created_at,
9
+ :assets, keyword_init: true)
10
+ GitHubAsset = Struct.new(:name, :browser_download_url, :size, :data,
11
+ keyword_init: true)
12
+
13
+ class ReleaseFetcher
14
+ include Metanorma::Release::ReleaseFetcher
15
+
16
+ def initialize(client:)
17
+ @client = client
18
+ end
19
+
20
+ def fetch(repo, etag: nil)
21
+ releases = @client.releases(repo.to_s)
22
+ parsed = releases.map { |r| parse_release(r) }
23
+ FetchResult.new(releases: parsed, etag: "etag-#{repo}", unchanged?: false)
24
+ end
25
+
26
+ private
27
+
28
+ def parse_release(r)
29
+ assets = (r[:assets] || []).map do |a|
30
+ GitHubAsset.new(
31
+ name: a[:name],
32
+ browser_download_url: a[:browser_download_url],
33
+ size: a[:size],
34
+ data: nil
35
+ )
36
+ end
37
+ GitHubRelease.new(
38
+ tag_name: r[:tag_name],
39
+ body: r[:body],
40
+ prerelease: r[:prerelease],
41
+ draft: r[:draft],
42
+ html_url: r[:html_url],
43
+ published_at: r[:published_at],
44
+ created_at: r[:created_at],
45
+ assets: assets
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end