diff --git a/Gemfile.lock b/Gemfile.lock index 68a8794..8a05df5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - json_schematize (0.3.0) + json_schematize (0.4.0) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index a0d242f..55b2fc2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/json_schematize/field.rb b/lib/json_schematize/field.rb index e145da4..e6177e2 100644 --- a/lib/json_schematize/field.rb +++ b/lib/json_schematize/field.rb @@ -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] @@ -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 diff --git a/lib/json_schematize/generator.rb b/lib/json_schematize/generator.rb index c5673ad..3298b4d 100644 --- a/lib/json_schematize/generator.rb +++ b/lib/json_schematize/generator.rb @@ -1,29 +1,31 @@ # 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 @@ -31,6 +33,18 @@ def self.add_field(name:, type: nil, types: [], dig_type: nil, dig: nil, validat 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 @@ -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! diff --git a/lib/json_schematize/introspect.rb b/lib/json_schematize/introspect.rb new file mode 100644 index 0000000..75fba63 --- /dev/null +++ b/lib/json_schematize/introspect.rb @@ -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 diff --git a/lib/json_schematize/version.rb b/lib/json_schematize/version.rb index df159a2..d596669 100644 --- a/lib/json_schematize/version.rb +++ b/lib/json_schematize/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JsonSchematize - VERSION = "0.3.1" + VERSION = "0.4.0" end diff --git a/spec/lib/json_schematize/generator_spec.rb b/spec/lib/json_schematize/generator_spec.rb index 056189f..f5e9ecd 100644 --- a/spec/lib/json_schematize/generator_spec.rb +++ b/spec/lib/json_schematize/generator_spec.rb @@ -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) } @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5712dc1..4ea6552 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -59,7 +59,7 @@ config.default_formatter = "doc" end - config.profile_examples = 10 + config.profile_examples = 2 config.order = :random Kernel.srand config.seed