Skip to content

Commit

Permalink
Merge remote-tracking branches 'origin/mutability' and 'origin/misc' …
Browse files Browse the repository at this point in the history
…into HEAD
  • Loading branch information
notEthan committed Apr 17, 2024
2 parents 09ccd96 + a8a1b44 commit edbeeaf
Show file tree
Hide file tree
Showing 33 changed files with 224 additions and 168 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Expand Up @@ -34,6 +34,8 @@ jobs:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true

- run: "if (( RANDOM % 2 )); then bin/chkbug.rb; fi"

- run: bundle exec rake test

- name: Report to Coveralls
Expand Down
13 changes: 7 additions & 6 deletions README.md
Expand Up @@ -15,7 +15,9 @@ Note: The canonical location of this README is on [RubyDoc](http://rubydoc.info/

## Example

Words are boring, let's code. Here's a schema in yaml:
Words are boring, let's code. You can follow along from the code blocks - install the gem (`gem install jsi`), load an irb (`irb -r jsi`), and copy/paste/hack.

Here's a schema in yaml:

```yaml
$schema: "http://json-schema.org/draft-07/schema"
Expand Down Expand Up @@ -91,7 +93,7 @@ bill.phone.map(&:location)
# => ["home"]
```

We also get validations, as you'd expect given that's largely what json-schema exists to do:
We also get validations, as you'd expect given that's largely what JSON Schema exists to do:

```ruby
bill.jsi_valid?
Expand Down Expand Up @@ -190,7 +192,7 @@ bill['name']

For `#name` and `#name=`, we're overriding existing accessor methods. note the use of `super` - this invokes the accessor methods defined by JSI which these override. You could alternatively use `self['name']` and `self['name']=` in these methods, with the same effect as `super`.

Working with subschemas is just about as easy as with root schemas.
Working with subschemas to add methods is just about as easy as with root schemas.

You can subscript or use property accessors on a JSI schema module to refer to the schema modules of its subschemas, e.g.:

Expand All @@ -211,8 +213,7 @@ bill.phone.first.number_with_dashes
# => "5-5-5"
```

A recommended convention for naming subschemas is to define them in the namespace of the module of their
parent schema. The module can then be opened to add methods to the subschema's module.
A recommended convention for naming subschemas is to define them in the namespace of the module of their parent schema. The module can then be opened to add methods to the subschema's module.

```ruby
module Contact
Expand Down Expand Up @@ -282,7 +283,7 @@ class User < ActiveRecord::Base
end
```

Now `user.contact_info` will be instantiated as a `Contact` JSI instance, from the JSON type in the database, with Contact's accessors, validations, and user-defined instance methods.
Now `user.contact_info` will be instantiated as a `Contact` JSI instance, from the JSON type in the database, with Contact's accessors, validations, and application-defined instance methods.

See the gem [`arms`](https://github.com/notEthan/arms) if you wish to serialize the dumped JSON-compatible objects further as text.

Expand Down
1 change: 1 addition & 0 deletions jsi.gemspec
Expand Up @@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_dependency "addressable", '~> 2.3'
spec.add_dependency "bigdecimal"
end
2 changes: 1 addition & 1 deletion lib/jsi.rb
Expand Up @@ -91,7 +91,7 @@ def self.new_metaschema(metaschema_document,
# Instantiates the given document as a JSI Metaschema, passing all params to
# {new_metaschema}, and returns its {Schema#jsi_schema_module JSI Schema Module}.
#
# @return [Module + JSI::SchemaModule::DescribesSchemaModule + JSI::SchemaModule]
# @return [JSI::SchemaModule + JSI::SchemaModule::DescribesSchemaModule]
def self.new_metaschema_module(metaschema_document, **kw)
new_metaschema(metaschema_document, **kw).jsi_schema_module
end
Expand Down
35 changes: 22 additions & 13 deletions lib/jsi/base.rb
Expand Up @@ -50,14 +50,17 @@ def inspect
end

if schema_names.empty?
"(JSI Schema Class for 0 schemas)"
"(JSI Schema Class for 0 schemas#{jsi_class_includes.map { |n| " + #{n}" }})"
else
-"(JSI Schema Class: #{schema_names.join(' + ')})"
-"(JSI Schema Class: #{(schema_names + jsi_class_includes.map(&:name)).join(' + ')})"
end
end
end

alias_method :to_s, :inspect
def to_s
inspect
end

# A constant name of this class. This is generated from any schema module name or URI of each schema
# this class represents, or random characters.
#
Expand Down Expand Up @@ -117,7 +120,7 @@ def initialize(jsi_document,
jsi_content_to_immutable: ,
jsi_root_node: nil
)
raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)
#chkbug raise(Bug, "no #jsi_schemas") unless respond_to?(:jsi_schemas)

super()

Expand All @@ -129,11 +132,11 @@ def initialize(jsi_document,
self.jsi_schema_registry = jsi_schema_registry
@jsi_content_to_immutable = jsi_content_to_immutable
if @jsi_ptr.root?
raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
#chkbug raise(Bug, "jsi_root_node specified for root JSI") if jsi_root_node
@jsi_root_node = self
else
raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
#chkbug raise(Bug, "jsi_root_node is not JSI::Base") if !jsi_root_node.is_a?(JSI::Base)
#chkbug raise(Bug, "jsi_root_node ptr is not root") if !jsi_root_node.jsi_ptr.root?
@jsi_root_node = jsi_root_node
end

Expand Down Expand Up @@ -532,6 +535,12 @@ def described_by?(schema)
end
end

# Is this a JSI Schema?
# @return [Boolean]
def jsi_is_schema?
false
end

# yields the content of this JSI's instance. the block must result in
# 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.
Expand Down Expand Up @@ -624,18 +633,18 @@ def inspect
-"\#<#{jsi_object_group_text.join(' ')} #{jsi_instance.inspect}>"
end

alias_method :to_s, :inspect
def to_s
inspect
end

# pretty-prints a representation of this JSI to the given printer
# @return [void]
def pretty_print(q)
q.text '#<'
q.text jsi_object_group_text.join(' ')
q.group_sub {
q.nest(2) {
q.group(2) {
q.breakable ' '
q.pp jsi_instance
}
}
q.breakable ''
q.text '>'
Expand Down Expand Up @@ -665,7 +674,7 @@ def jsi_object_group_text
class_txt,
is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
*content_txt,
].compact
].compact.freeze
end

# A structure coerced to JSONifiable types from the instance content.
Expand Down Expand Up @@ -693,7 +702,7 @@ def jsi_fingerprint
jsi_resource_ancestor_uri: jsi_resource_ancestor_uri,
# different registries mean references may resolve to different resources so must not be equal
jsi_schema_registry: jsi_schema_registry,
}
}.freeze
end

private
Expand Down
27 changes: 1 addition & 26 deletions lib/jsi/base/node.rb
@@ -1,32 +1,9 @@
# frozen_string_literal: true

module JSI
module Base::Enumerable
include ::Enumerable

# an Array containing each item in this JSI.
#
# @param kw keyword arguments are passed to {Base#[]} - see its keyword params
# @return [Array]
def to_a(**kw)
# TODO remove eventually (keyword argument compatibility)
# discard when all supported ruby versions Enumerable#to_a delegate keywords to #each (3.0.1 breaks; 2.7.x warns)
# https://bugs.ruby-lang.org/issues/18289
ary = []
each(**kw) do |e|
ary << e
end
ary.freeze
end

alias_method :entries, :to_a
end

# 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
include Base::Enumerable

# instantiates and yields each property name (hash key) as a JSI described by any `propertyNames` schemas.
#
# @yield [JSI::Base]
Expand Down Expand Up @@ -87,7 +64,7 @@ def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_defau
end
end

# yields each hash key and value of this node.
# yields each Hash key (JSON object property name) and value of this node.
#
# each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
#
Expand Down Expand Up @@ -174,8 +151,6 @@ def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
# 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
include Base::Enumerable

# See {Base#jsi_array?}. Always true for ArrayNode.
def jsi_array?
true
Expand Down
6 changes: 4 additions & 2 deletions lib/jsi/jsi_coder.rb
Expand Up @@ -30,13 +30,15 @@ class JSICoder
# @param array [Boolean] whether the dumped data represent one instance of the schema,
# or an array of them. note that it may be preferable to simply use an array schema.
# @param jsi_opt [Hash] keyword arguments to pass to {Schema#new_jsi} when loading
def initialize(schema, array: false, jsi_opt: {})
# @param as_json_opt [Hash] keyword arguments to pass to `#as_json` when dumping
def initialize(schema, array: false, jsi_opt: {}, as_json_opt: {})
unless schema.respond_to?(:new_jsi)
raise(ArgumentError, "schema param does not respond to #new_jsi: #{schema.inspect}")
end
@schema = schema
@array = array
@jsi_opt = jsi_opt
@as_json_opt = as_json_opt
end

# loads the database column to JSI instances of our schema
Expand Down Expand Up @@ -86,7 +88,7 @@ def load_object(data)
# @param object [JSI::Base, Object]
# @return [Object]
def dump_object(object)
JSI::Util.as_json(object)
JSI::Util.as_json(object, **@as_json_opt)
end
end
end
10 changes: 5 additions & 5 deletions lib/jsi/metaschema_node.rb
Expand Up @@ -55,7 +55,7 @@ def initialize(
jsi_root_node: jsi_root_node,
)

@schema_implementation_modules = Util.ensure_module_set(schema_implementation_modules)
@schema_implementation_modules = schema_implementation_modules = Util.ensure_module_set(schema_implementation_modules)
@metaschema_root_ptr = metaschema_root_ptr
@root_schema_ptr = root_schema_ptr

Expand Down Expand Up @@ -155,7 +155,7 @@ def jsi_indicated_schemas
def jsi_child(token, as_jsi: )
child_node = @root_descendent_node_map[ptr: jsi_ptr[token]]

jsi_child_as_jsi(jsi_node_content_child(token), child_node.jsi_schemas, as_jsi) do
jsi_child_as_jsi(child_node.jsi_node_content, child_node.jsi_schemas, as_jsi) do
child_node
end
end
Expand Down Expand Up @@ -196,13 +196,13 @@ def jsi_object_group_text
class_n_schemas,
is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
*(jsi_node_content.respond_to?(:jsi_object_group_text) ? jsi_node_content.jsi_object_group_text : nil),
].compact
].compact.freeze
end

# see {Util::Private::FingerprintHash}
# @api private
def jsi_fingerprint
{class: self.class, jsi_document: jsi_document}.merge(our_initialize_params)
{class: self.class, jsi_document: jsi_document}.merge(our_initialize_params).freeze
end

protected
Expand All @@ -213,7 +213,7 @@ def jsi_fingerprint

def jsi_memomaps_initialize
if jsi_ptr.root?
@root_descendent_node_map = jsi_memomap(key_by: proc { |i| i[:ptr] }, &method(:jsi_root_descendent_node_compute))
@root_descendent_node_map = jsi_memomap(&method(:jsi_root_descendent_node_compute))
else
@root_descendent_node_map = @jsi_root_node.root_descendent_node_map
end
Expand Down
20 changes: 10 additions & 10 deletions lib/jsi/metaschema_node/bootstrap_schema.rb
Expand Up @@ -23,13 +23,15 @@ class MetaschemaNode::BootstrapSchema
class << self
def inspect
if self == MetaschemaNode::BootstrapSchema
name
name.freeze
else
-"#{name || MetaschemaNode::BootstrapSchema.name} (#{schema_implementation_modules.map(&:inspect).join(', ')})"
end
end

alias_method :to_s, :inspect
def to_s
inspect
end
end

# @param jsi_ptr [JSI::Ptr] pointer to the schema in the document
Expand Down Expand Up @@ -87,18 +89,18 @@ def inspect
-"\#<#{jsi_object_group_text.join(' ')} #{schema_content.inspect}>"
end

alias_method :to_s, :inspect
def to_s
inspect
end

# pretty-prints a representation of self to the given printer
# @return [void]
def pretty_print(q)
q.text '#<'
q.text jsi_object_group_text.join(' ')
q.group_sub {
q.nest(2) {
q.group(2) {
q.breakable ' '
q.pp schema_content
}
}
q.breakable ''
q.text '>'
Expand All @@ -111,7 +113,7 @@ def jsi_object_group_text
self.class.name || MetaschemaNode::BootstrapSchema.name,
-"(#{schema_implementation_modules.map(&:inspect).join(', ')})",
jsi_ptr.uri,
]
].freeze
end

# see {Util::Private::FingerprintHash}
Expand All @@ -121,9 +123,7 @@ def jsi_fingerprint
class: self.class,
jsi_ptr: @jsi_ptr,
jsi_document: @jsi_document,
jsi_schema_base_uri: jsi_schema_base_uri,
schema_implementation_modules: schema_implementation_modules,
}
}.freeze
end

private
Expand Down
11 changes: 8 additions & 3 deletions lib/jsi/ptr.rb
Expand Up @@ -25,6 +25,8 @@ class ResolutionError < Error
def self.ary_ptr(ary_ptr)
if ary_ptr.is_a?(Ptr)
ary_ptr
elsif ary_ptr == Util::EMPTY_ARY
EMPTY
else
new(ary_ptr)
end
Expand Down Expand Up @@ -176,6 +178,7 @@ def contains?(other_ptr)
# @return [JSI::Ptr]
# @raise [JSI::Ptr::Error] if the given ancestor_ptr is not an ancestor of this pointer
def relative_to(ancestor_ptr)
return self if ancestor_ptr.empty?
unless ancestor_ptr.contains?(self)
raise(Error, "ancestor_ptr #{ancestor_ptr.inspect} is not ancestor of #{inspect}")
end
Expand Down Expand Up @@ -237,7 +240,7 @@ def modified_document_copy(document, &block)
Util.modified_copy(document, &block)
else
car = tokens[0]
cdr = Ptr.new(tokens[1..-1].freeze)
cdr = tokens.size == 1 ? EMPTY : Ptr.new(tokens[1..-1].freeze)
token, document_child = node_subscript_token_child(document, car)
modified_document_child = cdr.modified_document_copy(document_child, &block)
if modified_document_child.object_id == document_child.object_id
Expand All @@ -259,12 +262,14 @@ def inspect
-"#{self.class.name}[#{tokens.map(&:inspect).join(", ")}]"
end

alias_method :to_s, :inspect
def to_s
inspect
end

# see {Util::Private::FingerprintHash}
# @api private
def jsi_fingerprint
{class: Ptr, tokens: tokens}
{class: Ptr, tokens: tokens}.freeze
end
include Util::FingerprintHash::Immutable

Expand Down

0 comments on commit edbeeaf

Please sign in to comment.