# frozen_string_literal: true # Some of the code in this file was copied from the polyfill-data gem. # # MIT License # # Copyright (c) 2023 Jim Gay, Joel Drapper, Nicholas Evans # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. module Net class IMAP data_or_object = RUBY_VERSION >= "3.2.0" ? ::Data : Object class DataLite < data_or_object def encode_with(coder) coder.map = to_h.transform_keys(&:to_s) end def init_with(coder) initialize(**coder.map.transform_keys(&:to_sym)) end end Data = DataLite end end # :nocov: # Need to skip test coverage for the rest, because it isn't loaded by ruby 3.2+. return if RUBY_VERSION >= "3.2.0" module Net class IMAP # DataLite is a temporary substitute for ruby 3.2's +Data+ class. DataLite # is aliased as Net::IMAP::Data, so that code using it won't need to be # updated when it is removed. # # See {ruby 3.2's documentation for Data}[https://docs.ruby-lang.org/en/3.2/Data.html]. # # [When running ruby 3.1] # This class reimplements the API for ruby 3.2's +Data+, and should be # compatible for nearly all use-cases. This reimplementation will be # removed in +net-imap+ 0.6, when support for ruby 3.1 is dropped. # # _NOTE:_ +net-imap+ no longer supports ruby versions prior to 3.1. # [When running ruby >= 3.2] # This class inherits from +Data+ and _only_ defines the methods needed # for YAML serialization. This will be dropped when +psych+ adds support # for +Data+. # # Some of the code in this class was copied or adapted from the # {polyfill-data gem}[https://rubygems.org/gems/polyfill-data], by Jim Gay # and Joel Drapper, under the MIT license terms. class DataLite singleton_class.undef_method :new TYPE_ERROR = "%p is not a symbol nor a string" ATTRSET_ERROR = "invalid data member: %p" DUP_ERROR = "duplicate member: %p" ARITY_ERROR = "wrong number of arguments (given %d, expected %s)" private_constant :TYPE_ERROR, :ATTRSET_ERROR, :DUP_ERROR, :ARITY_ERROR # Defines a new Data class. # # _NOTE:_ Unlike ruby 3.2's +Data.define+, DataLite.define only supports # member names which are valid local variable names. Member names can't # be keywords (e.g: +next+ or +class+) or start with capital letters, "@", # etc. def self.define(*args, &block) members = args.each_with_object({}) do |arg, members| arg = arg.to_str unless arg in Symbol | String if arg.respond_to?(:to_str) arg = arg.to_sym if arg in String arg in Symbol or raise TypeError, TYPE_ERROR % [arg] arg in %r{=} and raise ArgumentError, ATTRSET_ERROR % [arg] members.key?(arg) and raise ArgumentError, DUP_ERROR % [arg] members[arg] = true end members = members.keys.freeze klass = ::Class.new(self) klass.singleton_class.undef_method :define klass.define_singleton_method(:members) { members } def klass.new(*args, **kwargs, &block) if kwargs.size.positive? if args.size.positive? raise ArgumentError, ARITY_ERROR % [args.size, 0] end elsif members.size < args.size expected = members.size.zero? ? 0 : 0..members.size raise ArgumentError, ARITY_ERROR % [args.size, expected] else kwargs = Hash[members.take(args.size).zip(args)] end allocate.tap do |instance| instance.__send__(:initialize, **kwargs, &block) end.freeze end klass.singleton_class.alias_method :[], :new klass.attr_reader(*members) # Dynamically defined initializer methods are in an included module, # rather than directly on DataLite (like in ruby 3.2+): # * simpler to handle required kwarg ArgumentErrors # * easier to ensure consistent ivar assignment order (object shape) # * faster than instance_variable_set klass.include(Module.new do if members.any? kwargs = members.map{"#{_1.name}:"}.join(", ") params = members.map(&:name).join(", ") ivars = members.map{"@#{_1.name}"}.join(", ") attrs = members.map{"attrs[:#{_1.name}]"}.join(", ") module_eval <<~RUBY, __FILE__, __LINE__ + 1 protected def initialize(#{kwargs}) #{ivars} = #{params}; freeze end def marshal_load(attrs) #{ivars} = #{attrs}; freeze end RUBY end end) klass.module_eval do _1.module_eval(&block) end if block_given? klass end ## # singleton-method: new # call-seq: # new(*args) -> instance # new(**kwargs) -> instance # # Constuctor for classes defined with ::define. # # Aliased as ::[]. ## # singleton-method: [] # call-seq: # ::[](*args) -> instance # ::[](**kwargs) -> instance # # Constuctor for classes defined with ::define. # # Alias for ::new ## def members; self.class.members end def to_h(&block) block ? __to_h__.to_h(&block) : __to_h__ end def hash; [self.class, __to_h__].hash end def ==(other) self.class == other.class && to_h == other.to_h end def eql?(other) self.class == other.class && hash == other.hash end def deconstruct; __to_h__.values end def deconstruct_keys(keys) raise TypeError unless keys.is_a?(Array) || keys.nil? return __to_h__ if keys&.first.nil? __to_h__.slice(*keys) end def with(**kwargs) return self if kwargs.empty? self.class.new(**__to_h__.merge(kwargs)) end def inspect __inspect_guard__(self) do |seen| return "#" if seen attrs = __to_h__.map {|kv| "%s=%p" % kv }.join(", ") display = ["data", self.class.name, attrs].compact.join(" ") "#<#{display}>" end end alias_method :to_s, :inspect private def initialize_copy(source) super.freeze end def marshal_dump; __to_h__ end def __to_h__; Hash[members.map {|m| [m, send(m)] }] end # Yields +true+ if +obj+ has been seen already, +false+ if it hasn't. # Marks +obj+ as seen inside the block, so circuler references don't # recursively trigger a SystemStackError (stack level too deep). # # Making circular references inside a Data object _should_ be very # uncommon, but we'll support them for the sake of completeness. def __inspect_guard__(obj) preexisting = Thread.current[:__net_imap_data__inspect__] Thread.current[:__net_imap_data__inspect__] ||= {}.compare_by_identity inspect_guard = Thread.current[:__net_imap_data__inspect__] if inspect_guard.include?(obj) yield true else begin inspect_guard[obj] = true yield false ensure inspect_guard.delete(obj) end end ensure unless preexisting.equal?(inspect_guard) Thread.current[:__net_imap_data__inspect__] = preexisting end end end end end # :nocov: