relaton-etsi 1.17.0

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.
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
3
+ <include href="basicdoc.rng"/>
4
+ <include href="relaton-etsi.rng"/>
5
+ <start>
6
+ <choice>
7
+ <ref name="bibitem"/>
8
+ <ref name="bibdata"/>
9
+ </choice>
10
+ </start>
11
+ </grammar>
@@ -0,0 +1,121 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
3
+ <include href="biblio-standoc.rng">
4
+ <define name="BibDataExtensionType">
5
+ <optional>
6
+ <attribute name="schema-version"/>
7
+ </optional>
8
+ <ref name="doctype"/>
9
+ <optional>
10
+ <ref name="docsubtype"/>
11
+ </optional>
12
+ <ref name="editorialgroup"/>
13
+ <ref name="marker"/>
14
+ <zeroOrMore>
15
+ <ref name="frequency"/>
16
+ </zeroOrMore>
17
+ <zeroOrMore>
18
+ <ref name="mandate"/>
19
+ </zeroOrMore>
20
+ <optional>
21
+ <ref name="custom-collection"/>
22
+ </optional>
23
+ </define>
24
+ <define name="status">
25
+ <element name="status">
26
+ <ref name="stage"/>
27
+ </element>
28
+ </define>
29
+ <define name="stage">
30
+ <element name="stage">
31
+ <ref name="StageType"/>
32
+ </element>
33
+ </define>
34
+ <define name="doctype">
35
+ <element name="doctype">
36
+ <optional>
37
+ <attribute name="abbreviation">
38
+ <ref name="DocumentTypeAbbreviation"/>
39
+ </attribute>
40
+ </optional>
41
+ <ref name="DocumentType"/>
42
+ </element>
43
+ </define>
44
+ <define name="DocumentType">
45
+ <choice>
46
+ <value>European Standard</value>
47
+ <value>ETSI Standard</value>
48
+ <value>ETSI Guide</value>
49
+ <value>Technical Specification</value>
50
+ <value>Group Specification</value>
51
+ <value>Group Report</value>
52
+ <value>Technical Report</value>
53
+ <value>ETSI Technical Report</value>
54
+ <value>GSM Technical Specification</value>
55
+ <value>Special Report</value>
56
+ <value>Technical Committee Reference Technical Report</value>
57
+ <value>Technical Basis for Regulation</value>
58
+ <value>European Telecommunication Standard</value>
59
+ <value>Interim European Telecommunication Standard</value>
60
+ <value>Norme Européenne de Télécommunication</value>
61
+ </choice>
62
+ </define>
63
+ </include>
64
+ <define name="StageType">
65
+ <choice>
66
+ <value>EN approval</value>
67
+ <value>SG approval</value>
68
+ <value>ES approval</value>
69
+ <value>Published</value>
70
+ <value>Withdrawn</value>
71
+ <value>Historical</value>
72
+ </choice>
73
+ </define>
74
+ <define name="DocumentTypeAbbreviation">
75
+ <choice>
76
+ <value>EN</value>
77
+ <value>ES</value>
78
+ <value>EG</value>
79
+ <value>TS</value>
80
+ <value>GS</value>
81
+ <value>GR</value>
82
+ <value>TR</value>
83
+ <value>ETR</value>
84
+ <value>GTS</value>
85
+ <value>SR</value>
86
+ <value>TCRTR</value>
87
+ <value>TBR</value>
88
+ <value>ETS</value>
89
+ <value>I-ETS</value>
90
+ <value>NET</value>
91
+ </choice>
92
+ </define>
93
+ <define name="marker">
94
+ <element name="marker">
95
+ <choice>
96
+ <value>Current</value>
97
+ <value>Superseded</value>
98
+ </choice>
99
+ </element>
100
+ </define>
101
+ <define name="frequency">
102
+ <element name="frequency">
103
+ <text/>
104
+ </element>
105
+ </define>
106
+ <define name="mandate">
107
+ <element name="mandate">
108
+ <text/>
109
+ </element>
110
+ </define>
111
+ <define name="custom-collection">
112
+ <element name="custom-collection">
113
+ <choice>
114
+ <value>HSs cited in OJ</value>
115
+ <value>HSs not yet cited in OJ</value>
116
+ <value>HSs RED cited in OJ</value>
117
+ <value>HSs EMC cited in OJ</value>
118
+ </choice>
119
+ </element>
120
+ </define>
121
+ </grammar>
@@ -0,0 +1,75 @@
1
+ module RelatonEtsi
2
+ class BibliographicItem < RelatonBib::BibliographicItem
3
+ MAKER = %w[Current Superseded].freeze
4
+ CUSTOM_COLLECTION = ["HSs cited in OJ", "HSs not yet cited in OJ", "HSs RED cited in OJ", "HSs EMC cited in OJ"].freeze
5
+
6
+ attr_reader :marker, :frequency, :mandate, :custom_collection
7
+
8
+ #
9
+ # Constructor.
10
+ #
11
+ # @param [RelatonEtsi::DocumentType] doctype document type
12
+ # @param [String] marker current or superseded
13
+ # @param [Array<String>] frequency list of frequencies
14
+ # @param [Array<String>] mandate list of mandates
15
+ # @param [String, nil] custom_collection custom collection
16
+ #
17
+ def initialize(**args)
18
+ @marker = args.delete(:marker)
19
+ @frequency = args.delete(:frequency) || []
20
+ @mandate = args.delete(:mandate) || []
21
+ @custom_collection = args.delete(:custom_collection)
22
+ super(**args)
23
+ end
24
+
25
+ def self.from_hash(hash)
26
+ item_hash = HashConverter.hash_to_bib(hash)
27
+ new(**item_hash)
28
+ end
29
+
30
+ #
31
+ # Fetch flavour schema version
32
+ #
33
+ # @return [String] flavour schema versio
34
+ #
35
+ def ext_schema
36
+ @ext_schema ||= schema_versions["relaton-model-etsi"]
37
+ end
38
+
39
+ def to_xml(**opts) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
40
+ super(**opts) do |b|
41
+ if opts[:bibdata] && (doctype || subdoctype || editorialgroup || marker || frequency.any? || mandate.any? || custom_collection)
42
+ ext = b.ext do
43
+ doctype&.to_xml b
44
+ b.subdoctype subdoctype if subdoctype
45
+ editorialgroup&.to_xml b
46
+ b.marker marker if marker
47
+ frequency.each { |f| b.frequency f }
48
+ mandate.each { |m| b.mandate m }
49
+ b.send("custom-collection", custom_collection) if custom_collection
50
+ end
51
+ ext["schema-version"] = ext_schema unless opts[:embedded]
52
+ end
53
+ end
54
+ end
55
+
56
+ def to_hash # rubocop:disable Metrics/AbcSize
57
+ hash = super
58
+ hash["marker"] = marker if marker
59
+ hash["frequency"] = frequency if frequency.any?
60
+ hash["mandate"] = mandate if mandate.any?
61
+ hash["custom_collection"] = custom_collection if custom_collection
62
+ hash
63
+ end
64
+
65
+ def to_asciibib(prefix = "") # rubocop:disable Metrics/AbcSize
66
+ out = super
67
+ pref = prefix.empty? ? prefix : "#{prefix}."
68
+ out += "#{pref}marker:: #{marker}\n" if marker
69
+ out += frequency.map { |f| "#{pref}frequency:: #{f}\n" }.join
70
+ out += mandate.map { |m| "#{pref}mandate:: #{m}\n" }.join
71
+ out += "#{pref}custom_collection:: #{custom_collection}" if custom_collection
72
+ out
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RelatonEtsi
4
+ # Methods for search IANA standards.
5
+ module Bibliography
6
+ SOURCE = "https://raw.githubusercontent.com/relaton/relaton-data-etsi/main/"
7
+ INDEX_FILE = "index-v1.yaml"
8
+
9
+ # @param text [String]
10
+ # @return [RelatonBib::BibliographicItem]
11
+ def search(text) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
12
+ index = Relaton::Index.find_or_create :etsi, url: "#{SOURCE}index-v1.zip", file: INDEX_FILE
13
+ row = index.search(text).min_by { |r| r[:id] }
14
+ return unless row
15
+
16
+ url = "#{SOURCE}#{row[:file]}"
17
+ resp = Net::HTTP.get_response URI(url)
18
+ return unless resp.code == "200"
19
+
20
+ hash = YAML.safe_load resp.body
21
+ bib_hash = HashConverter.hash_to_bib(hash)
22
+ bib_hash[:fetched] = Date.today.to_s
23
+ BibliographicItem.new(**bib_hash)
24
+ rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
25
+ EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
26
+ Net::ProtocolError, Errno::ETIMEDOUT => e
27
+ raise RelatonBib::RequestError, e.message
28
+ end
29
+
30
+ # @param ref [String] the ETSI standard Code to look up
31
+ # @param year [String, nil] year
32
+ # @param opts [Hash] options
33
+ # @return [RelatonEtsi::BibliographicItem]
34
+ def get(ref, _year = nil, _opts = {})
35
+ Util.warn "(#{ref}) Fetching from Relaton repository ..."
36
+ result = search(ref)
37
+ unless result
38
+ Util.warn "(#{ref}) Not found."
39
+ return
40
+ end
41
+
42
+ Util.warn "(#{ref}) Found: `#{result.docidentifier[0].id}`"
43
+ result
44
+ end
45
+
46
+ extend Bibliography
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ module RelatonEtsi
2
+ module Config
3
+ include RelatonBib::Config
4
+ end
5
+ extend Config
6
+
7
+ class Configuration < RelatonBib::Configuration
8
+ PROGNAME = "relaton-etsi".freeze
9
+ end
10
+ end
@@ -0,0 +1,56 @@
1
+ module RelatonEtsi
2
+ class DataFetcher
3
+ #
4
+ # Initialize data fetcher.
5
+ #
6
+ # @param [String] output output directory
7
+ # @param [String] format output format (xml, bibxml, yaml)
8
+ #
9
+ def initialize(output, format)
10
+ @output = output
11
+ @format = format
12
+ @ext = format.sub(/^bib/, "")
13
+ end
14
+
15
+ def self.fetch(output: "data", format: "yaml")
16
+ t1 = Time.now
17
+ puts "Started at: #{t1}"
18
+ FileUtils.mkdir_p output
19
+ new(output, format).fetch
20
+ t2 = Time.now
21
+ puts "Stopped at: #{t2}"
22
+ puts "Done in: #{(t2 - t1).round} sec."
23
+ end
24
+
25
+ def index1
26
+ @index1 ||= Relaton::Index.find_or_create :etsi, file: Bibliography::INDEX_FILE
27
+ end
28
+
29
+ def fetch
30
+ url = "https://www.etsi.org/?option=com_standardssearch&view=data&format=csv&includeScope=1&page=1&search=&" \
31
+ "title=1&etsiNumber=1&content=1&version=0&onApproval=1&published=1&withdrawn=1&historical=1&isCurrent=1&" \
32
+ "superseded=1&startDate=1988-01-15&endDate=2023-10-31&harmonized=0&keyword=&TB=&stdType=&frequency=&" \
33
+ "mandate=&collection=&sort=1&x=1698720135146"
34
+ csv = OpenURI.open_uri(url) { |f| f.readlines.join }
35
+ CSV.parse(csv, headers: true, col_sep: ';', skip_lines: /sep=;/).each do |row|
36
+ save DataParser.new(row).parse
37
+ end
38
+ index1.save
39
+ end
40
+
41
+ def save(bib)
42
+ filename = bib.docidentifier.first.id.gsub(/\//, "-").gsub(/\s|\./, "_").gsub(/\(|\)/, "")
43
+ file = File.join @output, "#{filename}.#{@ext}"
44
+ File.write file, content(bib), encoding: "UTF-8"
45
+ index1.add_or_update bib.docidentifier.first.id, file
46
+ end
47
+
48
+ def content(bib)
49
+ case @format
50
+ when "xml" then bib.to_xml bibdata: true
51
+ when "yaml" then bib.to_hash.to_yaml
52
+ else bib.send "to_#{@format}"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,88 @@
1
+ module RelatonEtsi
2
+ class DataParser
3
+ ATTRS = %i[id title docnumber link date docid version status keyword editorialgroup
4
+ doctype abstract language script].freeze
5
+
6
+ def initialize(row)
7
+ @row = row
8
+ end
9
+
10
+ def parse
11
+ args = ATTRS.each_with_object({}) do |attr, hash|
12
+ hash[attr] = send(attr)
13
+ end
14
+ BibliographicItem.new(**args)
15
+ end
16
+
17
+ def pubid
18
+ @pubid ||= PubId.parse(@row["ETSI deliverable"])
19
+ end
20
+
21
+ def id
22
+ @row["ETSI deliverable"].gsub(/[\s\(\)]/, "")
23
+ end
24
+
25
+ def title
26
+ [RelatonBib::TypedTitleString.new(content: @row["title"], language: "en", script: "Latn")]
27
+ end
28
+
29
+ def docnumber
30
+ @row["ETSI deliverable"]
31
+ end
32
+
33
+ def link
34
+ urls = []
35
+ urls << RelatonBib::TypedUri.new(content: @row["Details link"], type: "src")
36
+ urls << RelatonBib::TypedUri.new(content: @row["PDF link"], type: "pdf")
37
+ end
38
+
39
+ def date
40
+ return [] unless pubid.date
41
+
42
+ [RelatonBib::BibliographicDate.new(type: "published", on: pubid.date)]
43
+ end
44
+
45
+ def docid
46
+ [RelatonBib::DocumentIdentifier.new(id: @row["ETSI deliverable"], type: "ETSI", primary: true)]
47
+ end
48
+
49
+ def version
50
+ return [] unless pubid.version
51
+
52
+ [RelatonBib::BibliographicItem::Version.new(nil, pubid.version)]
53
+ end
54
+
55
+ def status
56
+ status = @row["Status"] == "On Approval" ? "#{pubid.type} approval" : @row["Status"]
57
+ RelatonBib::DocumentStatus.new(stage: status)
58
+ end
59
+
60
+ def keyword
61
+ @row["Keywords"].split(",")
62
+ end
63
+
64
+ def editorialgroup
65
+ wg = RelatonBib::WorkGroup.new name: @row["Technical body"]
66
+ tc = RelatonBib::TechnicalCommittee.new wg
67
+ RelatonBib::EditorialGroup.new [tc]
68
+ end
69
+
70
+ def doctype
71
+ DocumentType.create_from_abbreviation pubid.type
72
+ end
73
+
74
+ def abstract
75
+ return [] unless @row["Scope"]
76
+
77
+ [RelatonBib::FormattedString.new(content: @row["Scope"], language: "en", script: "Latn")]
78
+ end
79
+
80
+ def language
81
+ ["en"]
82
+ end
83
+
84
+ def script
85
+ ["Latn"]
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,46 @@
1
+ module RelatonEtsi
2
+ class DocumentType < RelatonBib::DocumentType
3
+ DOCTYPES = {
4
+ "EN" => "European Standard",
5
+ "ES" => "ETSI Standard",
6
+ "EG" => "ETSI Guide",
7
+ "TS" => "Technical Specification",
8
+ "GS" => "Group Specification",
9
+ "GR" => "Group Report",
10
+ "TR" => "Technical Report",
11
+ "ETR" => "ETSI Technical Report",
12
+ "GTS" => "GSM Technical Specification",
13
+ "SR" => "Special Report",
14
+ "TCRTR" => "Technical Committee Reference Technical Report",
15
+ "TBR" => "Technical Basis for Regulation",
16
+ "ETS" => "European Telecommunication Standard",
17
+ "I-ETS" => "Interim European Telecommunication Standard",
18
+ "NET" => "Norme Européenne de Télécommunication",
19
+ }.freeze
20
+
21
+ def initialize(type:, abbreviation: nil)
22
+ check_type type
23
+ check_abbreviation abbreviation
24
+ abbreviation ||= DOCTYPES.key(type)
25
+ super
26
+ end
27
+
28
+ def self.create_from_abbreviation(abbreviation)
29
+ new type: DOCTYPES[abbreviation], abbreviation: abbreviation
30
+ end
31
+
32
+ private
33
+
34
+ def check_type(type)
35
+ return if DOCTYPES.value? type
36
+
37
+ Util.warn "WARNING: invalid doctype: `#{type}`"
38
+ end
39
+
40
+ def check_abbreviation(abbreviation)
41
+ return if abbreviation.nil? || DOCTYPES.key?(abbreviation)
42
+
43
+ Util.warn "WARNING: invalid doctype abbreviation: `#{abbreviation}`"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ module RelatonEtsi
2
+ module HashConverter
3
+ include RelatonBib::HashConverter
4
+ extend self
5
+
6
+ private
7
+
8
+ # @param item_hash [Hash]
9
+ # @return [RelatonEtsi::BibliographicItem]
10
+ def bib_item(item_hash)
11
+ BibliographicItem.new(**item_hash)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ require "relaton/processor"
2
+
3
+ module RelatonEtsi
4
+ class Processor < Relaton::Processor
5
+ attr_reader :idtype
6
+
7
+ def initialize # rubocop:disable Lint/MissingSuper
8
+ @short = :relaton_etsi
9
+ @prefix = "ETSI"
10
+ @defaultprefix = %r{^ETSI\s}
11
+ @idtype = "ETSI"
12
+ @datasets = %w[etsi-csv]
13
+ end
14
+
15
+ # @param code [String]
16
+ # @param date [String, nil] year
17
+ # @param opts [Hash]
18
+ # @return [RelatonEtsi::BibliographicItem]
19
+ def get(code, date, opts)
20
+ ::RelatonEtsi::Bibliography.get(code, date, opts)
21
+ end
22
+
23
+ #
24
+ # Fetch all the documents from http://xml2rfc.tools.ietf.org/public/rfc/bibxml-3gpp-new/
25
+ #
26
+ # @param [String] _source source name
27
+ # @param [Hash] opts
28
+ # @option opts [String] :output directory to output documents
29
+ # @option opts [String] :format
30
+ #
31
+ def fetch_data(_source, opts)
32
+ DataFetcher.fetch(**opts)
33
+ end
34
+
35
+ # @param xml [String]
36
+ # @return [RelatonEtsi::BibliographicItem]
37
+ def from_xml(xml)
38
+ ::RelatonEtsi::XMLParser.from_xml xml
39
+ end
40
+
41
+ # @param hash [Hash]
42
+ # @return [RelatonEtsi::BibliographicItem]
43
+ def hash_to_bib(hash)
44
+ item_hash = ::RelatonEtsi::HashConverter.hash_to_bib(hash)
45
+ ::RelatonEtsi::BibliographicItem.new(**item_hash)
46
+ end
47
+
48
+ # Returns hash of XML grammar
49
+ # @return [String]
50
+ def grammar_hash
51
+ @grammar_hash ||= ::RelatonEtsi.grammar_hash
52
+ end
53
+
54
+ #
55
+ # Remove index file
56
+ #
57
+ def remove_index_file
58
+ Relaton::Index.find_or_create(:etsi, url: true, file: Bibliography::INDEX_FILE).remove_file
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,35 @@
1
+ module RelatonEtsi
2
+ class PubId
3
+ class Parser
4
+ def initialize(id)
5
+ @strscan = StringScanner.new id
6
+ end
7
+
8
+ def parse
9
+ @strscan.scan(/^ETSI\s+/)
10
+ type = @strscan.scan(/\S+/)
11
+ @strscan.scan(/\s+/)
12
+ docnumber = @strscan.scan_until(/(?=\s(V\d+\.\d+\.\d+)|ed\.\d+)/)
13
+ version = @strscan.scan(/\d+\.\d+\.\d+/) if @strscan.scan(/\sV(?=\d+\.\d+\.\d+)/)
14
+ edition = @strscan.scan(/\d+/) if @strscan.scan(/\sed\.(?=\d+)/)
15
+ date = @strscan.scan(/\d{4}-\d{2}/) if @strscan.scan(/\s\(/)
16
+
17
+ { type: type, docnumber: docnumber, version: version, edition: edition, date: date }
18
+ end
19
+ end
20
+
21
+ attr_accessor :type, :docnumber, :version, :edition, :date
22
+
23
+ def initialize(type:, docnumber:, version:, edition:, date:)
24
+ @type = type
25
+ @docnumber = docnumber
26
+ @version = version
27
+ @edition = edition
28
+ @date = date
29
+ end
30
+
31
+ def self.parse(id)
32
+ new(**Parser.new(id).parse)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,9 @@
1
+ module RelatonEtsi
2
+ module Util
3
+ extend RelatonBib::Util
4
+
5
+ def self.logger
6
+ RelatonEtsi.configuration.logger
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RelatonEtsi
4
+ VERSION = "1.17.0"
5
+ end
@@ -0,0 +1,44 @@
1
+ module RelatonEtsi
2
+ class XMLParser < RelatonBib::XMLParser
3
+ class << self
4
+ private
5
+
6
+ # @param intem [Nokogiri::XML::Document]
7
+ # @return [Hash]
8
+ def item_data(item) # rubocop:disable Metrics/AbcSize
9
+ data = super
10
+ ext = item.at "./ext"
11
+ return data unless ext
12
+
13
+ data[:marker] = ext.at("./marker")&.text
14
+ data[:frequency] = ext.xpath("./frequency").map(&:text)
15
+ data[:mandate] = ext.xpath("./mandate").map(&:text)
16
+ data[:custom_collection] = ext.at("./custom-collection")&.text
17
+ data
18
+ end
19
+
20
+ # @param item_hash [Hash]
21
+ # @return [RelatonEtsi::BibliographicItem]
22
+ def bib_item(item_hash)
23
+ BibliographicItem.new(**item_hash)
24
+ end
25
+
26
+ # def fetch_status(item)
27
+ # status = item.at "./status"
28
+ # return unless status
29
+
30
+ # DocumentStatus.new(
31
+ # stage: status.at("stage")&.text,
32
+ # substage: status.at("substage")&.text,
33
+ # iteration: status.at("iteration")&.text,
34
+ # )
35
+ # end
36
+
37
+ # # @param item [Nokogiri::XML::Element]
38
+ # # @return [Array<RelatonBib::DocumentRelation>]
39
+ # def fetch_relations(item)
40
+ # super item, DocumentRelation
41
+ # end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "open-uri"
5
+ require "csv"
6
+ require "relaton/index"
7
+ require "relaton_bib"
8
+ require_relative "relaton_etsi/version"
9
+ require_relative "relaton_etsi/config"
10
+ require_relative "relaton_etsi/util"
11
+ require_relative "relaton_etsi/pubid"
12
+ require_relative "relaton_etsi/document_type"
13
+ require_relative "relaton_etsi/bibliographic_item"
14
+ require_relative "relaton_etsi/xml_parser"
15
+ require_relative "relaton_etsi/hash_converter"
16
+ require_relative "relaton_etsi/bibliography"
17
+ require_relative "relaton_etsi/data_fetcher"
18
+ require_relative "relaton_etsi/data_parser"
19
+
20
+ module RelatonEtsi
21
+ class Error < StandardError; end
22
+
23
+ # Returns hash of gem versions used to generate data model.
24
+ # @return [String]
25
+ def grammar_hash
26
+ Digest::MD5.hexdigest RelatonEtsi::VERSION + RelatonBib::VERSION
27
+ end
28
+
29
+ extend self
30
+ end
@@ -0,0 +1,4 @@
1
+ module RelatonEtsi
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end