Skip to content

Commit 90d5b85

Browse files
nevanssaturnflyer
andcommitted
✨ Add Data polyfill for ruby 3.1
For new data structs, I'd prefer frozen by default and I don't want to support the entire Struct API. Data is perfect, but it's not available until ruby 3.2. So this adds a DataLite class that closely matches ruby 3.2's Data class, and it can be a drop-in replacement for Data. Net::IMAP::Data is an alias for Net::IMAP::DataLite, so when we remove our implementation, the constant will resolve to ruby's ::Data. Ideally, we wouldn't define this on newer ruby versions at all, but that breaks the YAML serialization for our test fixtures. The tests (and some of the implementation) have been copied from the polyfill-data gem and updated so that they use "Data" as it is resolved inside the "Net::IMAP" namespace. Copyright notices have been added to the appropriate files to satisfy the MIT license terms. Co-authored-by: Jim Gay <jim@saturnflyer.com>
1 parent ea47e34 commit 90d5b85

File tree

3 files changed

+462
-0
lines changed

3 files changed

+462
-0
lines changed

lib/net/imap.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3276,6 +3276,7 @@ def self.saslprep(string, **opts)
32763276
require_relative "imap/config"
32773277
require_relative "imap/command_data"
32783278
require_relative "imap/data_encoding"
3279+
require_relative "imap/data_lite"
32793280
require_relative "imap/flags"
32803281
require_relative "imap/response_data"
32813282
require_relative "imap/response_parser"

lib/net/imap/data_lite.rb

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# frozen_string_literal: true
2+
3+
# Some of the code in this file was copied from the polyfill-data gem.
4+
#
5+
# MIT License
6+
#
7+
# Copyright (c) 2023 Jim Gay, Joel Drapper, Nicholas Evans
8+
#
9+
# Permission is hereby granted, free of charge, to any person obtaining a copy
10+
# of this software and associated documentation files (the "Software"), to deal
11+
# in the Software without restriction, including without limitation the rights
12+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
# copies of the Software, and to permit persons to whom the Software is
14+
# furnished to do so, subject to the following conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be included in all
17+
# copies or substantial portions of the Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
# SOFTWARE.
26+
27+
# TODO: fix tests to allow same yaml for both Data and DataLite
28+
# NOTE: psych 5.1.2 doesn't encode ::Data correctly!
29+
# if RUBY_VERSION >= "3.2.0"
30+
# return
31+
# end
32+
33+
module Net
34+
class IMAP
35+
36+
# See {ruby's documentation for Data}[https://docs.ruby-lang.org/en/3.3/Data.html].
37+
#
38+
# DataLite is a temporary polyfill for ruby 3.2's
39+
# Data[https://docs.ruby-lang.org/en/3.3/Data.html]. <em>This class is
40+
# not defined for ruby versions >= 3.2.</em> It will only be defined when
41+
# using ruby 3.1 (+net-imap+ no longer supports ruby versions < 3.1). It
42+
# <em>will be removed</em> in +net-imap+ 0.6, when support for ruby 3.1 is
43+
# dropped.
44+
#
45+
# It is aliased as Net::IMAP::Data so that, in ruby 3.1, any reference to
46+
# "Data" that is namespaced inside Net::IMAP will use it. This way,
47+
# Net::IMAP's code shouldn't need to change to work with both
48+
# Net::IMAP::DataLite and
49+
# {::Data}[https://docs.ruby-lang.org/en/3.3/Data.html].
50+
#
51+
# Some of the code in this class was copied or adapted from the
52+
# {polyfill-data gem}[https://rubygems.org/gems/polyfill-data], by Jim Gay
53+
# and Joel Drapper, under the MIT license terms.
54+
class DataLite
55+
singleton_class.undef_method :new
56+
57+
TYPE_ERROR = "%p is not a symbol nor a string"
58+
ATTRSET_ERROR = "invalid data member: %p"
59+
DUP_ERROR = "duplicate member: %p"
60+
ARITY_ERROR = "wrong number of arguments (given %d, expected %s)"
61+
private_constant :TYPE_ERROR, :ATTRSET_ERROR, :DUP_ERROR, :ARITY_ERROR
62+
63+
# *NOTE:+ DataLite.define does not support member names which are not
64+
# valid local variable names.
65+
def self.define(*args, &block)
66+
members = args.each_with_object({}) do |arg, members|
67+
arg = arg.to_str unless arg in Symbol | String if arg.respond_to?(:to_str)
68+
arg = arg.to_sym if arg in String
69+
arg in Symbol or raise TypeError, TYPE_ERROR % [arg]
70+
arg in %r{=} and raise ArgumentError, ATTRSET_ERROR % [arg]
71+
members.key?(arg) and raise ArgumentError, DUP_ERROR % [arg]
72+
members[arg] = true
73+
end
74+
members = members.keys.freeze
75+
76+
klass = ::Class.new(self)
77+
78+
klass.singleton_class.undef_method :define
79+
klass.define_singleton_method(:members) { members }
80+
81+
def klass.new(*args, **kwargs, &block)
82+
if kwargs.size.positive?
83+
if args.size.positive?
84+
raise ArgumentError, ARITY_ERROR % [args.size, 0]
85+
end
86+
elsif members.size < args.size
87+
expected = members.size.zero? ? 0 : 0..members.size
88+
raise ArgumentError, ARITY_ERROR % [args.size, expected]
89+
else
90+
kwargs = Hash[members.take(args.size).zip(args)]
91+
end
92+
allocate.tap do |instance|
93+
instance.__send__(:initialize, **kwargs, &block)
94+
end.freeze
95+
end
96+
97+
klass.singleton_class.alias_method :[], :new
98+
klass.attr_reader(*members)
99+
100+
# Dynamically defined initializer methods are in an included module,
101+
# rather than directly on DataLite (like in ruby 3.2+):
102+
# * simpler to handle required kwarg ArgumentErrors
103+
# * easier to ensure consistent ivar assignment order (object shape)
104+
# * faster than instance_variable_set
105+
klass.include(Module.new do
106+
if members.any?
107+
kwargs = members.map{"#{_1.name}:"}.join(", ")
108+
params = members.map(&:name).join(", ")
109+
ivars = members.map{"@#{_1.name}"}.join(", ")
110+
attrs = members.map{"attrs[:#{_1.name}]"}.join(", ")
111+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
112+
protected
113+
def initialize(#{kwargs}) #{ivars} = #{params}; freeze end
114+
def marshal_load(attrs) #{ivars} = #{attrs}; freeze end
115+
RUBY
116+
end
117+
end)
118+
119+
klass.module_eval do _1.module_eval(&block) end if block_given?
120+
121+
klass
122+
end
123+
124+
def members; self.class.members end
125+
def attributes; Hash[members.map {|m| [m, send(m)] }] end
126+
def to_h(&block) attributes.to_h(&block) end
127+
def hash; to_h.hash end
128+
def ==(other) self.class == other.class && to_h == other.to_h end
129+
def eql?(other) self.class == other.class && hash == other.hash end
130+
def deconstruct; attributes.values end
131+
132+
def deconstruct_keys(keys)
133+
raise TypeError unless keys.is_a?(Array) || keys.nil?
134+
return attributes if keys&.first.nil?
135+
attributes.slice(*keys)
136+
end
137+
138+
def with(**kwargs)
139+
return self if kwargs.empty?
140+
self.class.new(**attributes.merge(kwargs))
141+
end
142+
143+
# +NOTE:+ Unlike ruby 3.2's <tt>Data#inspect</tt>, this has no guard
144+
# against infinite recursion.
145+
def inspect
146+
attrs = attributes.map {|kv| "%s=%p" % kv }.join(", ")
147+
display = ["data", self.class.name, attrs].compact.join(" ")
148+
"#<#{display}>"
149+
end
150+
alias_method :to_s, :inspect
151+
152+
def encode_with(coder) coder.map = attributes.transform_keys(&:to_s) end
153+
def init_with(coder) marshal_load(coder.map.transform_keys(&:to_sym)) end
154+
155+
private
156+
157+
def initialize_copy(source) super.freeze end
158+
def marshal_dump; attributes end
159+
160+
end
161+
162+
Data = DataLite
163+
164+
end
165+
end

0 commit comments

Comments
 (0)