Skip to content

Commit

Permalink
Merge remote-tracking branches 'origin/base_initialize', 'origin/base…
Browse files Browse the repository at this point in the history
…_child', 'origin/jsi_complex_children', 'origin/metaschema_instance_modules', 'origin/base_class_name_by_schemas_ancestors', 'origin/simple_wrap', 'origin/msn_root', 'origin/splat', 'origin/child_applicator_schemas', 'origin/default_metaschema', 'origin/deprecated', 'origin/misc' and 'origin/doc' into HEAD

base_initialize                  #183
base_child                        #225
jsi_complex_children               #227
metaschema_instance_modules         #228
base_class_name_by_schemas_ancestors #235
simple_wrap                         #234
msn_root                           #238
splat                             #239
child_applicator_schemas         #240
default_metaschema              #241
deprecated                     #226
misc                          #231
doc                          #237
  • Loading branch information
notEthan committed Mar 21, 2022
13 parents 4198b15 + 1e60a1c + 6557db5 + d2b65f9 + cb77852 + b69267a + b640606 + 9c7c4e7 + e0d2ed5 + 5bce40a + ef78709 + c4e3271 + 5480d14 commit 63929cd
Show file tree
Hide file tree
Showing 34 changed files with 649 additions and 374 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
2 changes: 1 addition & 1 deletion Gemfile
Expand Up @@ -3,7 +3,7 @@ source "https://rubygems.org"
gemspec

gem 'irb'
gem 'byebug'
platform(:mri) { gem 'byebug' }
gem 'rake'
gem 'gig'
gem 'minitest'
Expand Down
17 changes: 11 additions & 6 deletions README.md
@@ -1,6 +1,6 @@
# JSI: JSON Schema Instantiation

[![Build Status](https://travis-ci.org/notEthan/jsi.svg?branch=master)](https://travis-ci.org/notEthan/jsi)
![Test CI Status](https://github.com/notEthan/jsi/actions/workflows/test.yml/badge.svg?branch=stable)
[![Coverage Status](https://coveralls.io/repos/github/notEthan/jsi/badge.svg)](https://coveralls.io/github/notEthan/jsi)

JSI offers an Object-Oriented representation for JSON data using JSON Schemas. Given your JSON Schemas, JSI constructs Ruby modules and classes which are used to instantiate your JSON data. These modules let you use JSON with all the niceties of OOP such as property accessors and application-defined instance methods.
Expand Down Expand Up @@ -46,6 +46,8 @@ We name the module that JSI will use when instantiating a contact. Named modules
Contact = contact_schema.jsi_schema_module
```

Note: it is more concise to instantiate the schema module with the shortcut {JSI.new_schema_module}, i.e. `Contact = JSI.new_schema_module(...)`. This example includes the intermediate step to help show all that is happening.

To instantiate the schema, we need some JSON data (expressed here as YAML)

```yaml
Expand Down Expand Up @@ -173,17 +175,20 @@ module Contact
end
end

bill.phone_numbers
# => ["555"]

bill.name
# => "bill esq."
bill.name = 'rob esq.'
# => "rob esq."
bill['name']
# => "rob"
bill.phone_numbers
# => ["555"]
```

Note the use of `super` - you can call to accessors defined by JSI and make your accessors act as wrappers. You can alternatively use `[]` and `[]=` with the same effect.
`#phone_numbers` is a new method returning each number in the `phone` array - pretty straightforward.

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.

Expand Down Expand Up @@ -269,15 +274,15 @@ Let's say you're sticking to JSON types in the database - you have to do so if y

But if your database contains JSON, then your deserialized objects in ruby are likewise Hash / Array / basic types. You have to use subscripts instead of accessors, and you don't have any way to add methods to your data types.

JSI gives you the best of both with JSICoder. This coder dumps objects which are simple JSON types, and loads instances of a specified JSI schema. Here's an example, supposing a `users` table with a JSON column `contact_info`:
JSI gives you the best of both with {JSI::JSICoder}. This coder dumps objects which are simple JSON types, and loads instances of a specified JSON Schema. Here's an example, supposing a `users` table with a JSON column `contact_info` to be instantiated using the `Contact` schema module defined in the Example section above:

```ruby
class User < ActiveRecord::Base
serialize :contact_info, JSI::JSICoder.new(Contact)
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 user-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: 0 additions & 1 deletion Rakefile.rb
Expand Up @@ -17,7 +17,6 @@
.gitignore
.gitmodules
Gemfile
jsi.gemspec
Rakefile.rb
test/**/*
\\{resources\\}/icons/**/*
Expand Down
1 change: 1 addition & 0 deletions jsi.gemspec
Expand Up @@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
'README.md',
'readme.rb',
'.yardopts',
'jsi.gemspec',
*Dir['lib/**/*'],
*Dir['\\{resources\\}/schemas/**/*'],
].reject { |f| File.lstat(f).ftype == 'directory' }
Expand Down
12 changes: 0 additions & 12 deletions lib/jsi.rb
Expand Up @@ -30,13 +30,6 @@ class Bug < NotImplementedError
SCHEMAS_PATH = RESOURCES_PATH.join('schemas')

autoload :Ptr, 'jsi/ptr'

# @private
# @deprecated
module JSON
Pointer = Ptr
end

autoload :PathedNode, 'jsi/pathed_node'
autoload :Typelike, 'jsi/typelike_modules'
autoload :Hashlike, 'jsi/typelike_modules'
Expand Down Expand Up @@ -77,11 +70,6 @@ def self.new_schema_module(schema_object, **kw)
JSI::Schema.new_schema(schema_object, **kw).jsi_schema_module
end

# @private @deprecated
def self.class_for_schemas(schemas)
SchemaClasses.class_for_schemas(schemas.map { |schema| JSI.new_schema(schema) })
end

# `JSI.schema_registry` is the {JSI::SchemaRegistry} in which schemas are registered.
#
# @return [JSI::SchemaRegistry]
Expand Down
82 changes: 41 additions & 41 deletions lib/jsi/base.rb
Expand Up @@ -22,16 +22,11 @@ class Base
# the class than to conditionally extend the instance.
include Enumerable

# an exception raised when #[] is invoked on an instance which is not an array or hash
# an exception raised when {Base#[]} is invoked on an instance which is not an array or hash
class CannotSubscriptError < StandardError
end

class << self
# @private @deprecated
def new_jsi(instance, **kw, &b)
new(instance, **kw, &b)
end

# @private
# is the constant JSI::SchemaClasses::<self.schema_classes_const_name> defined?
# (if so, we will prefer to use something more human-readable than that ugly mess.)
Expand Down Expand Up @@ -84,8 +79,9 @@ def inspect
def schema_classes_const_name
if respond_to?(:jsi_class_schemas)
schema_names = jsi_class_schemas.map do |schema|
if schema.jsi_schema_module.name
schema.jsi_schema_module.name
named_ancestor_schema, tokens = schema.jsi_schema_module.send(:named_ancestor_schema_tokens)
if named_ancestor_schema
[named_ancestor_schema.jsi_schema_module.name, *tokens].join('_')
elsif schema.schema_uri
schema.schema_uri.to_s
else
Expand Down Expand Up @@ -157,10 +153,10 @@ def initialize(jsi_document,
self.jsi_schema_base_uri = jsi_schema_base_uri
self.jsi_schema_resource_ancestors = jsi_schema_resource_ancestors

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

Expand Down Expand Up @@ -305,6 +301,15 @@ def jsi_parent_node
jsi_parent_nodes.first
end

# the child node at the given pointer
#
# @param ptr [JSI::Ptr, #to_ary]
# @return [JSI::Base]
def jsi_child_node(ptr)
child = Ptr.ary_ptr(ptr).evaluate(self, as_jsi: true)
child
end

# subscripts to return a child value identified by the given token.
#
# @param token [String, Integer, Object] an array index or hash key (JSON object property name)
Expand All @@ -313,14 +318,14 @@ def jsi_parent_node
#
# - :auto (default): by default a JSI will be returned when either:
#
# - the result is a complex value (responds to #to_ary or #to_hash) and is described by some schemas
# - the result is a complex value (responds to #to_ary or #to_hash)
# - the result is a schema (including true/false schemas)
#
# a plain value is returned when no schemas are known to describe the instance, or when the value is a
# simple type (anything unresponsive to #to_ary / #to_hash).
#
# - true: the result value will always be returned as a JSI. the #jsi_schemas of the result may be empty
# if no schemas describe the instance.
# - true: the result value will always be returned as a JSI. the {#jsi_schemas} of the result may be
# empty if no schemas describe the instance.
# - false: the result value will always be the plain instance.
#
# note that nil is returned (regardless of as_jsi) when there is no value to return because the token
Expand Down Expand Up @@ -393,7 +398,7 @@ def [](token, as_jsi: :auto, use_default: true)
# @param value [JSI::Base, Object] the value to be assigned
def []=(token, value)
unless respond_to?(:to_hash) || respond_to?(:to_ary)
raise(NoMethodError, "cannot assign subscript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
raise(CannotSubscriptError, "cannot assign subscript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
end
if value.is_a?(Base)
self[token] = value.jsi_instance
Expand All @@ -405,7 +410,7 @@ def []=(token, value)
# the set of JSI schema modules corresponding to the schemas that describe this JSI
# @return [Set<Module>]
def jsi_schema_modules
jsi_schemas.map(&:jsi_schema_module).to_set.freeze
Util.ensure_module_set(jsi_schemas.map(&:jsi_schema_module))
end

# yields the content of this JSI's instance. the block must result in
Expand All @@ -428,7 +433,7 @@ def jsi_modified_copy(&block)
modified_jsi_root_node = @jsi_root_node.jsi_modified_copy do |root|
@jsi_ptr.modified_document_copy(root, &block)
end
@jsi_ptr.evaluate(modified_jsi_root_node, as_jsi: true)
modified_jsi_root_node.jsi_child_node(@jsi_ptr)
end
end

Expand All @@ -445,21 +450,6 @@ def jsi_valid?
jsi_schemas.instance_valid?(self)
end

# @private
def fully_validate(errors_as_objects: false)
raise(NotImplementedError, "Base#fully_validate removed: see new validation interface Base#jsi_validate")
end

# @private
def validate
raise(NotImplementedError, "Base#validate renamed: see Base#jsi_valid?")
end

# @private
def validate!
raise(NotImplementedError, "Base#validate! removed")
end

def dup
jsi_modified_copy(&:dup)
end
Expand All @@ -485,6 +475,24 @@ def pretty_print(q)
q.text '>'
end

# an Array containing each item in this JSI, if this JSI's instance is enumerable. the same
# as `Enumerable#to_a`.
#
# @param kw keyword arguments are passed to {#[]} - see its keyword params
# @return [Array]
def to_a(**kw)
# TODO remove eventually (keyword argument compatibility)
# discard when all supported ruby versions 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
end

alias_method :entries, :to_a

# @private
# @return [Array<String>]
def jsi_object_group_text
Expand Down Expand Up @@ -549,15 +557,7 @@ def jsi_fingerprint

def jsi_subinstance_schemas_memos
jsi_memomap(:subinstance_schemas, key_by: -> (i) { i[:token] }) do |token: , instance: , subinstance: |
SchemaSet.build do |schemas|
jsi_schemas.each do |schema|
schema.each_child_applicator_schema(token, instance) do |child_app_schema|
child_app_schema.each_inplace_applicator_schema(subinstance) do |child_inpl_app_schema|
schemas << child_inpl_app_schema
end
end
end
end
jsi_schemas.child_applicator_schemas(token, instance).inplace_applicator_schemas(subinstance)
end
end

Expand All @@ -576,7 +576,7 @@ def jsi_subinstance_as_jsi(value, subinstance_schemas, as_jsi)
value_as_jsi = if [true, false].include?(as_jsi)
as_jsi
elsif as_jsi == :auto
complex_value = subinstance_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
complex_value = value.respond_to?(:to_hash) || value.respond_to?(:to_ary)
schema_value = subinstance_schemas.any? { |subinstance_schema| subinstance_schema.describes_schema? }
complex_value || schema_value
else
Expand Down
1 change: 0 additions & 1 deletion lib/jsi/metaschema.rb
Expand Up @@ -2,6 +2,5 @@

module JSI
module Metaschema
include JSI::Schema::DescribesSchema
end
end

0 comments on commit 63929cd

Please sign in to comment.