lutaml-vformat 0.1.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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ module Value
6
+ class Boolean < Base
7
+ def self.native?(value)
8
+ [true, false].include?(value)
9
+ end
10
+
11
+ def self.parse(wire)
12
+ case wire.upcase
13
+ when "TRUE" then true
14
+ when "FALSE" then false
15
+ else invalid!("boolean", wire)
16
+ end
17
+ end
18
+
19
+ def self.format(value)
20
+ value ? "TRUE" : "FALSE"
21
+ end
22
+ end
23
+
24
+ class Integer < Base
25
+ def self.native?(value)
26
+ value.is_a?(::Integer)
27
+ end
28
+
29
+ def self.parse(wire)
30
+ invalid!("integer", wire) unless wire.match?(/\A[+-]?\d+\z/)
31
+
32
+ wire.to_i
33
+ end
34
+
35
+ def self.format(value)
36
+ Integer(value).to_s
37
+ end
38
+ end
39
+
40
+ class Float < Base
41
+ def self.native?(value)
42
+ value.is_a?(::Float)
43
+ end
44
+
45
+ def self.parse(wire)
46
+ invalid!("float", wire) unless wire.match?(/\A[+-]?\d+(\.\d+)?\z/)
47
+
48
+ wire.to_f
49
+ end
50
+
51
+ # Plain decimal notation, never exponent form.
52
+ def self.format(value)
53
+ number = Kernel.Float(value)
54
+ unless number.finite?
55
+ raise SerializeError, "#{value} has no vObject float form"
56
+ end
57
+
58
+ text = number.to_s
59
+ text.include?("e") ? expand(number) : text
60
+ end
61
+
62
+ def self.expand(number)
63
+ ::Kernel.format("%.17f", number).sub(/0+\z/, "").sub(/\.\z/, ".0")
64
+ end
65
+ private_class_method :expand
66
+ end
67
+
68
+ # +hhmm or -hhmm, optionally with seconds. Native value: signed seconds
69
+ # east of UTC.
70
+ class UtcOffset < Base
71
+ PATTERN = /\A([+-])(\d{2})(\d{2})(\d{2})?\z/
72
+
73
+ def self.native?(value)
74
+ value.is_a?(::Integer)
75
+ end
76
+
77
+ def self.parse(wire)
78
+ match = PATTERN.match(wire) or invalid!("utc-offset", wire)
79
+ hours, minutes, seconds = match.values_at(2, 3, 4).map(&:to_i)
80
+ invalid!("utc-offset", wire) if out_of_range?(hours, minutes, seconds)
81
+
82
+ total = (hours * 3600) + (minutes * 60) + seconds
83
+ match[1] == "-" ? -total : total
84
+ end
85
+
86
+ def self.out_of_range?(hours, minutes, seconds)
87
+ hours > 23 || minutes > 59 || seconds > 59
88
+ end
89
+ private_class_method :out_of_range?
90
+
91
+ def self.format(value)
92
+ seconds = Kernel.Integer(value)
93
+ hours, rest = seconds.abs.divmod(3600)
94
+ minutes, secs = rest.divmod(60)
95
+ sign = seconds.negative? ? "-" : "+"
96
+ text = ::Kernel.format("%<s>s%<h>02d%<m>02d", s: sign, h: hours,
97
+ m: minutes)
98
+ secs.zero? ? text : text + ::Kernel.format("%02d", secs)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ module Value
6
+ class Text < Base
7
+ def self.escaped?
8
+ true
9
+ end
10
+
11
+ def self.native?(value)
12
+ value.is_a?(::String)
13
+ end
14
+
15
+ def self.parse(wire)
16
+ Escaping.unescape(wire)
17
+ end
18
+
19
+ def self.format(value)
20
+ Escaping.escape(value)
21
+ end
22
+ end
23
+
24
+ # RFC 3986 URI. Not text-escaped.
25
+ class Uri < Base
26
+ def self.native?(value)
27
+ value.is_a?(::String)
28
+ end
29
+
30
+ def self.parse(wire)
31
+ URI.parse(wire)
32
+ raise URI::InvalidURIError, wire if wire.empty?
33
+
34
+ wire
35
+ rescue URI::InvalidURIError
36
+ invalid!("URI", wire)
37
+ end
38
+
39
+ def self.format(value)
40
+ parse(value.to_s)
41
+ end
42
+ end
43
+
44
+ # RFC 5646 language tag.
45
+ class LanguageTag < Base
46
+ GRANDFATHERED = %w[
47
+ en-GB-oed i-ami i-bnn i-default i-enochian i-hak i-klingon i-lux
48
+ i-mingo i-navajo i-pwn i-tao i-tay i-tsu sgn-BE-FR sgn-BE-NL
49
+ sgn-CH-DE
50
+ ].map(&:downcase).freeze
51
+ ALNUM = "[a-z0-9]"
52
+ LANGTAG = /\A
53
+ (?:[a-z]{2,3}(?:-[a-z]{3}){0,3}|[a-z]{4}|[a-z]{5,8})
54
+ (?:-[a-z]{4})?
55
+ (?:-(?:[a-z]{2}|\d{3}))?
56
+ (?:-(?:#{ALNUM}{5,8}|\d#{ALNUM}{3}))*
57
+ (?:-[0-9a-wy-z](?:-#{ALNUM}{2,8})+)*
58
+ (?:-x(?:-#{ALNUM}{1,8})+)?
59
+ \z/xi
60
+ PRIVATE_USE = /\Ax(?:-#{ALNUM}{1,8})+\z/i
61
+
62
+ def self.native?(value)
63
+ value.is_a?(::String) && valid?(value)
64
+ end
65
+
66
+ def self.parse(wire)
67
+ return wire if wire.match?(LANGTAG) || wire.match?(PRIVATE_USE) ||
68
+ GRANDFATHERED.include?(wire.downcase)
69
+
70
+ invalid!("language-tag", wire)
71
+ end
72
+
73
+ def self.format(value)
74
+ parse(value.to_s)
75
+ end
76
+ end
77
+
78
+ # Base64 (RFC 4648 section 4), strict. The native value is a Blob: a
79
+ # plain binary String would be re-labelled as UTF-8 by lutaml-model's
80
+ # attribute casting, which would make it unreadable as raw bytes.
81
+ class Binary < Base
82
+ # Immutable holder of raw bytes.
83
+ class Blob
84
+ attr_reader :bytes
85
+
86
+ def initialize(bytes)
87
+ @bytes = bytes.b.freeze
88
+ freeze
89
+ end
90
+
91
+ def ==(other)
92
+ other.is_a?(Blob) && bytes == other.bytes
93
+ end
94
+ alias eql? ==
95
+
96
+ def hash
97
+ bytes.hash
98
+ end
99
+
100
+ def to_s
101
+ bytes
102
+ end
103
+ end
104
+
105
+ def self.native?(value)
106
+ value.is_a?(Blob)
107
+ end
108
+
109
+ def self.parse(wire)
110
+ Blob.new(wire.unpack1("m0"))
111
+ rescue ::ArgumentError
112
+ invalid!("binary", wire)
113
+ end
114
+
115
+ def self.format(value)
116
+ bytes = value.is_a?(Blob) ? value.bytes : value.to_s
117
+ [bytes].pack("m0")
118
+ end
119
+
120
+ def self.serialize(value)
121
+ value.nil? ? nil : format(value)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "value/base"
5
+ require_relative "value/text"
6
+ require_relative "value/scalar"
7
+ require_relative "value/date_and_or_time"
8
+
9
+ module Lutaml
10
+ module Vformat
11
+ # The vObject value types.
12
+ module Value
13
+ TYPES = {
14
+ text: Text,
15
+ uri: Uri,
16
+ binary: Binary,
17
+ boolean: Boolean,
18
+ integer: Integer,
19
+ float: Float,
20
+ utc_offset: UtcOffset,
21
+ language_tag: LanguageTag,
22
+ date_and_or_time: DateAndOrTime,
23
+ }.freeze
24
+
25
+ # RFC 6350 VALUE= parameter spellings that are not just TYPES keys.
26
+ ALIASES = {
27
+ "date" => :date_and_or_time, "time" => :date_and_or_time,
28
+ "date-time" => :date_and_or_time,
29
+ "date-and-or-time" => :date_and_or_time,
30
+ "timestamp" => :date_and_or_time,
31
+ "utc-offset" => :utc_offset, "language-tag" => :language_tag
32
+ }.freeze
33
+
34
+ # @param name [Symbol, String] :integer, "integer", "language-tag", ...
35
+ def self.for(name)
36
+ key = name.to_s.downcase
37
+ TYPES[key.tr("-",
38
+ "_").to_sym] || (ALIASES[key] && TYPES[ALIASES[key]]) ||
39
+ raise(ArgumentError, "unknown vObject value type: #{name.inspect}")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # Components to text: CRLF line endings, lines folded at 75 octets.
6
+ class Writer
7
+ QUOTE_NEEDED = /[:;,]/
8
+
9
+ def self.write(components)
10
+ new.write(components)
11
+ end
12
+
13
+ def write(components)
14
+ components.flat_map { |c| component_lines(c) }
15
+ .map { |line| Folding.fold(line) + Folding::CRLF }.join
16
+ end
17
+
18
+ private
19
+
20
+ def component_lines(component)
21
+ [
22
+ "BEGIN:#{component.name}",
23
+ *component.properties.map { |p| property_line(p) },
24
+ *component.components.flat_map { |c| component_lines(c) },
25
+ "END:#{component.name}",
26
+ ]
27
+ end
28
+
29
+ def property_line(property)
30
+ head = [property.group && "#{property.group}.", property.name].join
31
+ params = property.params.map { |k, v| ";#{k}=#{param_values(k, v)}" }
32
+ "#{head}#{params.join}:#{property.value}"
33
+ end
34
+
35
+ def param_values(name, values)
36
+ values.map { |v| param_value(name, v) }.join(",")
37
+ end
38
+
39
+ def param_value(name, value)
40
+ if value.match?(/["\r\n]/)
41
+ raise SerializeError,
42
+ "parameter #{name} value #{value.inspect} contains a " \
43
+ "double quote or line break, which cannot be written"
44
+ end
45
+
46
+ value.match?(QUOTE_NEEDED) ? "\"#{value}\"" : value
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "vformat/version"
5
+ require_relative "vformat/errors"
6
+ require_relative "vformat/folding"
7
+ require_relative "vformat/escaping"
8
+ require_relative "vformat/structured"
9
+ require_relative "vformat/property"
10
+ require_relative "vformat/component"
11
+ require_relative "vformat/parser"
12
+ require_relative "vformat/writer"
13
+ require_relative "vformat/document"
14
+ require_relative "vformat/value"
15
+ require_relative "vformat/codec"
16
+ require_relative "vformat/adapter"
17
+ require_relative "vformat/mapping_rule"
18
+ require_relative "vformat/mapping"
19
+ require_relative "vformat/model_support"
20
+ require_relative "vformat/model_reader"
21
+ require_relative "vformat/model_writer"
22
+ require_relative "vformat/transform"
23
+
24
+ module Lutaml
25
+ # The vObject wire format (vCard, iCalendar) for Lutaml::Model.
26
+ module Vformat
27
+ FORMAT = :vobject
28
+
29
+ # Registers :vobject into Lutaml::Model::FormatRegistry. Runs on require;
30
+ # calling it again is a no-op while this gem's registration is in place.
31
+ def self.register!
32
+ registry = Lutaml::Model::FormatRegistry
33
+ return if registry.mappings_class_for(FORMAT) == Mapping
34
+
35
+ registry.register(
36
+ FORMAT,
37
+ mapping_class: Mapping,
38
+ adapter_class: Adapter,
39
+ transformer: Transform,
40
+ error_types: [ParseError],
41
+ )
42
+ end
43
+
44
+ register!
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/lutaml/vformat/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "lutaml-vformat"
7
+ spec.version = Lutaml::Vformat::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "vObject wire format (RFC 6350 / RFC 5545 style) for " \
12
+ "Lutaml::Model"
13
+ spec.description = <<~DESC
14
+ Parses and serializes the vObject wire format used by vCard and iCalendar:
15
+ BEGIN/END component nesting, property parameters, structured values, text
16
+ escaping and 75-octet line folding, with the vObject value types. Registers
17
+ the :vobject format into Lutaml::Model::FormatRegistry when required.
18
+ DESC
19
+
20
+ spec.homepage = "https://github.com/lutaml/lutaml-vformat"
21
+ spec.license = "BSD-2-Clause"
22
+
23
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
24
+
25
+ spec.metadata["homepage_uri"] = spec.homepage
26
+ spec.metadata["source_code_uri"] = spec.homepage
27
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
28
+ spec.metadata["rubygems_mfa_required"] = "true"
29
+
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
+ `git ls-files -z`.split("\x0").reject do |f|
32
+ f.match(%r{^(test|spec|features|\.github)/})
33
+ end
34
+ end
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_dependency "lutaml-model", "~> 0.8.164"
38
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lutaml-vformat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lutaml-model
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.8.164
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.8.164
26
+ description: |
27
+ Parses and serializes the vObject wire format used by vCard and iCalendar:
28
+ BEGIN/END component nesting, property parameters, structured values, text
29
+ escaping and 75-octet line folding, with the vObject value types. Registers
30
+ the :vobject format into Lutaml::Model::FormatRegistry when required.
31
+ email:
32
+ - open.source@ribose.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - ".gitignore"
38
+ - ".rspec"
39
+ - ".rubocop.yml"
40
+ - Gemfile
41
+ - LICENSE.md
42
+ - README.adoc
43
+ - Rakefile
44
+ - lib/lutaml/vformat.rb
45
+ - lib/lutaml/vformat/adapter.rb
46
+ - lib/lutaml/vformat/codec.rb
47
+ - lib/lutaml/vformat/component.rb
48
+ - lib/lutaml/vformat/document.rb
49
+ - lib/lutaml/vformat/errors.rb
50
+ - lib/lutaml/vformat/escaping.rb
51
+ - lib/lutaml/vformat/folding.rb
52
+ - lib/lutaml/vformat/mapping.rb
53
+ - lib/lutaml/vformat/mapping_rule.rb
54
+ - lib/lutaml/vformat/model_reader.rb
55
+ - lib/lutaml/vformat/model_support.rb
56
+ - lib/lutaml/vformat/model_writer.rb
57
+ - lib/lutaml/vformat/parser.rb
58
+ - lib/lutaml/vformat/property.rb
59
+ - lib/lutaml/vformat/structured.rb
60
+ - lib/lutaml/vformat/transform.rb
61
+ - lib/lutaml/vformat/value.rb
62
+ - lib/lutaml/vformat/value/base.rb
63
+ - lib/lutaml/vformat/value/date_and_or_time.rb
64
+ - lib/lutaml/vformat/value/scalar.rb
65
+ - lib/lutaml/vformat/value/text.rb
66
+ - lib/lutaml/vformat/version.rb
67
+ - lib/lutaml/vformat/writer.rb
68
+ - lutaml-vformat.gemspec
69
+ homepage: https://github.com/lutaml/lutaml-vformat
70
+ licenses:
71
+ - BSD-2-Clause
72
+ metadata:
73
+ homepage_uri: https://github.com/lutaml/lutaml-vformat
74
+ source_code_uri: https://github.com/lutaml/lutaml-vformat
75
+ changelog_uri: https://github.com/lutaml/lutaml-vformat/releases
76
+ rubygems_mfa_required: 'true'
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 3.0.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 4.0.23
92
+ specification_version: 4
93
+ summary: vObject wire format (RFC 6350 / RFC 5545 style) for Lutaml::Model
94
+ test_files: []