relaton-etsi 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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