diff --git a/Gemfile b/Gemfile index fa6f2c7..e1670d0 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ source "https://rubygems.org" # Specify your gem's dependencies in scheemer.gemspec gemspec +gem "pry-nav" gem "rake", "~> 13.0" gem "rspec", "~> 3.0" gem "rubocop", "~> 1.21" diff --git a/Gemfile.lock b/Gemfile.lock index d3112e7..fa324ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,12 +2,22 @@ PATH remote: . specs: scheemer (3.0.0) + activesupport dry-schema (~> 1.13) + json-schema (~> 4.0) GEM remote: https://rubygems.org/ specs: + activesupport (7.0.4.3) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + coderay (1.1.3) concurrent-ruby (1.2.2) diff-lcs (1.5.0) dry-configurable (1.0.1) @@ -36,10 +46,22 @@ GEM dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) + i18n (1.13.0) + concurrent-ruby (~> 1.0) json (2.6.3) + json-schema (4.0.0) + addressable (>= 2.8) + method_source (1.0.0) + minitest (5.18.0) parallel (1.23.0) parser (3.2.2.1) ast (~> 2.4.1) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-nav (1.0.0) + pry (>= 0.9.10, < 0.15) + public_suffix (5.0.1) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.8.0) @@ -72,6 +94,8 @@ GEM rubocop-rspec (2.12.1) rubocop (~> 1.31) ruby-progressbar (1.13.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) zeitwerk (2.6.8) @@ -79,6 +103,7 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + pry-nav rake (~> 13.0) rspec (~> 3.0) rubocop (~> 1.21) diff --git a/lib/scheemer/params.rb b/lib/scheemer/params.rb index d548602..cf3e327 100644 --- a/lib/scheemer/params.rb +++ b/lib/scheemer/params.rb @@ -3,6 +3,7 @@ require_relative "./fallbacker" require_relative "./extensions/string" +require "active_support/core_ext/hash" module Scheemer # This handles the conversion from the HTTP linguo (camelCase) @@ -28,7 +29,10 @@ def params_fallbacks module InstanceMethods def initialize(params, data = {}) - @params = Fallbacker.apply(params, self.class.params_fallbacks) + @params = Fallbacker.apply( + params.to_h.deep_symbolize_keys, + self.class.params_fallbacks + ) validate!(data.to_h) if respond_to?(:validate!) end diff --git a/lib/scheemer/schema.rb b/lib/scheemer/schema.rb index 2cf2f91..6a2108a 100644 --- a/lib/scheemer/schema.rb +++ b/lib/scheemer/schema.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "dry-schema" +require "json-schema" Dry::Schema.load_extensions(:hints, :json_schema) @@ -40,6 +41,8 @@ def check_schema_exists! def initialize(&) @definitions = ::Dry::Schema.Params do + config.validate_keys = false + instance_eval(&) end end @@ -49,15 +52,16 @@ def validate(params) end def validate!(params) - validate(params).tap do |result| - next if result.success? + valid_as_json = JSON::Validator.validate(json_schema, params) + valid_as_dry = validate(params) - raise InvalidSchemaError, result.messages.to_h - end + return params if valid_as_json && valid_as_dry.success? + + raise InvalidSchemaError, valid_as_dry.messages.to_h end def json_schema - @definitions.json_schema + @definitions.json_schema(loose: true) end end end diff --git a/scheemer.gemspec b/scheemer.gemspec index eceae48..e90c088 100644 --- a/scheemer.gemspec +++ b/scheemer.gemspec @@ -32,6 +32,8 @@ Gem::Specification.new do |spec| # Uncomment to register a new dependency of your gem spec.add_dependency "dry-schema", "~> 1.13" + spec.add_dependency "json-schema", "~> 4.0" + spec.add_dependency "activesupport" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/spec/scheemer/params_spec.rb b/spec/scheemer/params_spec.rb index cced0ea..64a413f 100644 --- a/spec/scheemer/params_spec.rb +++ b/spec/scheemer/params_spec.rb @@ -11,20 +11,34 @@ end end - subject(:record) { klass.new({ someValue: "testing" }) } + context "when keys are symbols" do + subject(:record) { klass.new({ someValue: "testing" }) } + + it "allows access to fields using underscored accessors" do + expect(record.some_value).to eql("testing") + end - it "allows access to fields using underscored accessors" do - expect(record.some_value).to eql("testing") + it "allows access to fields using camelcase accessors" do + expect(record.someValue).to eql("testing") + end end - it "allows access to fields using camelcase accessors" do - expect(record.someValue).to eql("testing") + context "when keys are strings" do + subject(:record) { klass.new({ "someValue" => "testing" }) } + + it "allows access to fields using underscored accessors" do + expect(record.some_value).to eql("testing") + end + + it "allows access to fields using camelcase accessors" do + expect(record.someValue).to eql("testing") + end end end end describe ".on_missing" do - context "with a single level key" do + context "with a shallow key" do let(:klass) do Class.new do extend Scheemer::Params::DSL @@ -35,7 +49,7 @@ subject(:record) { klass.new({ someValue: "testing" }) } - it "allows access to fields using underscored accessors" do + it "fills in the missing missing" do expect(record.content).to eql({ fall: "back" }) expect(record.someValue).to eql("testing") end diff --git a/spec/scheemer/schema_spec.rb b/spec/scheemer/schema_spec.rb index 86021a7..3c9ae15 100644 --- a/spec/scheemer/schema_spec.rb +++ b/spec/scheemer/schema_spec.rb @@ -6,7 +6,7 @@ describe ".validate!" do subject(:schema) do described_class.new do - required(:test) + required(:test).filled(:string) end end @@ -24,4 +24,30 @@ end end end + + describe "#json_schema" do + context "when a key is using unsupported `format?`" do + subject(:schema) do + described_class.new do + required(:test).filled(:str?, format?: /asd/) + end + end + + it "generates the schema without it" do + expect(schema.json_schema).to eql( + { + "$schema": "http://json-schema.org/draft-06/schema#", + properties: { + test: { + minLength: 1, + type: "string", + }, + }, + required: ["test"], + type: "object", + } + ) + end + end + end end diff --git a/spec/scheemer_spec.rb b/spec/scheemer_spec.rb index ec30500..145d447 100644 --- a/spec/scheemer_spec.rb +++ b/spec/scheemer_spec.rb @@ -21,7 +21,9 @@ end end - subject(:record) { klass.new({ root: { someValue: "testing" } }) } + subject(:record) do + klass.new({ root: { someValue: "testing" } }) + end it "allows access to fields using underscored accessors" do expect(record.some_value).to eql("testing") @@ -58,5 +60,41 @@ it { expect { klass.new({}) }.to raise_error(NotImplementedError) } end + + context "when the extra fields are specified" do + let(:klass) do + Class.new do + extend Scheemer::DSL + + schema do + required(:item).hash do + required(:content).hash do + required(:name).filled(:string) + optional(:address).filled(:string) + end + end + end + end + end + let(:data) do + { + item: { + content: { + name: "John", + age: "9999", + address: "John's Street 69" + } + } + } + end + + subject(:record) { klass.new(data) } + + it "it allows all fields through" do + expect(record.content.dig(:name)).to eql "John" + expect(record.content.dig(:age)).to eql "9999" + expect(record.content.dig(:address)).to eql "John's Street 69" + end + end end end