Skip to content

Commit

Permalink
Introspection and Schema defaults (#6)
Browse files Browse the repository at this point in the history
* introspection and schema defaults

* update readme
  • Loading branch information
matt-taylor authored Mar 12, 2022
1 parent 03c6750 commit c0ad8b5
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
json_schematize (0.3.0)
json_schematize (0.4.0)

GEM
remote: https://rubygems.org/
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ required -- Default is true. When not set, each instance class can optionally de
converter -- Proc return is set to the field value. No furter validation is done. Given (value) as a parameter
array_of_types -- Detailed example above. Set this value to true when the dig param is to an array and you want all values in array to be parsed the given type
```
### Schema defaults
Defaults can be added for all fields for any of the available options. This can be useful for returned API calls when the body is parsed as a Hash with String keys.
```ruby
class SchemaWithDefaults < JsonSchematize::Generator
schema_default option: :dig_type, value: :string

add_field name: :internals, type: InternalBody, array_of_types: true
add_field name: :id, type: Integer
add_field name: :status, type: Symbol
end
```
### Custom Classes
```ruby
Expand Down
4 changes: 2 additions & 2 deletions lib/json_schematize/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

class JsonSchematize::Field

attr_reader :name, :types, :dig, :symbol, :validator, :acceptable_types, :required, :converter, :array_of_types
attr_reader :name, :types, :dig, :dig_type, :symbol, :validator, :acceptable_types, :required, :converter, :array_of_types

EXPECTED_DIG_TYPE = [DIG_SYMBOL = :symbol, DEFAULT_DIG = DIG_NONE =:none, DIG_STRING = :string]

Expand All @@ -23,7 +23,7 @@ def initialize(name:, types:, dig:, dig_type:, validator:, type:, required:, con
end

def setup!
# validations must be done beofre transformations
# validations must be done before transformations
valiadtions!
transformations!
end
Expand Down
58 changes: 39 additions & 19 deletions lib/json_schematize/generator.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
# frozen_string_literal: true

require "json_schematize/field"

# SchemafiedJSON
# JSONSchematize
require "json_schematize/introspect"

class JsonSchematize::Generator
EMPTY_VALIDATOR = ->(_transformed_value,_raw_value) { true }
EMPTY_VALIDATOR = ->(_transformed_value, _raw_value) { true }
PROTECTED_METHODS = [:assign_values!, :convenience_methods, :validate_required!, :validate_optional!, :validate_value]

def self.add_field(name:, type: nil, types: [], dig_type: nil, dig: nil, validator: EMPTY_VALIDATOR, required: true, converter: nil, array_of_types: false)
include JsonSchematize::Introspect

def self.add_field(name:, type: nil, types: nil, dig_type: nil, dig: nil, validator: nil, required: nil, converter: nil, array_of_types: nil)
field_params = {
converter: converter,
dig: dig,
dig_type: dig_type,
converter: converter || schema_defaults[:converter],
dig: dig || schema_defaults[:dig],
dig_type: dig_type || schema_defaults[:dig_type],
name: name,
required: required,
type: type,
types: types,
validator: validator,
required: (required.nil? ? schema_defaults.fetch(:required, true) : required),
type: type || schema_defaults[:type],
types: types || schema_defaults.fetch(:types, []),
validator: validator || schema_defaults.fetch(:validator, EMPTY_VALIDATOR),
array_of_types: (array_of_types.nil? ? schema_defaults.fetch(:array_of_types, false) : array_of_types),
}

field = JsonSchematize::Field.new(**field_params)
field.setup!

if required
if field_params[:required] == true
required_fields << field
else
optional_fields << field
end
convenience_methods(field: field)
end

def self.schema_default(option:, value:)
if fields.length > 0
::Kernel.warn("Default [#{option}] set after fields #{fields.map(&:name)} created. #{option} default will behave inconsistently")
end

schema_defaults[option.to_sym] = value
end

def self.schema_defaults
@schema_defaults ||= {}
end

def self.fields
required_fields + optional_fields
end
Expand Down Expand Up @@ -65,18 +79,24 @@ def self.convenience_methods(field:)
end
end

attr_reader :__raw_params, :raise_on_error
attr_reader :__raw_params, :raise_on_error, :values_assigned

# stringified_params allows for params with stringed keys
def initialize(stringified_params = {}, raise_on_error: true, **params)
@__params = stringified_params.empty? ? params : stringified_params
def initialize(stringified_params = nil, raise_on_error: true, **params)
@values_assigned = false
@__params = stringified_params.nil? ? params : stringified_params
@__raw_params = @__params
@raise_on_error = raise_on_error

validate_required!
validate_optional!
assign_values!
if @__params
validate_required!
validate_optional!
assign_values!
@values_assigned = true
end
end


private

def assign_values!
Expand Down
27 changes: 27 additions & 0 deletions lib/json_schematize/introspect.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module JsonSchematize::Introspect
def to_h
self.class.fields.map do |field|
[field.name, instance_variable_get(:"@#{field.name}")]
end.to_h
end
alias :to_hash :to_h

def deep_inspect(with_raw_params: false, with_field: false)
self.class.fields.map do |field|
value = {
required: field.required,
acceptable_types: field.acceptable_types,
value: instance_variable_get(:"@#{field.name}"),
}
value[:field] = field if with_field
value[:raw_params] = @__raw_params if with_raw_params
[field.name, value]
end.to_h
end

def inspect
"#<#{self.class} - required fields: #{self.class.required_fields.map(&:name)}; optional fields: #{self.class.optional_fields.map(&:name)}>"
end
end
2 changes: 1 addition & 1 deletion lib/json_schematize/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module JsonSchematize
VERSION = "0.3.1"
VERSION = "0.4.0"
end
126 changes: 126 additions & 0 deletions spec/lib/json_schematize/generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,39 @@
end
end

describe ".schema_default" do
let(:instance) { klass.new(**params) }
let(:klass) do
class SchemaDefault < described_class
schema_default option: :dig_type, value: :string

add_field name: :count, type: Integer
add_field name: :status, type: Symbol, dig: [:l1, :status]
add_field name: :something, type: Symbol, required: false
end
SchemaDefault
end
let(:raise_on_error) { true }
let(:params) do
{
"count" => count,
"l1" => { "status" => status },
"something" => something,
}
end
let(:count) { 5 }
let(:status) { "status" }
let(:something) { "something" }

it "sets correct default value" do
expect(klass.fields.all? { |f| f.dig_type == :string }).to eq(true)
end

it "gets correct value" do
expect(instance.status).to eq(status.to_sym)
end
end

describe ".initialize" do
subject { instance }
let(:instance) { klass.new(raise_on_error: raise_on_error, **params) }
Expand Down Expand Up @@ -99,6 +132,99 @@ class KlassInit < described_class
end
end

describe "introspection" do
let(:instance) { klass.new(**params) }

let(:params) do
{
id: 6457,
count: 9145,
style: :symbol,
something: "danger",
danger: :count,
zone: :zone,
}
end

describe "#to_h" do
subject(:to_h) { instance.to_h }

let(:klass) do
class IntrospectKlassToH < described_class
add_field name: :id, type: Integer
add_field name: :count, type: Integer
add_field name: :style, type: Symbol
add_field name: :something, type: String
add_field name: :danger, type: Symbol
add_field name: :zone, type: Symbol
end
IntrospectKlassToH
end
it { is_expected.to eq(params) }
end

describe "#deep_inspect" do
subject(:deep_inspect) { instance.deep_inspect(with_raw_params: with_raw_params, with_field: with_field) }

let(:klass) do
class IntrospectKlassDeepInspect < described_class
add_field name: :id, type: Integer
add_field name: :count, type: Integer
add_field name: :style, type: Symbol
add_field name: :something, type: String
add_field name: :danger, type: Symbol
add_field name: :zone, type: Symbol
end
IntrospectKlassDeepInspect
end
let(:with_raw_params) { false }
let(:with_field) { false }
let(:enumerate_expected) do
klass.fields.map do |field|
value = {
required: field.required,
acceptable_types: field.acceptable_types,
value: params[field.name],
}
value[:field] = field if with_field
value[:raw_params] = params if with_raw_params
[field.name, value]
end.to_h
end

it { is_expected.to eq(enumerate_expected) }

context 'when with_raw_params' do
let(:with_raw_params) { true }
it { is_expected.to eq(enumerate_expected) }
end

context 'when with_field' do
let(:with_field) { true }

it { is_expected.to eq(enumerate_expected) }
end
end

describe "#inspect" do
subject(:inspect) { instance.inspect }

let(:klass) do
class IntrospectKlassInspect < described_class
add_field name: :id, type: Integer
add_field name: :count, type: Integer
add_field name: :style, type: Symbol
add_field name: :something, type: String
add_field name: :danger, type: Symbol
add_field name: :zone, type: Symbol
end
IntrospectKlassInspect
end
let(:expected) { "#<#{klass} - required fields: #{params.keys}; optional fields: []>" }
it { is_expected.to eq(expected) }
end
end

context "when modifying values" do
let(:instance) { klass.new(raise_on_error: raise_on_error, **params) }
let(:klass) do
Expand Down
2 changes: 1 addition & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
config.default_formatter = "doc"
end

config.profile_examples = 10
config.profile_examples = 2

config.order = :random
Kernel.srand config.seed
Expand Down

0 comments on commit c0ad8b5

Please sign in to comment.