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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1b4498040b2d01535f5469b36d771aa21ba3ed86ece6149af63fe5fd2a78a71d
4
+ data.tar.gz: b17bc582b5a99aa90aecb71b27547ffee35accc9d74f5ea85f7a46170e1176db
5
+ SHA512:
6
+ metadata.gz: a258aa0b6bae31d8ad38c57f00468917a41b290f6ac25515416f419bd4c5eeec290fda5bc66487abc90098a0a0e0bb78c9f2119471ed55a89de1a16b64da7244
7
+ data.tar.gz: 955446d8c4ed7d24a71473f7ef34cb24fa2e8fd8690dbbdc4bb366d41693ab9ee8db56b2835483b16d70df641d4646ba3bf5602fff78fc35ab9f6955a47647db
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /Gemfile.lock
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ .DS_Store
15
+ .rubocop-https---*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ # See https://github.com/metanorma/cimas
2
+ inherit_from:
3
+ - https://raw.githubusercontent.com/riboseinc/oss-guides/main/ci/rubocop.yml
4
+
5
+ # local repo-specific modifications
6
+ # ...
7
+
8
+ AllCops:
9
+ NewCops: disable
10
+ TargetRubyVersion: 3.0
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake"
8
+ gem "rspec"
9
+ gem "rubocop"
10
+ gem "rubocop-performance"
data/LICENSE.md ADDED
@@ -0,0 +1,33 @@
1
+ Licenses & Copyright
2
+ ====================
3
+
4
+ This license file adheres to the formatting guidelines of
5
+ [readable-licenses](https://github.com/nevir/readable-licenses).
6
+
7
+
8
+ Ribose's BSD 2-Clause License
9
+ -----------------------------
10
+
11
+ Copyright (c) 2026, [Ribose Inc](https://www.ribose.com).
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without modification,
15
+ are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice,
18
+ this list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
33
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.adoc ADDED
@@ -0,0 +1,172 @@
1
+ = lutaml-vformat
2
+
3
+ The vObject wire format (the line format shared by RFC 6350 vCard and RFC 5545
4
+ iCalendar) for https://github.com/lutaml/lutaml-model[lutaml-model]. The gem
5
+ registers a `:vobject` format into `Lutaml::Model::FormatRegistry` when it is
6
+ required, so models gain `from_vobject` and `to_vobject`.
7
+
8
+ This gem is the wire layer only. It has no vCard or iCalendar vocabulary; the
9
+ vCard property set is a separate gem built on this one.
10
+
11
+ == Installation
12
+
13
+ [source,ruby]
14
+ ----
15
+ gem "lutaml-vformat"
16
+ ----
17
+
18
+ Requires `lutaml-model` `~> 0.8.164` (the version the registration API was
19
+ measured against) and Ruby 3.0 or later.
20
+
21
+ == What it covers
22
+
23
+ * `BEGIN:` / `END:` component nesting to any depth
24
+ * property parameters, including quoted values and comma-separated lists
25
+ * property groups (`item1.TEL`)
26
+ * structured values (`;` between fields, `,` between values in a field)
27
+ * text escaping: backslash, comma, semicolon and newline
28
+ * line folding at 75 octets (counted in octets, never inside a UTF-8
29
+ sequence) and unfolding
30
+ * value types: text, uri, binary, boolean, integer, float, utc-offset,
31
+ language-tag, date-and-or-time
32
+
33
+ Output uses CRLF line endings and ends with one.
34
+
35
+ == Usage
36
+
37
+ [source,ruby]
38
+ ----
39
+ require "lutaml/vformat"
40
+
41
+ class Phone < Lutaml::Model::Serializable
42
+ attribute :number, :string
43
+ attribute :types, :string, collection: true
44
+ attribute :pref, :integer
45
+
46
+ vobject do
47
+ type :property
48
+ map_value to: :number
49
+ map_param "TYPE", to: :types
50
+ map_param "PREF", to: :pref
51
+ end
52
+ end
53
+
54
+ class PersonName < Lutaml::Model::Serializable
55
+ attribute :family, :string
56
+ attribute :given, :string
57
+
58
+ vobject do
59
+ type :property
60
+ map_field 0, to: :family # N:family;given
61
+ map_field 1, to: :given
62
+ end
63
+ end
64
+
65
+ class Card < Lutaml::Model::Serializable
66
+ attribute :fn, :string
67
+ attribute :n, PersonName
68
+ attribute :tel, Phone, collection: true
69
+ attribute :bday, Lutaml::Vformat::Value::DateAndOrTime
70
+
71
+ vobject do
72
+ type :component
73
+ component_root "VCARD"
74
+ map_property "FN", to: :fn
75
+ map_property "N", to: :n
76
+ map_property "TEL", to: :tel
77
+ map_property "BDAY", to: :bday
78
+ end
79
+ end
80
+
81
+ card = Card.from_vobject(<<~VCARD.gsub("\n", "\r\n"))
82
+ BEGIN:VCARD
83
+ FN:Jo Doe
84
+ N:Doe;Jo
85
+ TEL;TYPE=work,voice;PREF=1:+1 555 0100
86
+ BDAY:19850412
87
+ END:VCARD
88
+ VCARD
89
+
90
+ card.tel.first.types # => ["work", "voice"]
91
+ card.bday.kind # => :date
92
+ card.to_vobject # => "BEGIN:VCARD\r\nFN:Jo Doe\r\n..."
93
+ ----
94
+
95
+ === Mapping DSL
96
+
97
+ Inside `vobject do ... end`:
98
+
99
+ [cols="2,5"]
100
+ |===
101
+ | `type :component` / `type :property` | What the model represents.
102
+ | `component_root "NAME"` | The `BEGIN:` name of a top-level component model.
103
+ | `map_property "NAME", to: :attr` | A content line of the component. The attribute is a scalar type or a `:property` model. Repeated lines need `collection: true`.
104
+ | `map_component "NAME", to: :attr` | A nested `BEGIN:NAME` block; the attribute is a `:component` model.
105
+ | `map_value to: :attr` | In a property model: the property value.
106
+ | `map_param "NAME", to: :attr` | In a property model: a parameter. A `collection: true` attribute receives every value.
107
+ | `map_group to: :attr` | In a property model: the group prefix.
108
+ | `map_field index, to: :attr` | In a property model: one field of a structured value.
109
+ |===
110
+
111
+ `map_property`, `map_value`, `map_param` and `map_field` accept
112
+ `value_type:` (`:text`, `:uri`, `:binary`, `:boolean`, `:integer`, `:float`,
113
+ `:utc_offset`, `:language_tag`, `:date_and_or_time`). Without it the type
114
+ comes from the attribute (`:string`, `:integer`, `:float`, `:boolean` and the
115
+ `Lutaml::Vformat::Value` classes).
116
+
117
+ === Value types
118
+
119
+ [cols="2,3,3"]
120
+ |===
121
+ | Type | Ruby object | Wire form
122
+
123
+ | `Value::Text` | `String` | escaped
124
+ | `Value::Uri` | `String` | as is
125
+ | `Value::Binary` | `Value::Binary::Blob` (`#bytes`) | strict base64
126
+ | `Value::Boolean` | `true` / `false` | `TRUE` / `FALSE`
127
+ | `Value::Integer` | `Integer` | `-12`
128
+ | `Value::Float` | `Float` | `1.5`, never exponent form
129
+ | `Value::UtcOffset` | `Integer` (seconds east of UTC) | `+0530`, `-0800`
130
+ | `Value::LanguageTag` | `String` | RFC 5646 tag
131
+ | `Value::DateAndOrTime` | `Value::DateAndOrTime::Instant` (`#kind`, `#to_s`) | `19850412`, `--0412`, `19961022T140000Z`, `T1022`
132
+ |===
133
+
134
+ Each class has `.parse(wire)` and `.format(object)`; an invalid wire value
135
+ raises `Lutaml::Vformat::ValueError`.
136
+
137
+ === Without models
138
+
139
+ [source,ruby]
140
+ ----
141
+ document = Lutaml::Vformat::Document.parse(text)
142
+ event = document.component("VCALENDAR").components_named("VEVENT").first
143
+ event.property("DTSTART").value
144
+ event.property("DTSTART").param("TZID")
145
+ document.to_vobject
146
+ ----
147
+
148
+ Names and parameter names are case-insensitive and kept upper-case.
149
+ Property values stay in wire (escaped) form; use `Property#text` or
150
+ `Property#fields` for the unescaped view.
151
+
152
+ == Errors
153
+
154
+ Malformed input (unbalanced `BEGIN`/`END`, a line without `:`, invalid
155
+ UTF-8, a value that does not match its type) raises
156
+ `Lutaml::Vformat::ParseError`; through `Model.from_vobject` it surfaces as
157
+ `Lutaml::Model::InvalidFormatError`. A parameter value that contains a double
158
+ quote cannot be written and raises `Lutaml::Vformat::SerializeError`.
159
+
160
+ == Not covered in 0.1.0
161
+
162
+ * the vCard and iCalendar vocabularies
163
+ * vCard 2.1 syntax (bare parameters such as `TEL;WORK:`, quoted-printable)
164
+ * RFC 6868 caret encoding of parameter values
165
+ * the `VALUE=` parameter: it is available as `Property#value_type` but models
166
+ neither read nor write it; name the type with `value_type:` or the
167
+ attribute type
168
+ * converting `DateAndOrTime` to `Date` or `Time`
169
+
170
+ == License
171
+
172
+ BSD-2-Clause. See link:LICENSE.md[LICENSE.md].
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # The adapter registered for :vobject. Parsing returns an Adapter (a
6
+ # Document); serializing wraps the Document built by the transform.
7
+ class Adapter < Document
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # Wire text <-> attribute values. A vObject value type (or the
6
+ # +value_type:+ of a mapping rule) wins; plain lutaml-model String,
7
+ # Integer, Float and Boolean attributes use the matching vObject type;
8
+ # anything else is cast from the unescaped text.
9
+ module Codec
10
+ BUILTIN = {
11
+ Lutaml::Model::Type::String => Value::Text,
12
+ Lutaml::Model::Type::Integer => Value::Integer,
13
+ Lutaml::Model::Type::Float => Value::Float,
14
+ Lutaml::Model::Type::Boolean => Value::Boolean,
15
+ }.freeze
16
+
17
+ module_function
18
+
19
+ def value_class(type, value_type = nil)
20
+ return Value.for(value_type) if value_type
21
+ return type if type <= Value::Base
22
+
23
+ BUILTIN[type]
24
+ end
25
+
26
+ def read(type, wire, value_type: nil)
27
+ klass = value_class(type, value_type)
28
+ return klass.parse(wire) if klass
29
+
30
+ type.cast(Escaping.unescape(wire))
31
+ end
32
+
33
+ # Parameter values are never text-escaped.
34
+ def read_param(type, text, value_type: nil)
35
+ klass = value_class(type, value_type)
36
+ return type.cast(text) if klass.nil?
37
+
38
+ klass.escaped? ? text : klass.parse(text)
39
+ end
40
+
41
+ def write(type, value, value_type: nil)
42
+ klass = value_class(type, value_type)
43
+ return klass.format(value) if klass
44
+
45
+ return Value::Boolean.format(value) if [true, false].include?(value)
46
+
47
+ Escaping.escape(type.serialize(value).to_s)
48
+ end
49
+
50
+ def write_param(type, value, value_type: nil)
51
+ klass = value_class(type, value_type)
52
+ return value.to_s unless klass
53
+
54
+ klass.escaped? ? value.to_s : klass.format(value)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # A BEGIN:NAME ... END:NAME block holding properties and nested components,
6
+ # each in document order.
7
+ class Component
8
+ attr_reader :name, :properties, :components
9
+
10
+ def initialize(name, properties: [], components: [])
11
+ @name = name.to_s.upcase
12
+ @properties = properties
13
+ @components = components
14
+ end
15
+
16
+ def properties_named(name)
17
+ properties.select { |p| p.name == name.to_s.upcase }
18
+ end
19
+
20
+ def components_named(name)
21
+ components.select { |c| c.name == name.to_s.upcase }
22
+ end
23
+
24
+ def property(name)
25
+ properties_named(name).first
26
+ end
27
+
28
+ def ==(other)
29
+ other.is_a?(Component) && [name, properties, components] ==
30
+ [other.name, other.properties, other.components]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # A parsed vObject stream: zero or more top-level components.
6
+ class Document
7
+ attr_reader :components
8
+
9
+ def self.parse(data, _options = {})
10
+ new(Parser.parse(data))
11
+ end
12
+
13
+ def initialize(components = [], _options = {})
14
+ components = components.components if components.is_a?(Document)
15
+ @components = Array(components)
16
+ end
17
+
18
+ def component(name)
19
+ @components.find { |c| c.name == name.to_s.upcase }
20
+ end
21
+
22
+ def to_vobject(_options = {})
23
+ Writer.write(@components)
24
+ end
25
+
26
+ def ==(other)
27
+ other.is_a?(Document) && components == other.components
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ class Error < StandardError; end
6
+
7
+ # Malformed vObject input. Registered as the format's error type, so
8
+ # Model.from_vobject surfaces it as Lutaml::Model::InvalidFormatError.
9
+ class ParseError < Error; end
10
+
11
+ # A value that does not match its declared vObject value type.
12
+ class ValueError < ParseError; end
13
+
14
+ # A model or document that cannot be written as vObject.
15
+ class SerializeError < Error; end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # Text value escaping (RFC 6350 section 3.4): backslash, semicolon and
6
+ # comma get a backslash prefix; a line break becomes the two characters
7
+ # backslash-n.
8
+ module Escaping
9
+ BACKSLASH = "\\"
10
+ ESCAPES = {
11
+ BACKSLASH => BACKSLASH * 2,
12
+ ";" => "#{BACKSLASH};",
13
+ "," => "#{BACKSLASH},",
14
+ "\n" => "#{BACKSLASH}n",
15
+ }.freeze
16
+ ESCAPABLE = Regexp.union(ESCAPES.keys)
17
+ ESCAPE_SEQUENCE = /#{Regexp.escape(BACKSLASH)}(.)/m
18
+ UNESCAPES = { "n" => "\n", "N" => "\n" }.freeze
19
+
20
+ module_function
21
+
22
+ def escape(text)
23
+ text.to_s.gsub(/\r\n|\r|\n/, "\n").gsub(ESCAPABLE, ESCAPES)
24
+ end
25
+
26
+ # Unknown escapes drop the backslash; a trailing lone backslash is
27
+ # kept literally.
28
+ def unescape(text)
29
+ text.gsub(ESCAPE_SEQUENCE) do
30
+ char = Regexp.last_match(1)
31
+ UNESCAPES.fetch(char, char)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # Line folding (RFC 6350 section 3.2, RFC 5545 section 3.1).
6
+ #
7
+ # The limit counts octets of the encoded line, not characters, and a fold
8
+ # never falls inside a multi-byte UTF-8 sequence. A continuation line
9
+ # starts with one space that counts toward its limit.
10
+ module Folding
11
+ LIMIT = 75
12
+ CRLF = "\r\n"
13
+
14
+ module_function
15
+
16
+ def fold(line, limit: LIMIT, eol: CRLF)
17
+ bytes = line.b
18
+ return line.dup if bytes.bytesize <= limit
19
+
20
+ chunks(bytes, limit).join("#{eol} ".b).force_encoding(line.encoding)
21
+ end
22
+
23
+ # The first chunk gets +limit+ octets; later ones one fewer, because
24
+ # each is written after a leading space.
25
+ def chunks(bytes, limit)
26
+ chunks = []
27
+ pos = 0
28
+ while pos < bytes.bytesize
29
+ cut = cut_point(bytes, pos, pos.zero? ? limit : limit - 1)
30
+ chunks << bytes.byteslice(pos, cut - pos)
31
+ pos = cut
32
+ end
33
+ chunks
34
+ end
35
+
36
+ def unfold(text)
37
+ text.gsub(/\r?\n[ \t]/, "")
38
+ end
39
+
40
+ # Largest end offset, at most +budget+ bytes after +pos+, that is not
41
+ # in the middle of a UTF-8 sequence.
42
+ def cut_point(bytes, pos, budget)
43
+ cut = pos + budget
44
+ return bytes.bytesize if cut >= bytes.bytesize
45
+
46
+ cut -= 1 while cut > pos && continuation?(bytes.getbyte(cut))
47
+ cut
48
+ end
49
+
50
+ def continuation?(byte)
51
+ (byte & 0xC0) == 0x80
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # The +vobject do ... end+ block.
6
+ #
7
+ # vobject do
8
+ # type :component # or :property
9
+ # component_root "VEVENT" # type :component, top-level name
10
+ # property_root "TEL" # type :property, informational
11
+ #
12
+ # map_property "SUMMARY", to: :summary
13
+ # map_component "VALARM", to: :alarms
14
+ #
15
+ # # in a property model:
16
+ # map_value to: :value
17
+ # map_param "TYPE", to: :types
18
+ # map_group to: :group
19
+ # map_field 0, to: :family # structured value, ";"-separated
20
+ # end
21
+ class Mapping < Lutaml::Model::Mapping
22
+ TYPES = %i[component property].freeze
23
+
24
+ attr_reader :mappings, :element_type, :component_name, :property_name
25
+
26
+ def initialize
27
+ super
28
+ @element_type = nil
29
+ @component_name = nil
30
+ @property_name = nil
31
+ end
32
+
33
+ def deep_dup
34
+ duped = super
35
+ duped.copy_roots(@element_type, @component_name, @property_name)
36
+ duped
37
+ end
38
+
39
+ def copy_roots(element_type, component_name, property_name)
40
+ @element_type = element_type
41
+ @component_name = component_name
42
+ @property_name = property_name
43
+ end
44
+
45
+ def finalize(_mapper_class)
46
+ @finalized = true
47
+ end
48
+
49
+ def finalized?
50
+ @finalized == true
51
+ end
52
+
53
+ def type(element_type)
54
+ unless TYPES.include?(element_type)
55
+ raise ArgumentError,
56
+ "Invalid element type: #{element_type.inspect} " \
57
+ "(expected one of #{TYPES.inspect})"
58
+ end
59
+
60
+ @element_type = element_type
61
+ end
62
+
63
+ def component_root(name)
64
+ @component_name = name.to_s.upcase
65
+ end
66
+
67
+ def property_root(name)
68
+ @property_name = name.to_s.upcase
69
+ end
70
+
71
+ def map_property(name, to:, value_type: nil)
72
+ add(name.to_s.upcase, to, :property, value_type)
73
+ end
74
+
75
+ def map_component(name, to:)
76
+ add(name.to_s.upcase, to, :component, nil)
77
+ end
78
+
79
+ def map_value(to:, value_type: nil)
80
+ add("VALUE", to, :value, value_type)
81
+ end
82
+
83
+ def map_param(name, to:, value_type: nil)
84
+ add(name.to_s.upcase, to, :param, value_type)
85
+ end
86
+
87
+ def map_field(index, to:, value_type: nil)
88
+ add(index, to, :field, value_type)
89
+ end
90
+
91
+ def map_group(to:)
92
+ add("GROUP", to, :group, nil)
93
+ end
94
+
95
+ def rules(kind)
96
+ @mappings.select { |r| r.kind == kind }
97
+ end
98
+
99
+ private
100
+
101
+ def add(name, to, kind, value_type)
102
+ @mappings << MappingRule.new(name, to: to, kind: kind,
103
+ value_type: value_type)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Vformat
5
+ # One mapping line. +kind+ is :property, :component, :value, :param,
6
+ # :field or :group; +name+ is the vObject name (or field index).
7
+ class MappingRule < Lutaml::Model::MappingRule
8
+ KINDS = %i[property component value param field group].freeze
9
+
10
+ attr_reader :kind, :value_type
11
+
12
+ def initialize(name, to:, kind:, value_type: nil)
13
+ super(name, to: to)
14
+ @kind = kind
15
+ @value_type = value_type
16
+ end
17
+
18
+ def deep_dup
19
+ self.class.new(name.dup, to: to, kind: kind, value_type: value_type)
20
+ end
21
+ end
22
+ end
23
+ end