metanorma-release 0.2.24 → 0.2.26

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +21 -319
  3. data/README.adoc +319 -92
  4. data/lib/metanorma/release/aggregation_pipeline.rb +128 -45
  5. data/lib/metanorma/release/asset_processor.rb +3 -3
  6. data/lib/metanorma/release/cache_store.rb +1 -0
  7. data/lib/metanorma/release/change_detector.rb +3 -2
  8. data/lib/metanorma/release/cli.rb +28 -4
  9. data/lib/metanorma/release/commands/aggregate.rb +1 -17
  10. data/lib/metanorma/release/commands/package.rb +9 -17
  11. data/lib/metanorma/release/commands/release_command.rb +19 -14
  12. data/lib/metanorma/release/config.rb +16 -7
  13. data/lib/metanorma/release/delta_state.rb +15 -1
  14. data/lib/metanorma/release/dependency_validation.rb +19 -0
  15. data/lib/metanorma/release/document_flattener.rb +173 -0
  16. data/lib/metanorma/release/index.rb +1 -1
  17. data/lib/metanorma/release/interfaces.rb +12 -0
  18. data/lib/metanorma/release/platform/github/manifest_reader.rb +3 -1
  19. data/lib/metanorma/release/platform/github/release_fetcher.rb +10 -14
  20. data/lib/metanorma/release/platform/github/topic_discoverer.rb +1 -1
  21. data/lib/metanorma/release/platform/github.rb +0 -4
  22. data/lib/metanorma/release/platform/local/fetcher.rb +5 -17
  23. data/lib/metanorma/release/platform/null/manifest_reader.rb +15 -0
  24. data/lib/metanorma/release/platform/null/publisher.rb +1 -1
  25. data/lib/metanorma/release/platform/null.rb +2 -0
  26. data/lib/metanorma/release/platform/static_discoverer.rb +19 -0
  27. data/lib/metanorma/release/platform.rb +1 -0
  28. data/lib/metanorma/release/platform_factory.rb +6 -21
  29. data/lib/metanorma/release/publication.rb +23 -161
  30. data/lib/metanorma/release/publication_serializer.rb +59 -0
  31. data/lib/metanorma/release/release_pipeline.rb +7 -15
  32. data/lib/metanorma/release/rxl_extractor.rb +106 -0
  33. data/lib/metanorma/release/site.rb +5 -165
  34. data/lib/metanorma/release/slug_strategy.rb +30 -15
  35. data/lib/metanorma/release/version.rb +1 -1
  36. data/lib/metanorma/release/zip_packager.rb +1 -1
  37. data/lib/metanorma/release.rb +36 -19
  38. metadata +11 -5
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ class DocumentFlattener
6
+ def initialize(display_categories: [])
7
+ @display_categories = display_categories || []
8
+ end
9
+
10
+ def flatten(doc)
11
+ bib = doc["bibliographic"] || {}
12
+ doctype = extract_doctype(bib) || doc.fetch("doctype", "")
13
+ formats = doc["formats"] || []
14
+ base = {
15
+ "slug" => doc["id"],
16
+ "id" => resolve_doc_id(bib, doc),
17
+ "title" => doc["title"].to_s,
18
+ "abstract" => extract_abstract(bib),
19
+ "stage" => (doc["stage"] || "published").to_s.downcase,
20
+ "doctype" => doctype,
21
+ "edition" => doc["edition"],
22
+ "date" => extract_date(doc),
23
+ "channels" => doc["channels"] || [],
24
+ "formats" => formats,
25
+ "files" => doc["files"] || [],
26
+ "bibliographic" => bib,
27
+ }
28
+ add_format_flags(base, formats)
29
+ add_display_category(base, doctype)
30
+ add_contributors(base, bib)
31
+ base
32
+ end
33
+
34
+ private
35
+
36
+ def add_format_flags(hash, formats)
37
+ hash["has_html"] = formats.include?("html")
38
+ hash["has_pdf"] = formats.include?("pdf")
39
+ hash["has_xml"] = formats.include?("xml")
40
+ hash["has_rxl"] = formats.include?("rxl")
41
+
42
+ files = hash["files"]
43
+ %w[html pdf xml rxl].each do |fmt|
44
+ file = files.find { |f| f["format"] == fmt }
45
+ hash["#{fmt}_path"] = file["path"] if file
46
+ end
47
+ end
48
+
49
+ def add_display_category(hash, doctype)
50
+ cat = resolve_display_category(doctype)
51
+ hash["stage_css"] = hash["stage"].gsub(/\s+/, "-")
52
+ hash["doctype_class"] = "type-#{doctype.downcase}"
53
+ hash["display_category"] = cat&.fetch("name", nil)
54
+ hash["display_category_slug"] = cat&.fetch("slug", nil)
55
+ end
56
+
57
+ def resolve_display_category(doctype)
58
+ return nil if doctype.nil? || doctype.empty?
59
+
60
+ @display_categories.each do |cat|
61
+ doctypes = cat["doctypes"] || []
62
+ if doctypes.include?(doctype)
63
+ return { "name" => cat["name"],
64
+ "slug" => cat["slug"] }
65
+ end
66
+ end
67
+ nil
68
+ end
69
+
70
+ def resolve_doc_id(bib, doc)
71
+ extract_primary_id(bib) || doc["identifier"] || doc["id"]
72
+ end
73
+
74
+ def extract_primary_id(bib)
75
+ ids = bib["docidentifier"]
76
+ return nil unless ids&.any?
77
+
78
+ primary = ids.find { |di| di["primary"] == true } || ids.first
79
+ primary["content"]
80
+ end
81
+
82
+ def extract_doctype(bib)
83
+ bib.dig("ext", "doctype", "content")
84
+ end
85
+
86
+ def extract_abstract(bib)
87
+ abstracts = bib["abstract"]
88
+ return nil unless abstracts&.any?
89
+
90
+ abstracts.first["content"]
91
+ end
92
+
93
+ def extract_date(doc)
94
+ bib_date = extract_bib_date(doc["bibliographic"])
95
+ return bib_date if bib_date
96
+
97
+ release_date = doc.dig("source", "releaseDate")
98
+ return nil unless release_date
99
+
100
+ release_date.to_s.split(/[T ]/).first
101
+ end
102
+
103
+ def extract_bib_date(bib)
104
+ return nil unless bib
105
+
106
+ dates = bib["date"]
107
+ return nil if dates.nil? || dates.empty?
108
+
109
+ published = dates.find { |d| d["type"] == "published" }
110
+ entry = published || dates.first
111
+ at = entry["at"]
112
+ at ? at.to_s.split(/[T ]/).first : nil
113
+ end
114
+
115
+ def add_contributors(hash, bib)
116
+ contribs = bib["contributor"] || []
117
+ persons, committees = partition_contributors(contribs)
118
+ hash["authors"] = persons
119
+ hash["committee"] = committees.first
120
+ end
121
+
122
+ def partition_contributors(contribs)
123
+ persons = contribs.filter_map { |c| parse_person(c) }
124
+ committees = contribs.filter_map { |c| parse_committee(c) }
125
+ [persons, committees]
126
+ end
127
+
128
+ def parse_person(contrib)
129
+ return nil unless contrib["person"]
130
+
131
+ name = extract_person_name(contrib["person"])
132
+ return nil unless name
133
+
134
+ role = (contrib["role"] || []).first&.fetch("type", nil)
135
+ { "name" => name, "role" => role }
136
+ end
137
+
138
+ def parse_committee(contrib)
139
+ return nil unless contrib["organization"]
140
+
141
+ extract_org_subdivision(contrib["organization"])
142
+ end
143
+
144
+ def extract_person_name(person)
145
+ n = person["name"] || {}
146
+ complete = n["completename"]
147
+ return complete["content"] if complete.is_a?(Hash) && complete["content"]
148
+ return complete if complete.is_a?(String)
149
+
150
+ surname = n["surname"]
151
+ given = n["given"]
152
+ given_str = given.is_a?(Hash) ? given["content"].to_s : given.to_s
153
+ parts = [given_str, surname].compact
154
+ parts.empty? ? nil : parts.join(" ")
155
+ end
156
+
157
+ def extract_org_subdivision(org)
158
+ subs = org["subdivision"]
159
+ return nil unless subs&.any?
160
+
161
+ sd = subs.first
162
+ sd_name = sd["name"]
163
+ if sd_name.is_a?(Array)
164
+ sd_name.first&.dig("content")
165
+ elsif sd_name.is_a?(Hash)
166
+ sd_name["content"]
167
+ else
168
+ sd_name.to_s
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -51,7 +51,7 @@ module Metanorma
51
51
  end
52
52
 
53
53
  def to_json(*_args)
54
- JSON.generate(to_h)
54
+ JSON.pretty_generate(to_h)
55
55
  end
56
56
 
57
57
  def write(path)
@@ -2,6 +2,18 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
+ module Extractor
6
+ def discover(output_dir)
7
+ raise NotImplementedError, "#{self} must implement .discover"
8
+ end
9
+ end
10
+
11
+ Release = Struct.new(:tag_name, :body, :prerelease, :draft,
12
+ :html_url, :published_at, :created_at,
13
+ :assets, keyword_init: true)
14
+ Asset = Struct.new(:name, :browser_download_url, :size, :data,
15
+ keyword_init: true)
16
+
5
17
  module Filter
6
18
  def apply(documents)
7
19
  raise NotImplementedError, "#{self.class} must implement #apply"
@@ -22,7 +22,9 @@ module Metanorma
22
22
  return nil unless parsed.is_a?(Hash)
23
23
 
24
24
  channels = Array(parsed["channels"])
25
- Array(parsed["documents"]).each { |doc| channels.concat(Array(doc["channels"])) }
25
+ Array(parsed["documents"]).each do |doc|
26
+ channels.concat(Array(doc["channels"]))
27
+ end
26
28
  channels.map(&:to_s).uniq
27
29
  rescue StandardError
28
30
  nil
@@ -6,12 +6,6 @@ module Metanorma
6
6
  module Release
7
7
  module Platform
8
8
  module GitHub
9
- GitHubRelease = Struct.new(:tag_name, :body, :prerelease, :draft,
10
- :html_url, :published_at, :created_at,
11
- :assets, keyword_init: true)
12
- GitHubAsset = Struct.new(:name, :browser_download_url, :size, :data,
13
- keyword_init: true)
14
-
15
9
  class ReleaseFetcher
16
10
  include Metanorma::Release::ReleaseFetcher
17
11
 
@@ -21,14 +15,16 @@ module Metanorma
21
15
  end
22
16
 
23
17
  def fetch(repo, etag: nil)
24
- releases = paginate_releases(repo.to_s)
18
+ releases, failed = paginate_releases(repo.to_s)
25
19
  parsed = releases.map { |r| parse_release(r) }
26
20
  FetchResult.new(releases: parsed, etag: "etag-#{repo}",
27
- unchanged?: false)
21
+ unchanged?: false, failed?: failed)
28
22
  end
29
23
 
30
24
  private
31
25
 
26
+ # Returns [releases, failed] — failed is true when the listing
27
+ # errored, which callers must not treat as an empty release set.
32
28
  def paginate_releases(repo_slug)
33
29
  all = []
34
30
  page = 1
@@ -41,23 +37,23 @@ module Metanorma
41
37
 
42
38
  page += 1
43
39
  end
44
- all
40
+ [all, false]
45
41
  rescue StandardError => e
46
- warn "Warning: Failed to fetch releases for #{repo_slug}: #{e.message}"
47
- []
42
+ Metanorma::Release.logger.warn "Failed to fetch releases for #{repo_slug}: #{e.message}"
43
+ [[], true]
48
44
  end
49
45
 
50
46
  def parse_release(r)
51
47
  assets = (r[:assets] || []).map do |a|
52
48
  data = download_asset(a[:url]) if a[:name].end_with?(".zip")
53
- GitHubAsset.new(
49
+ Asset.new(
54
50
  name: a[:name],
55
51
  browser_download_url: a[:browser_download_url],
56
52
  size: a[:size],
57
53
  data: data,
58
54
  )
59
55
  end
60
- GitHubRelease.new(
56
+ Release.new(
61
57
  tag_name: r[:tag_name],
62
58
  body: r[:body],
63
59
  prerelease: r[:prerelease],
@@ -82,7 +78,7 @@ module Metanorma
82
78
  end
83
79
  data
84
80
  rescue StandardError => e
85
- warn "Warning: Failed to download asset #{url}: #{e.message}"
81
+ Metanorma::Release.logger.warn "Failed to download asset #{url}: #{e.message}"
86
82
  nil
87
83
  end
88
84
 
@@ -20,7 +20,7 @@ module Metanorma
20
20
  page = 1
21
21
  loop do
22
22
  results = @client.search_repositories(query, per_page: 100,
23
- page: page)
23
+ page: page)
24
24
  items = results[:items]
25
25
  break if items.nil? || items.empty?
26
26
 
@@ -18,10 +18,6 @@ module Metanorma
18
18
  "metanorma/release/platform/github/release_fetcher"
19
19
  autoload :ManifestReader,
20
20
  "metanorma/release/platform/github/manifest_reader"
21
-
22
- def self.cache_store(cache_dir:)
23
- FileCacheStore.new(cache_dir)
24
- end
25
21
  end
26
22
  end
27
23
  end
@@ -6,12 +6,6 @@ module Metanorma
6
6
  module Release
7
7
  module Platform
8
8
  module Local
9
- LocalRelease = Struct.new(:tag_name, :body, :prerelease, :draft,
10
- :html_url, :published_at, :created_at,
11
- :assets, keyword_init: true)
12
- LocalAsset = Struct.new(:name, :browser_download_url, :size, :data,
13
- keyword_init: true)
14
-
15
9
  class Fetcher
16
10
  include Metanorma::Release::ReleaseFetcher
17
11
 
@@ -42,22 +36,22 @@ module Metanorma
42
36
  zip_path = File.join(dir, "#{base}.zip")
43
37
 
44
38
  unless File.exist?(zip_path)
45
- warn "Warning: Missing zip for #{meta_path}, skipping"
39
+ Metanorma::Release.logger.warn "Missing zip for #{meta_path}, skipping"
46
40
  return nil
47
41
  end
48
42
 
49
43
  metadata = Publication.from_metadata_hash(data)
50
- asset = LocalAsset.new(
44
+ asset = Asset.new(
51
45
  name: "#{base}.zip",
52
46
  browser_download_url: "file://#{File.expand_path(zip_path)}",
53
47
  size: File.size(zip_path),
54
48
  data: File.binread(zip_path),
55
49
  )
56
50
 
57
- LocalRelease.new(
51
+ Release.new(
58
52
  tag_name: "#{metadata.slug}/#{metadata.edition || '1'}",
59
53
  body: metadata.to_release_body,
60
- prerelease: prerelease?(metadata),
54
+ prerelease: metadata.draft?,
61
55
  draft: false,
62
56
  html_url: "file://#{File.expand_path(dir)}",
63
57
  published_at: File.mtime(zip_path).iso8601,
@@ -65,15 +59,9 @@ module Metanorma
65
59
  assets: [asset],
66
60
  )
67
61
  rescue JSON::ParserError
68
- warn "Warning: Invalid metadata JSON in #{meta_path}, skipping"
62
+ Metanorma::Release.logger.warn "Invalid metadata JSON in #{meta_path}, skipping"
69
63
  nil
70
64
  end
71
-
72
- def prerelease?(metadata)
73
- stage = metadata.stage.to_s
74
- %w[working-draft committee-draft draft-standard
75
- final-draft].include?(stage)
76
- end
77
65
  end
78
66
  end
79
67
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module Platform
6
+ module Null
7
+ class ManifestReader
8
+ include Metanorma::Release::ManifestReader
9
+
10
+ def read(_repo) = nil
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -8,7 +8,7 @@ module Metanorma
8
8
  include Metanorma::Release::Publisher
9
9
 
10
10
  def publish(tag, _artifact, _metadata, channels:,
11
- force_replace: false)
11
+ force_replace: false)
12
12
  PublishResult.new(tag: tag.to_s, url: "null://", created?: true)
13
13
  end
14
14
  end
@@ -5,6 +5,8 @@ module Metanorma
5
5
  module Platform
6
6
  module Null
7
7
  autoload :Publisher, "metanorma/release/platform/null/publisher"
8
+ autoload :ManifestReader,
9
+ "metanorma/release/platform/null/manifest_reader"
8
10
  end
9
11
  end
10
12
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Release
5
+ module Platform
6
+ class StaticDiscoverer
7
+ include RepoDiscoverer
8
+
9
+ def initialize(repos:)
10
+ @repos = repos
11
+ end
12
+
13
+ def discover
14
+ @repos
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -6,6 +6,7 @@ module Metanorma
6
6
  autoload :GitHub, "metanorma/release/platform/github"
7
7
  autoload :Local, "metanorma/release/platform/local"
8
8
  autoload :Null, "metanorma/release/platform/null"
9
+ autoload :StaticDiscoverer, "metanorma/release/platform/static_discoverer"
9
10
  end
10
11
  end
11
12
  end
@@ -24,14 +24,17 @@ module Metanorma
24
24
 
25
25
  discoverer = if opts[:repos]
26
26
  repos = opts[:repos].map { |r| RepoRef.from_string(r) }
27
- StaticDiscoverer.new(repos: repos)
27
+ Platform::StaticDiscoverer.new(repos: repos)
28
28
  else
29
29
  Platform::GitHub::TopicDiscoverer.new(
30
30
  client: client, organizations: opts[:organizations], topic: opts[:topic],
31
31
  )
32
32
  end
33
33
 
34
- download_cache = opts[:cache_dir] ? File.join(opts[:cache_dir], "downloads") : nil
34
+ download_cache = if opts[:cache_dir]
35
+ File.join(opts[:cache_dir],
36
+ "downloads")
37
+ end
35
38
 
36
39
  {
37
40
  discoverer: discoverer,
@@ -58,7 +61,7 @@ module Metanorma
58
61
  if source.start_with?("local:")
59
62
  adapters = AGGREGATION_REGISTRY["local"].call(options,
60
63
  options[:token])
61
- adapters[:manifest_reader] = NullManifestReader.new
64
+ adapters[:manifest_reader] = Platform::Null::ManifestReader.new
62
65
  return adapters
63
66
  end
64
67
 
@@ -78,24 +81,6 @@ module Metanorma
78
81
  def self.register_aggregation(name, factory)
79
82
  AGGREGATION_REGISTRY[name] = factory
80
83
  end
81
-
82
- class StaticDiscoverer
83
- include RepoDiscoverer
84
-
85
- def initialize(repos:)
86
- @repos = repos
87
- end
88
-
89
- def discover
90
- @repos
91
- end
92
- end
93
-
94
- class NullManifestReader
95
- include Metanorma::Release::ManifestReader
96
-
97
- def read(_repo) = nil
98
- end
99
84
  end
100
85
  end
101
86
  end