Skip to content

Commit

Permalink
Merge remote-tracking branches 'origin/descendent', 'origin/schema_im…
Browse files Browse the repository at this point in the history
…plementation_modules', 'origin/node', 'origin/schema.keyword', 'origin/attr_struct', 'origin/test_assert_schemas', 'origin/test', 'origin/misc', 'origin/deprecated' and 'origin/ruby-version' into HEAD

descendent                   #253
schema_implementation_modules #245
node                         #250
schema.keyword              #252
attr_struct                #246
test_assert_schemas       #233
test                     #242
misc                    #243
deprecated             #244
ruby-version          #247
  • Loading branch information
notEthan committed Jun 26, 2022
10 parents 035cef5 + c997a39 + bf5f8b8 + 004568a + 6b86e19 + 09e6857 + c7980b5 + 2504edb + 930d9fc + 9c79374 commit 4e7a154
Show file tree
Hide file tree
Showing 61 changed files with 649 additions and 739 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Expand Up @@ -8,8 +8,13 @@ jobs:
fail-fast: false
matrix:
ruby-version:
- jruby
- truffleruby
- truffleruby+graalvm
- '2.3'
- '2.7'
- '3.0'
- head
runs-on:
- ubuntu-latest

Expand Down
10 changes: 7 additions & 3 deletions Gemfile
Expand Up @@ -11,6 +11,10 @@ gem 'minitest-around'
gem 'minitest-reporters'
gem 'simplecov'
gem 'simplecov-lcov'
gem 'scorpio', '~> 0.6'
gem 'spreedly_openapi', github: 'notEthan/spreedly_openapi', tag: 'v0.2.0'
gem 'activesupport'

# jsi does not depend on these, but we wish to test integration with them
group(:extdep) do
gem 'scorpio', '~> 0.6'
gem 'spreedly_openapi', github: 'notEthan/spreedly_openapi', tag: 'v0.2.0'
gem 'activesupport'
end
62 changes: 56 additions & 6 deletions Rakefile.rb
@@ -1,14 +1,64 @@
# frozen_string_literal: true

require "rake/testtask"
namespace 'test' do
require "rake/testtask"
require "ansi/code"

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
class JSITestTask < Rake::TestTask
def initialize(name: , title: , description: nil, pattern: nil, test_files: nil, env: {})
@title = title
@env = env.merge('JSI_TEST_TASK' => "test:#{name}")
super(name) do |t|
t.description = description
t.pattern = pattern
t.test_files = test_files
t.verbose = true
t.warning = true
end
end

# hack in some things
# - print task title
# - support @env hash
# method #ruby isn't the right entry point for these, but there isn't a better one
# overrides #ruby defined on FileUtils in rake/file_utils.rb
# that #ruby handles more params but Rake::TestTask only calls it with one ruby command args string + block
def ruby(cmd_args, &block)
puts
puts "#{ANSI::Code.magenta('𐡷')} #{ANSI::Code.cyan(@title.upcase)} #{ANSI::Code.magenta('𐡸')}"
puts

sh(@env, "#{RUBY} #{cmd_args}", &block)
end
end

JSITestTask.new(
name: 'unit',
title: 'unit tests',
description: 'run JSI unit tests',
pattern: "test/*_test.rb",
)

# tests which rely on libraries jsi does not itself depend on are run separately.
# if code is added to JSI which inadvertantly relies on these, and the tests require that dependency,
# then tests might pass when applications without that dependency would fail.
# the JSI_TEST_EXTDEP variable causes the :extdep bundler group in the Gemfile to be set up.
JSITestTask.new(
name: 'extdep',
title: 'external dependencies',
description: 'run tests which rely on libraries JSI does not itself depend on',
pattern: "test/extdep/*_test.rb",
env: {'JSI_TEST_EXTDEP' => 'y'},
)
end

task :default => :test
desc 'run all tests'
task 'test' => [
'test:unit',
'test:extdep'
]

task 'default' => 'test:unit'

require 'gig'

Expand Down
1 change: 0 additions & 1 deletion lib/jsi.rb
Expand Up @@ -30,7 +30,6 @@ class Bug < NotImplementedError
SCHEMAS_PATH = RESOURCES_PATH.join('schemas')

autoload :Ptr, 'jsi/ptr'
autoload :PathedNode, 'jsi/pathed_node'
autoload :Typelike, 'jsi/typelike_modules'
autoload :Hashlike, 'jsi/typelike_modules'
autoload :Arraylike, 'jsi/typelike_modules'
Expand Down
56 changes: 25 additions & 31 deletions lib/jsi/base.rb
Expand Up @@ -14,7 +14,9 @@ module JSI
#
# the JSI::Base class itself is not intended to be instantiated.
class Base
include PathedNode
autoload :ArrayNode, 'jsi/base/node'
autoload :HashNode, 'jsi/base/node'

include Schema::SchemaAncestorNode
include Util::Memoize

Expand Down Expand Up @@ -134,9 +136,7 @@ def initialize(jsi_document,
jsi_schema_base_uri: nil,
jsi_schema_resource_ancestors: []
)
unless respond_to?(:jsi_schemas)
raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #jsi_schemas. it is recommended to instantiate JSIs from a schema using JSI::Schema#new_jsi.")
end
raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)

jsi_initialize_memos

Expand All @@ -154,10 +154,10 @@ def initialize(jsi_document,
self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors

if jsi_instance.respond_to?(:to_hash)
extend PathedHashNode
extend HashNode
end
if jsi_instance.respond_to?(:to_ary)
extend PathedArrayNode
extend ArrayNode
end

if jsi_instance.is_a?(JSI::Base)
Expand All @@ -182,10 +182,16 @@ def initialize(jsi_document,
# @return [JSI::Base]
attr_reader :jsi_root_node

# the content of this node in our {#jsi_document} at our {#jsi_ptr}. the same as {#jsi_instance}.
def jsi_node_content
content = jsi_ptr.evaluate(jsi_document)
content
end

# the JSON schema instance this JSI represents - the underlying JSON data used to instantiate this JSI
alias_method :jsi_instance, :jsi_node_content

# each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base #each
# each is overridden by Base::HashNode or Base::ArrayNode when appropriate. the base #each
# is not actually implemented, along with all the methods of Enumerable.
def each(*_)
raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
Expand Down Expand Up @@ -376,7 +382,7 @@ def [](token, as_jsi: :auto, use_default: true)
if use_default
defaults = Set.new
subinstance_schemas.each do |subinstance_schema|
if subinstance_schema.respond_to?(:to_hash) && subinstance_schema.key?('default')
if subinstance_schema.keyword?('default')
defaults << subinstance_schema['default']
end
end
Expand Down Expand Up @@ -420,8 +426,8 @@ def jsi_schema_modules
end

# yields the content of this JSI's instance. the block must result in
# a modified copy of the yielded instance (not destructively modifying it)
# which will be used to instantiate a new JSI with the modified content.
# a modified copy of the yielded instance (not modified in place, which would alter this JSI
# as well) which will be used to instantiate and return a new JSI with the modified content.
#
# the result may have different schemas which describe it than this JSI's schemas,
# if conditional applicator schemas apply differently to the modified instance.
Expand Down Expand Up @@ -502,27 +508,14 @@ def to_a(**kw)
# @private
# @return [Array<String>]
def jsi_object_group_text
class_name = self.class.name unless self.class.in_schema_classes
class_txt = begin
if class_name
# ignore ID
schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact
if schema_module_names.empty?
class_name
else
"#{class_name} (#{schema_module_names.join(', ')})"
end
else
schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
if schema_names.empty?
"JSI"
else
"JSI (#{schema_names.join(', ')})"
end
end
schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name_from_ancestor || schema.schema_uri }.compact
if schema_names.empty?
class_txt = "JSI"
else
class_txt = "JSI (#{schema_names.join(', ')})"
end

if (is_a?(PathedArrayNode) || is_a?(PathedHashNode)) && ![Array, Hash].include?(jsi_node_content.class)
if (is_a?(ArrayNode) || is_a?(HashNode)) && ![Array, Hash].include?(jsi_node_content.class)
if jsi_node_content.respond_to?(:jsi_object_group_text)
content_txt = jsi_node_content.jsi_object_group_text
else
Expand Down Expand Up @@ -569,7 +562,8 @@ def jsi_subinstance_schemas_memos

def jsi_subinstance_memos
jsi_memomap(:subinstance, key_by: -> (i) { i[:token] }) do |token: , subinstance_schemas: |
JSI::SchemaClasses.class_for_schemas(subinstance_schemas).new(@jsi_document,
jsi_class = JSI::SchemaClasses.class_for_schemas(subinstance_schemas)
jsi_class.new(@jsi_document,
jsi_ptr: @jsi_ptr[token],
jsi_root_node: @jsi_root_node,
jsi_schema_base_uri: jsi_resource_ancestor_uri,
Expand All @@ -583,7 +577,7 @@ def jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi)
as_jsi
elsif as_jsi == :auto
complex_value = value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
schema_value = subinstance_schemas.any? { |subinstance_schema| subinstance_schema.describes_schema? }
schema_value = subinstance_schemas.any?(&:describes_schema?)
complex_value || schema_value
else
raise(ArgumentError, "as_jsi must be one of: :auto, true, false")
Expand Down
43 changes: 15 additions & 28 deletions lib/jsi/pathed_node.rb → lib/jsi/base/node.rb
@@ -1,30 +1,16 @@
# frozen_string_literal: true

module JSI
# this module represents a node in a document.
#
# including class MUST define
#
# - `#jsi_document` [Object] the document
# - `#jsi_ptr` [JSI::Ptr] a pointer to the node in the document
module PathedNode
# the content of this node
def jsi_node_content
content = jsi_ptr.evaluate(jsi_document)
content
end
end

# module extending a {JSI::PathedNode} object when its jsi_node_content is Hash-like (responds to #to_hash)
module PathedHashNode
# module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
# is a Hash (or responds to `#to_hash`)
module Base::HashNode
# yields each hash key and value of this node.
#
# each yielded key is the same as a key of the node content hash,
# and each yielded value is the result of self[key] (see #[]).
# each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
#
# returns an Enumerator if no block is given.
#
# @param kw keyword arguments are passed to `#[]`
# @param kw keyword arguments are passed to {Base#[]}
# @yield [Object, Object] each key and value of this hash node
# @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
def each(**kw, &block)
Expand All @@ -37,9 +23,8 @@ def each(**kw, &block)
self
end

# a hash in which each key is a key of the jsi_node_content hash and each value is the
# result of `self[key]`
# @param kw keyword arguments are passed to `#[]`
# a hash in which each key is a key of the instance hash and each value is the result of {Base#[]}
# @param kw keyword arguments are passed to {Base#[]}
# @return [Hash]
def to_hash(**kw)
{}.tap { |h| jsi_node_content_hash_pubsend(:each_key) { |k| h[k] = self[k, **kw] } }
Expand Down Expand Up @@ -90,14 +75,16 @@ def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
end
end

module PathedArrayNode
# module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
# is an Array (or responds to `#to_ary`)
module Base::ArrayNode
# yields each array element of this node.
#
# each yielded element is the result of self[index] for each index of our array (see #[]).
# each yielded element is the result of {Base#[]} for each index of the instance array.
#
# returns an Enumerator if no block is given.
#
# @param kw keyword arguments are passed to `#[]`
# @param kw keyword arguments are passed to {Base#[]}
# @yield [Object] each element of this array node
# @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
def each(**kw, &block)
Expand All @@ -106,9 +93,9 @@ def each(**kw, &block)
self
end

# an array, the same size as the jsi_node_content, in which the element at each index is the
# result of `self[index]`
# @param kw keyword arguments are passed to `#[]`
# an array, the same size as the instance array, in which the element at each index is the
# result of {Base#[]}.
# @param kw keyword arguments are passed to {Base#[]}
# @return [Array]
def to_ary(**kw)
to_a(**kw)
Expand Down

0 comments on commit 4e7a154

Please sign in to comment.