Module: JSI::Util

Extended by:
Util
Includes:
Private
Included in:
Util
Defined in:
lib/jsi/util.rb

Overview

JSI::Util contains public utilities

Defined Under Namespace

Modules: Arraylike, Hashlike, Private

Constant Summary

Constants included from Private

Private::CLASSES_ALWAYS_FROZEN, Private::EMPTY_ARY, Private::EMPTY_SET, Private::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS, Private::RUBY_REJECT_NAME_CODEPOINTS, Private::RUBY_REJECT_NAME_RE, Private::USE_TO_JSON_METHOD

Instance Method Summary collapse

Methods included from Private

#const_name_from_parts, #ok_ruby_method_name?, #require_jmespath, #uri, #ycomb

Instance Method Details

#as_json(object, options = {}) ⇒ Array, ...

A structure like the given object, recursively coerced to JSON-compatible types.

  • Structures of Hash, Array, and basic types of String/number/boolean/nil are returned as-is.
  • If the object responds to #as_json, that method is used, passing any given options.
  • If the object supports implicit conversion with #to_hash, #to_ary, #to_str, or #to_int, that is used.
  • Set becomes Array; Symbol becomes String.
  • Types with no known coersion to JSON-compatible raise TypeError.

Parameters:

  • object (Object)

Returns:

  • (Array, Hash, String, Integer, Float, Boolean, NilClass)

    a JSON-compatible structure like the given object

Raises:

  • (TypeError)

    If the object cannot be coerced to a JSON-compatible structure



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/jsi/util.rb', line 45

def as_json(object, options = {})
  type_err = proc { raise(TypeError, "cannot express object as json: #{object.pretty_inspect.chomp}") }
  if object.respond_to?(:as_json)
    options.empty? ? object.as_json : object.as_json(**options) # TODO remove eventually (keyword argument compatibility)
  elsif object.is_a?(Addressable::URI)
    object.to_s
  elsif object.respond_to?(:to_hash) && (object_to_hash = object.to_hash).is_a?(Hash)
    result = {}
    object_to_hash.each_pair do |k, v|
      ks = k.is_a?(String) ? k :
        k.is_a?(Symbol) ? k.to_s :
        k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
        raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
      result[ks] = as_json(v, **options)
    end
    result
  elsif object.respond_to?(:to_ary) && (object_to_ary = object.to_ary).is_a?(Array)
    object_to_ary.map { |e| as_json(e, **options) }
  elsif [String, Integer, TrueClass, FalseClass, NilClass].any? { |c| object.is_a?(c) }
    object
  elsif object.is_a?(Float)
    type_err.call unless object.finite?
    object
  elsif object.is_a?(Symbol)
    object.to_s
  elsif object.is_a?(Set)
    as_json(object.to_a, **options)
  elsif object.respond_to?(:to_str) && (object_to_str = object.to_str).is_a?(String)
    object_to_str
  elsif object.respond_to?(:to_int) && (object_to_int = object.to_int).is_a?(Integer)
    object_to_int
  else
    type_err.call
  end
end

#deep_stringify_symbol_keys(object) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/jsi/util.rb', line 122

def deep_stringify_symbol_keys(object)
  if object.respond_to?(:to_hash) && !object.is_a?(Addressable::URI)
    JSI::Util.modified_copy(object) do |hash|
      out = {}
      (hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
        out[k.is_a?(Symbol) ? k.to_s.freeze : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
      end
      out
    end
  elsif object.respond_to?(:to_ary)
    JSI::Util.modified_copy(object) do |ary|
      (ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
        deep_stringify_symbol_keys(e)
      end
    end
  else
    object
  end
end

#deep_to_frozen(object, not_implemented: nil) ⇒ Object

returns an object which is equal to the param object, and is recursively frozen. the given object is not modified.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/jsi/util.rb', line 144

def deep_to_frozen(object, not_implemented: nil)
  dtf = proc { |o| deep_to_frozen(o, not_implemented: not_implemented) }
  if object.instance_of?(Hash)
    out = {}
    identical = object.frozen?
    object.each do |k, v|
      fk = dtf[k]
      fv = dtf[v]
      identical &&= fk.__id__ == k.__id__
      identical &&= fv.__id__ == v.__id__
      out[fk] = fv
    end
    if !object.default.nil?
      out.default = dtf[object.default]
      identical &&= out.default.__id__ == object.default.__id__
    end
    if object.default_proc
      raise(ArgumentError, "cannot make immutable copy of a Hash with default_proc")
    end
    if identical
      object
    else
      out.freeze
    end
  elsif object.instance_of?(Array)
    identical = object.frozen?
    out = Array.new(object.size)
    object.each_with_index do |e, i|
      fe = dtf[e]
      identical &&= fe.__id__ == e.__id__
      out[i] = fe
    end
    if identical
      object
    else
      out.freeze
    end
  elsif object.instance_of?(String)
    if object.frozen?
      object
    else
      object.dup.freeze
    end
  elsif CLASSES_ALWAYS_FROZEN.any? { |c| object.is_a?(c) } # note: `is_a?`, not `instance_of?`, here because instance_of?(Integer) is false until Fixnum/Bignum is gone. this is fine here; there is no concern of subclasses of CLASSES_ALWAYS_FROZEN duping/freezing differently (as with e.g. ActiveSupport::HashWithIndifferentAccess)
    object
  else
    if not_implemented
      not_implemented.call(object)
    else
      raise(NotImplementedError, [
        "deep_to_frozen not implemented for class: #{object.class}",
        "object: #{object.pretty_inspect.chomp}",
      ].join("\n"))
    end
  end
end

#ensure_module_set(modules) ⇒ Set

ensures the given param becomes a frozen Set of Modules. returns the param if it is already that, otherwise initializes and freezes such a Set.

Parameters:

  • modules (Set, Enumerable)

    the object to ensure becomes a frozen Set of Modules

Returns:

  • (Set)

    frozen Set containing the given modules

Raises:

  • (ArgumentError)

    when the modules param is not an Enumerable

  • (Schema::NotASchemaError)

    when the modules param contains objects which are not Schemas



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/jsi/util.rb', line 208

def ensure_module_set(modules)
  if modules.is_a?(Set) && modules.frozen?
    set = modules
  elsif modules.is_a?(Enumerable)
    set = Set.new(modules).freeze
  else
    raise(TypeError, "not given an Enumerable of Modules")
  end
  not_modules = set.reject { |s| s.is_a?(Module) }
  if !not_modules.empty?
    raise(TypeError, [
      "ensure_module_set given non-Module objects:",
      *not_modules.map { |ns| ns.pretty_inspect.chomp },
    ].join("\n"))
  end

  set
end

#modified_copy(object) {|Object| ... } ⇒ object.class

yields the content of the given param object. for objects which have a #jsi_modified_copy method of their own (JSI::Base, JSI::MetaschemaNode) that method is invoked with the given block. otherwise the given object itself is yielded.

the given block must result in a modified copy of its block parameter (not destructively modifying the yielded content).

Yields:

  • (Object)

    the content of the given object. the block should result in a (nondestructively) modified copy of this.

Returns:

  • (object.class)

    modified copy of the given object



25
26
27
28
29
30
31
# File 'lib/jsi/util.rb', line 25

def modified_copy(object, &block)
  if object.respond_to?(:jsi_modified_copy)
    object.jsi_modified_copy(&block)
  else
    yield(object)
  end
end

#stringify_symbol_keys(hashlike) ⇒ same class as the param `hash`, or Hash if the former cannot be done

a hash copied from the given hashlike, in which any symbol keys are converted to strings. behavior on collisions is undefined (but in the future could take a block like ActiveSupport::HashWithIndifferentAccess#update)

at the moment it is undefined whether the returned hash is the same instance as the hash param. if hash is already a hash which contains no symbol keys, this method MAY return that same instance. use #dup on the return if you need to ensure it is not the same instance as the argument instance.

Parameters:

  • hashlike (#to_hash)

    the hash from which to convert symbol keys to strings

Returns:

  • (same class as the param `hash`, or Hash if the former cannot be done)

    a hash(-like) instance containing no symbol keys



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/jsi/util.rb', line 109

def stringify_symbol_keys(hashlike)
  unless hashlike.respond_to?(:to_hash)
    raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
  end
  JSI::Util.modified_copy(hashlike) do |hash|
    out = {}
    hash.each do |k, v|
      out[k.is_a?(Symbol) ? k.to_s : k] = v
    end
    out
  end
end

#to_json(object, options = {}) ⇒ String

A JSON encoded string of the given object.

  • If the object has a #to_json method that isn't defined by the stdlib json gem, that method is used, passing any given options.
  • Otherwise, JSON is generated using #as_json to coerce to compatible types.

Returns:

  • (String)


87
88
89
90
91
92
93
# File 'lib/jsi/util.rb', line 87

def to_json(object, options = {})
  if USE_TO_JSON_METHOD[object.class]
    options.empty? ? object.to_json : object.to_json(**options) # TODO remove eventually (keyword argument compatibility)
  else
    JSON.generate(as_json(object, **options))
  end
end