lib/jsi/util/private.rb in jsi-0.7.0 vs lib/jsi/util/private.rb in jsi-0.8.0
- old
+ new
@@ -4,15 +4,22 @@
# JSI::Util::Private classes, modules, constants, and methods are internal, and will be added and removed without warning.
#
# @api private
module Util::Private
autoload :AttrStruct, 'jsi/util/private/attr_struct'
+ autoload :MemoMap, 'jsi/util/private/memo_map'
+ extend self
+
EMPTY_ARY = [].freeze
+ EMPTY_HASH = {}.freeze
+
EMPTY_SET = Set[].freeze
+ CLASSES_ALWAYS_FROZEN = Set[TrueClass, FalseClass, NilClass, Integer, Float, BigDecimal, Rational, Symbol].freeze
+
# is a hash as the last argument passed to keyword params? (false in ruby 3; true before - generates
# a warning in 2.7 but no way to make 2.7 behave like 3 so the warning is useless)
#
# TODO remove eventually (keyword argument compatibility)
LAST_ARGUMENT_AS_KEYWORD_PARAMETERS = begin
@@ -31,22 +38,67 @@
::Warning.send(:remove_method, :warn)
::Warning.send(:define_method, :warn, warn)
end
end
+ # we won't use #to_json on classes where it is defined by
+ # JSON::Ext::Generator::GeneratorMethods / JSON::Pure::Generator::GeneratorMethods
+ # this is a bit of a kluge and disregards any singleton class to_json, but it will do.
+ USE_TO_JSON_METHOD = Hash.new do |h, klass|
+ h[klass] = klass.method_defined?(:to_json) &&
+ klass.instance_method(:to_json).owner.name !~ /\AJSON:.*:GeneratorMethods\b/
+ end
+
+ RUBY_REJECT_NAME_CODEPOINTS = [
+ 0..31, # C0 control chars
+ %q( !"#$%&'()*+,-./:;<=>?@[\\]^`{|}~).each_codepoint, # printable special chars (note: "_" not included)
+ 127..159, # C1 control chars
+ ].inject(Set[], &:merge).freeze
+
+ RUBY_REJECT_NAME_RE = Regexp.new('[' + Regexp.escape(RUBY_REJECT_NAME_CODEPOINTS.to_a.pack('U*')) + ']+').freeze
+
# is the given name ok to use as a ruby method name?
def ok_ruby_method_name?(name)
# must be a string
return false unless name.respond_to?(:to_str)
# must not begin with a digit
return false if name =~ /\A[0-9]/
- # must not contain characters special to ruby syntax
- return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
+ # must not contain special or control characters
+ return false if name =~ RUBY_REJECT_NAME_RE
return true
end
+ def const_name_from_parts(parts, join: '')
+ parts = parts.map do |part|
+ part = part.dup
+ part[/\A[^a-zA-Z]*/] = ''
+ part[0] = part[0].upcase if part[0]
+ part.gsub!(RUBY_REJECT_NAME_RE, '_')
+ part
+ end
+ if !parts.all?(&:empty?)
+ parts.reject(&:empty?).join(join).freeze
+ else
+ nil
+ end
+ end
+
+ # string or URI → frozen URI
+ # @return [Addressable::URI]
+ def uri(uri)
+ if uri.is_a?(Addressable::URI)
+ if uri.frozen?
+ uri
+ else
+ uri.dup.freeze
+ end
+ else
+ Addressable::URI.parse(uri).freeze
+ end
+ end
+
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
# to define a recursive function to return the length of an array:
#
# length = ycomb do |len|
# proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
@@ -87,118 +139,47 @@
end
@jmespath_required = true
nil
end
+ # Defines equality methods and #hash (for Hash / Set), based on a method #jsi_fingerprint
+ # implemented by the includer. #jsi_fingerprint is to include the class and any properties
+ # of the instance which constitute its identity.
module FingerprintHash
# overrides BasicObject#==
def ==(other)
- __id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == jsi_fingerprint)
+ __id__ == other.__id__ || (other.is_a?(FingerprintHash) && jsi_fingerprint == other.jsi_fingerprint)
end
alias_method :eql?, :==
# overrides Kernel#hash
def hash
jsi_fingerprint.hash
end
end
- class MemoMap
- Result = AttrStruct[*%w(
- value
- inputs
- inputs_hash
- )]
+ module FingerprintHash::Immutable
+ include FingerprintHash
- class Result
+ def ==(other)
+ return true if __id__ == other.__id__
+ return false unless other.is_a?(FingerprintHash)
+ # FingerprintHash::Immutable#hash being memoized, comparing that is basically free.
+ # not done with FingerprintHash, its #hash can be expensive.
+ return false if other.is_a?(FingerprintHash::Immutable) && hash != other.hash
+ jsi_fingerprint == other.jsi_fingerprint
end
- def initialize(key_by: nil, &block)
- @key_by = key_by
- @block = block
+ alias_method :eql?, :==
- # each result has its own mutex to update its memoized value thread-safely
- @result_mutexes = {}
- # another mutex to thread-safely initialize each result mutex
- @result_mutexes_mutex = Mutex.new
-
- @results = {}
+ def hash
+ @jsi_fingerprint_hash ||= jsi_fingerprint.hash
end
- def [](**inputs)
- if @key_by
- key = @key_by.call(**inputs)
- else
- key = inputs
- end
- result_mutex = @result_mutexes_mutex.synchronize do
- @result_mutexes[key] ||= Mutex.new
- end
-
- result_mutex.synchronize do
- inputs_hash = inputs.hash
- if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
- @results[key].value
- else
- value = @block.call(**inputs)
- @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
- value
- end
- end
+ def freeze
+ hash
+ super
end
end
-
- module Memoize
- def self.extended(object)
- object.send(:jsi_initialize_memos)
- end
-
- private
-
- def jsi_initialize_memos
- @jsi_memomaps_mutex = Mutex.new
- @jsi_memomaps = {}
- end
-
- # @return [Util::MemoMap]
- def jsi_memomap(name, **options, &block)
- raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
- unless @jsi_memomaps.key?(name)
- @jsi_memomaps_mutex.synchronize do
- # note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
- # but that check is not thread safe. this check is.
- @jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
- end
- end
- @jsi_memomaps[name]
- end
-
- def jsi_memoize(name, **inputs, &block)
- jsi_memomap(name, &block)[**inputs]
- end
- end
-
- module Virtual
- class InstantiationError < StandardError
- end
-
- # this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
- def initialize
- # :nocov:
- raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
- # :nocov:
- end
-
- # virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
- def virtual_method
- # :nocov:
- raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
- # :nocov:
- end
- end
-
- public
-
- extend self
end
end