From c89e30f10525a22cd87ca3cdd71b5dc732fdf044 Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Mon, 9 Jul 2018 14:29:44 +0200 Subject: [PATCH 01/10] feat(): implement _object as a type --- lib/jsonschema_serializer/builder.rb | 3 ++- lib/jsonschema_serializer/types.rb | 30 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 lib/jsonschema_serializer/types.rb diff --git a/lib/jsonschema_serializer/builder.rb b/lib/jsonschema_serializer/builder.rb index c1bf757..a4971b1 100644 --- a/lib/jsonschema_serializer/builder.rb +++ b/lib/jsonschema_serializer/builder.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'json' +require_relative 'types' module JsonschemaSerializer # The +JsonschemaSerializer::Builder+ class provides @@ -171,7 +172,7 @@ def string(name, **opts) # end def _object(**opts) - { type: :object, properties: {} }.merge(opts).tap do |h| + JsonschemaSerializer::Types::Object.empty(opts).tap do |h| yield(h[:properties]) if block_given? end end diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb new file mode 100644 index 0000000..b2dbf00 --- /dev/null +++ b/lib/jsonschema_serializer/types.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module JsonschemaSerializer + # This module contains types declarations + module Types + # Base type from which all other types inherit + class Base < Hash + # Backport ruby 2.5 yield_self method + def yield_self + yield(self) + end + end + + # Object type for jsonschema serializer + class Object < JsonschemaSerializer::Types::Base + class << self + # Default Hash structure + DEFAULT_HASH = { + type: :object, properties: {} + }.freeze + # initialize a empty object + def empty(**opts) + new + .yield_self { |h| h.merge(DEFAULT_HASH) } + .merge(opts) + end + end + end + end +end From 5910079b05816818eafd502fefe415bdd1181d4e Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Mon, 9 Jul 2018 15:06:39 +0200 Subject: [PATCH 02/10] feat(): implement integer and number as a type --- lib/jsonschema_serializer/builder.rb | 12 ++--- lib/jsonschema_serializer/types.rb | 50 ++++++++++++++++--- spec/jsonschema_serializer/error_spec.rb | 1 - spec/jsonschema_serializer/types_spec.rb | 63 ++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 spec/jsonschema_serializer/types_spec.rb diff --git a/lib/jsonschema_serializer/builder.rb b/lib/jsonschema_serializer/builder.rb index a4971b1..9dfc906 100644 --- a/lib/jsonschema_serializer/builder.rb +++ b/lib/jsonschema_serializer/builder.rb @@ -22,7 +22,7 @@ def build # The +new+ method creates assigns an empty object # to a +schema+ instance variable def initialize - @schema ||= _object + @schema ||= JsonschemaSerializer::Types::Object.empty end # The +to_json+ method exports the schema as a json string @@ -96,7 +96,7 @@ def boolean(name, **opts) # A base representation of the +integer+ type. def _integer(**opts) - { type: :integer }.merge(opts) + JsonschemaSerializer::Types::Integer.empty(**opts) end # A property representation of the +integer+ type. @@ -114,12 +114,12 @@ def _integer(**opts) # +multipleOf+:: +Integer+ property conditional constraint def integer(name, **opts) - { name => _integer(opts) } + JsonschemaSerializer::Types::Integer.named(name, **opts) end # A base representation of the +number+ type. def _number(**opts) - { type: :number }.merge(opts) + JsonschemaSerializer::Types::Number.empty(**opts) end # A property representation of the +number+ type. @@ -137,7 +137,7 @@ def _number(**opts) # +multipleOf+:: +Numeric+ property conditional constraint def number(name, **opts) - { name => _number(opts) } + JsonschemaSerializer::Types::Number.named(name, **opts) end # A base representation of the +string+ type. @@ -172,7 +172,7 @@ def string(name, **opts) # end def _object(**opts) - JsonschemaSerializer::Types::Object.empty(opts).tap do |h| + JsonschemaSerializer::Types::Object.empty(**opts).tap do |h| yield(h[:properties]) if block_given? end end diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index b2dbf00..17ba01b 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -9,20 +9,56 @@ class Base < Hash def yield_self yield(self) end + + class << self + # Default Hash structure. This should be implemented in + # every subclass + def default_hash + {} + end + + def empty(**opts) + new + .yield_self { |h| h.merge(default_hash) } + .merge(opts) + end + + def named(name, **opts) + new + .yield_self do |h| + h.merge(name => empty(**opts)) + end + end + end end # Object type for jsonschema serializer class Object < JsonschemaSerializer::Types::Base class << self # Default Hash structure - DEFAULT_HASH = { - type: :object, properties: {} - }.freeze + def default_hash + { type: :object, properties: {} } + end # initialize a empty object - def empty(**opts) - new - .yield_self { |h| h.merge(DEFAULT_HASH) } - .merge(opts) + end + end + + # Number type for jsonschema serializer + class Number < JsonschemaSerializer::Types::Base + class << self + # Default Hash structure + def default_hash + { type: :number } + end + end + end + + # Integer type for jsonschema serializer + class Integer < JsonschemaSerializer::Types::Base + class << self + # Default Hash structure + def default_hash + { type: :integer } end end end diff --git a/spec/jsonschema_serializer/error_spec.rb b/spec/jsonschema_serializer/error_spec.rb index e4ad236..292fe27 100644 --- a/spec/jsonschema_serializer/error_spec.rb +++ b/spec/jsonschema_serializer/error_spec.rb @@ -1,4 +1,3 @@ - # frozen_string_literal: true RSpec.describe 'JsonschemaSerializer errors' do diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb new file mode 100644 index 0000000..b8bee08 --- /dev/null +++ b/spec/jsonschema_serializer/types_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +RSpec.describe 'JsonschemaSerializer types' do + describe JsonschemaSerializer::Types::Base do + subject { JsonschemaSerializer::Types::Base } + + it { subject.respond_to?(:default_hash) } + it { expect(subject.default_hash).to eq({}) } + + it { subject.respond_to?(:empty) } + it { expect(subject.empty).to eq({}) } + it { expect(subject.empty(a: 1)).to eq(a: 1) } + + it { subject.respond_to?(:named) } + it { expect(subject.named(:foo)).to eq(foo: {}) } + it { expect(subject.named(:foo, bar: :baz)).to eq(foo: { bar: :baz }) } + end + + describe JsonschemaSerializer::Types::Object do + subject { JsonschemaSerializer::Types::Object } + + it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } + it { expect(subject.default_hash).to eq(type: :object, properties: {}) } + it { expect(subject.empty).to eq(type: :object, properties: {}) } + it { expect(subject.empty(a: 1)).to eq(type: :object, properties: {}, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :object, properties: {} }) } + it do + expect(subject.named(:foo, bar: :baz)).to eq( + foo: { type: :object, properties: {}, bar: :baz } + ) + end + end + + describe JsonschemaSerializer::Types::Integer do + subject { JsonschemaSerializer::Types::Integer } + + it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } + it { expect(subject.default_hash).to eq(type: :integer) } + it { expect(subject.empty).to eq(type: :integer) } + it { expect(subject.empty(a: 1)).to eq(type: :integer, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :integer }) } + it do + expect(subject.named(:foo, bar: :baz)).to eq( + foo: { type: :integer, bar: :baz } + ) + end + end + + describe JsonschemaSerializer::Types::Number do + subject { JsonschemaSerializer::Types::Number } + + it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } + it { expect(subject.default_hash).to eq(type: :number) } + it { expect(subject.empty).to eq(type: :number) } + it { expect(subject.empty(a: 1)).to eq(type: :number, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :number }) } + it do + expect(subject.named(:foo, bar: :baz)).to eq( + foo: { type: :number, bar: :baz } + ) + end + end +end From 19ef938b8da05b52c0e6554bb586496d874a0e16 Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Mon, 9 Jul 2018 16:44:24 +0200 Subject: [PATCH 03/10] feat(): implement string as a type --- lib/jsonschema_serializer/builder.rb | 4 ++-- lib/jsonschema_serializer/types.rb | 10 ++++++++++ spec/jsonschema_serializer/builder_spec.rb | 9 +++++++++ spec/jsonschema_serializer/types_spec.rb | 15 +++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/jsonschema_serializer/builder.rb b/lib/jsonschema_serializer/builder.rb index 9dfc906..cf452fc 100644 --- a/lib/jsonschema_serializer/builder.rb +++ b/lib/jsonschema_serializer/builder.rb @@ -142,7 +142,7 @@ def number(name, **opts) # A base representation of the +string+ type. def _string(**opts) - { type: :string }.merge(opts) + JsonschemaSerializer::Types::String.empty(**opts) end # A property representation of the +string+ type. @@ -158,7 +158,7 @@ def _string(**opts) # +minLength+:: +Int+ property minimum length def string(name, **opts) - { name => _string(opts) } + JsonschemaSerializer::Types::String.named(name, **opts) end # A base representation of the +object+ type. diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index 17ba01b..9d1b7da 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -62,5 +62,15 @@ def default_hash end end end + + # String type for jsonschema serializer + class String < JsonschemaSerializer::Types::Base + class << self + # Default Hash structure + def default_hash + { type: :string } + end + end + end end end diff --git a/spec/jsonschema_serializer/builder_spec.rb b/spec/jsonschema_serializer/builder_spec.rb index 0f9d39a..96622ec 100644 --- a/spec/jsonschema_serializer/builder_spec.rb +++ b/spec/jsonschema_serializer/builder_spec.rb @@ -219,6 +219,15 @@ end end + describe 'dsl for empty types' do + subject { builder.new } + + it { expect(subject._integer).to eq(JsonschemaSerializer::Types::Integer.empty) } + it { expect(subject._number).to eq(JsonschemaSerializer::Types::Number.empty) } + it { expect(subject._object).to eq(JsonschemaSerializer::Types::Object.empty) } + it { expect(subject._string).to eq(JsonschemaSerializer::Types::String.empty) } + end + context '.to_json' do let(:instance) { builder.new } diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index b8bee08..3b48bff 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -60,4 +60,19 @@ ) end end + + describe JsonschemaSerializer::Types::String do + subject { JsonschemaSerializer::Types::String } + + it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } + it { expect(subject.default_hash).to eq(type: :string) } + it { expect(subject.empty).to eq(type: :string) } + it { expect(subject.empty(a: 1)).to eq(type: :string, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :string }) } + it do + expect(subject.named(:foo, bar: :baz)).to eq( + foo: { type: :string, bar: :baz } + ) + end + end end From 7af5a6548947954833fa01502b935b677a49a739 Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Mon, 9 Jul 2018 16:52:58 +0200 Subject: [PATCH 04/10] feat(): implement boolean as a type --- lib/jsonschema_serializer/builder.rb | 4 ++-- lib/jsonschema_serializer/types.rb | 24 +++++++++++++++------- spec/jsonschema_serializer/builder_spec.rb | 1 + spec/jsonschema_serializer/types_spec.rb | 15 ++++++++++++++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/jsonschema_serializer/builder.rb b/lib/jsonschema_serializer/builder.rb index cf452fc..5f42157 100644 --- a/lib/jsonschema_serializer/builder.rb +++ b/lib/jsonschema_serializer/builder.rb @@ -77,7 +77,7 @@ def properties # A base representation of the +boolean+ type. def _boolean(**opts) - { type: :boolean }.merge(opts) + JsonschemaSerializer::Types::Boolean.empty(**opts) end # A property representation of the +boolean+ type. @@ -91,7 +91,7 @@ def _boolean(**opts) # +title+:: +String+ property title def boolean(name, **opts) - { name => _boolean(opts) } + JsonschemaSerializer::Types::Boolean.named(name, **opts) end # A base representation of the +integer+ type. diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index 9d1b7da..21a1b58 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -32,14 +32,23 @@ def named(name, **opts) end end - # Object type for jsonschema serializer - class Object < JsonschemaSerializer::Types::Base + # Boolean type for jsonschema serializer + class Boolean < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :object, properties: {} } + { type: :boolean } + end + end + end + + # Integer type for jsonschema serializer + class Integer < JsonschemaSerializer::Types::Base + class << self + # Default Hash structure + def default_hash + { type: :integer } end - # initialize a empty object end end @@ -53,13 +62,14 @@ def default_hash end end - # Integer type for jsonschema serializer - class Integer < JsonschemaSerializer::Types::Base + # Object type for jsonschema serializer + class Object < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :integer } + { type: :object, properties: {} } end + # initialize a empty object end end diff --git a/spec/jsonschema_serializer/builder_spec.rb b/spec/jsonschema_serializer/builder_spec.rb index 96622ec..e959339 100644 --- a/spec/jsonschema_serializer/builder_spec.rb +++ b/spec/jsonschema_serializer/builder_spec.rb @@ -222,6 +222,7 @@ describe 'dsl for empty types' do subject { builder.new } + it { expect(subject._boolean).to eq(JsonschemaSerializer::Types::Boolean.empty) } it { expect(subject._integer).to eq(JsonschemaSerializer::Types::Integer.empty) } it { expect(subject._number).to eq(JsonschemaSerializer::Types::Number.empty) } it { expect(subject._object).to eq(JsonschemaSerializer::Types::Object.empty) } diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index 3b48bff..f075335 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -75,4 +75,19 @@ ) end end + + describe JsonschemaSerializer::Types::Boolean do + subject { JsonschemaSerializer::Types::Boolean } + + it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } + it { expect(subject.default_hash).to eq(type: :boolean) } + it { expect(subject.empty).to eq(type: :boolean) } + it { expect(subject.empty(a: 1)).to eq(type: :boolean, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :boolean }) } + it do + expect(subject.named(:foo, bar: :baz)).to eq( + foo: { type: :boolean, bar: :baz } + ) + end + end end From 65a57118ce3acc3bd1f2770cca737c4b0b331e5b Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Tue, 10 Jul 2018 00:04:00 +0200 Subject: [PATCH 05/10] feat(): implement array as a type --- lib/jsonschema_serializer/builder.rb | 4 +- lib/jsonschema_serializer/types.rb | 21 ++++++++ spec/jsonschema_serializer/types_spec.rb | 66 ++++++++++++++++-------- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/lib/jsonschema_serializer/builder.rb b/lib/jsonschema_serializer/builder.rb index 5f42157..12b2a35 100644 --- a/lib/jsonschema_serializer/builder.rb +++ b/lib/jsonschema_serializer/builder.rb @@ -203,9 +203,7 @@ def _object(**opts) # end def array(name, items:, **opts) - { - name => { type: :array, items: items }.merge(opts) - } + JsonschemaSerializer::Types::Array.named(name, items: items, **opts) end end end diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index 21a1b58..28d8ae2 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -32,6 +32,27 @@ def named(name, **opts) end end + # Array type for jsonschema serializer + class Array < JsonschemaSerializer::Types::Base + class << self + # Default Hash structure + def default_hash + { type: :array } + end + + # Default empty array + # + # Params: + # +items+:: an object representation or an array of objects + # + + def empty(items:, **opts) + super(**opts) + .yield_self { |h| h.merge(items: items) } + end + end + end + # Boolean type for jsonschema serializer class Boolean < JsonschemaSerializer::Types::Base class << self diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index f075335..352551e 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -16,17 +16,41 @@ it { expect(subject.named(:foo, bar: :baz)).to eq(foo: { bar: :baz }) } end - describe JsonschemaSerializer::Types::Object do - subject { JsonschemaSerializer::Types::Object } + describe JsonschemaSerializer::Types::Array do + subject { JsonschemaSerializer::Types::Array } it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } - it { expect(subject.default_hash).to eq(type: :object, properties: {}) } - it { expect(subject.empty).to eq(type: :object, properties: {}) } - it { expect(subject.empty(a: 1)).to eq(type: :object, properties: {}, a: 1) } - it { expect(subject.named(:foo)).to eq(foo: { type: :object, properties: {} }) } + it { expect(subject.default_hash).to eq(type: :array) } + + it 'needs items key' do + expect { subject.empty }.to raise_error(ArgumentError, /items/) + end + + it { expect(subject.empty(items: {})).to eq(type: :array, items: {}) } + it do + expect(subject.named(:foo, items: {})).to eq( + foo: { type: :array, items: {} } + ) + end + + it do + expect(subject.named(:foo, bar: :baz, items: {})).to eq( + foo: { type: :array, bar: :baz, items: {} } + ) + end + end + + describe JsonschemaSerializer::Types::Boolean do + subject { JsonschemaSerializer::Types::Boolean } + + it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } + it { expect(subject.default_hash).to eq(type: :boolean) } + it { expect(subject.empty).to eq(type: :boolean) } + it { expect(subject.empty(a: 1)).to eq(type: :boolean, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :boolean }) } it do expect(subject.named(:foo, bar: :baz)).to eq( - foo: { type: :object, properties: {}, bar: :baz } + foo: { type: :boolean, bar: :baz } ) end end @@ -61,32 +85,32 @@ end end - describe JsonschemaSerializer::Types::String do - subject { JsonschemaSerializer::Types::String } + describe JsonschemaSerializer::Types::Object do + subject { JsonschemaSerializer::Types::Object } it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } - it { expect(subject.default_hash).to eq(type: :string) } - it { expect(subject.empty).to eq(type: :string) } - it { expect(subject.empty(a: 1)).to eq(type: :string, a: 1) } - it { expect(subject.named(:foo)).to eq(foo: { type: :string }) } + it { expect(subject.default_hash).to eq(type: :object, properties: {}) } + it { expect(subject.empty).to eq(type: :object, properties: {}) } + it { expect(subject.empty(a: 1)).to eq(type: :object, properties: {}, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :object, properties: {} }) } it do expect(subject.named(:foo, bar: :baz)).to eq( - foo: { type: :string, bar: :baz } + foo: { type: :object, properties: {}, bar: :baz } ) end end - describe JsonschemaSerializer::Types::Boolean do - subject { JsonschemaSerializer::Types::Boolean } + describe JsonschemaSerializer::Types::String do + subject { JsonschemaSerializer::Types::String } it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } - it { expect(subject.default_hash).to eq(type: :boolean) } - it { expect(subject.empty).to eq(type: :boolean) } - it { expect(subject.empty(a: 1)).to eq(type: :boolean, a: 1) } - it { expect(subject.named(:foo)).to eq(foo: { type: :boolean }) } + it { expect(subject.default_hash).to eq(type: :string) } + it { expect(subject.empty).to eq(type: :string) } + it { expect(subject.empty(a: 1)).to eq(type: :string, a: 1) } + it { expect(subject.named(:foo)).to eq(foo: { type: :string }) } it do expect(subject.named(:foo, bar: :baz)).to eq( - foo: { type: :boolean, bar: :baz } + foo: { type: :string, bar: :baz } ) end end From 04440b13eae44e78ae9cc57a27219499ceb6abc0 Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Tue, 10 Jul 2018 17:15:19 +0200 Subject: [PATCH 06/10] feat(): implement common class methods in JsonschemaSerializer::Base:Types --- lib/jsonschema_serializer/types.rb | 41 ++++++++++++++- spec/jsonschema_serializer/types_spec.rb | 65 ++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index 28d8ae2..8743046 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -17,18 +17,58 @@ def default_hash {} end + # Initialize an empty object with the default hash attributes def empty(**opts) new .yield_self { |h| h.merge(default_hash) } + .yield_self { |h| class_attributes.reduce(h) { |h1, a| h1.merge(a) } } .merge(opts) end + # Initialize an empty object with name as key def named(name, **opts) new .yield_self do |h| h.merge(name => empty(**opts)) end end + + # Allowed class attributes declaration + def allowed_class_attributes + [@title, @description, @default] + end + + # Class level attributes declaration + def class_attributes + allowed_class_attributes.compact + end + + # Assigns the +default+ to the object + # + # Params: + # +default+:: default value field of the object + + def default(default_value) + @default = { default: default_value } + end + + # Assigns the +description+ to the object + # + # Params: + # +description+:: +String+ or +Symbol+ description field of the object + + def description(description) + @description = { description: description } + end + + # Assigns the +title+ to the object + # + # Params: + # +title+:: +String+ or +Symbol+ title field of the object + + def title(title) + @title = { title: title } + end end end @@ -90,7 +130,6 @@ class << self def default_hash { type: :object, properties: {} } end - # initialize a empty object end end diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index 352551e..20d0f23 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -14,6 +14,36 @@ it { subject.respond_to?(:named) } it { expect(subject.named(:foo)).to eq(foo: {}) } it { expect(subject.named(:foo, bar: :baz)).to eq(foo: { bar: :baz }) } + + it { subject.respond_to?(:allowed_class_attributes) } + it { subject.respond_to?(:title) } + it { subject.respond_to?(:description) } + it { subject.respond_to?(:default) } + it { expect(subject.allowed_class_attributes).to eq([nil, nil, nil]) } + + it { subject.respond_to?(:class_attributes) } + it { expect(subject.class_attributes).to eq([]) } + + context 'a subclass' do + class PartialSerializer < JsonschemaSerializer::Types::Base + title 'a title' + end + + it 'PartialSerializer.allowed_class_attributes' do + expect(PartialSerializer.allowed_class_attributes).to match_array( + [{ title: 'a title' }, nil, nil] + ) + end + + it 'PartialSerializer.class_attributes' do + expect(PartialSerializer.class_attributes) + .to match_array([{ title: 'a title' }]) + end + + it 'PartialSerializer.empty' do + expect(PartialSerializer.empty).to eq(title: 'a title') + end + end end describe JsonschemaSerializer::Types::Array do @@ -53,6 +83,23 @@ foo: { type: :boolean, bar: :baz } ) end + + context 'a subclass' do + class BooleanSerializer < JsonschemaSerializer::Types::Boolean + title 'BooleanSerializer' + description 'a dummy serializer for testing purposes' + default true + end + + it 'should include class level attribute declaration' do + expect(BooleanSerializer.empty).to eq( + title: 'BooleanSerializer', + description: 'a dummy serializer for testing purposes', + default: true, + type: :boolean + ) + end + end end describe JsonschemaSerializer::Types::Integer do @@ -98,6 +145,24 @@ foo: { type: :object, properties: {}, bar: :baz } ) end + + context 'a subclass' do + class ObjectSerializer < JsonschemaSerializer::Types::Object + title 'ObjectSerializer' + description 'a dummy serializer for testing purposes' + default '{}' + end + + it 'should include class level attribute declaration' do + expect(ObjectSerializer.empty).to eq( + title: 'ObjectSerializer', + description: 'a dummy serializer for testing purposes', + default: '{}', + type: :object, + properties: {} + ) + end + end end describe JsonschemaSerializer::Types::String do From 509e085bf0bb11ba1f43434529dbc35a294866cd Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Thu, 12 Jul 2018 23:32:32 +0200 Subject: [PATCH 07/10] refacto(): kept a copy of current builder --- lib/jsonschema_serializer.rb | 1 + lib/jsonschema_serializer/builder.rb | 25 +- lib/jsonschema_serializer/typed_builder.rb | 209 +++++++++++++++ .../typed_builder_spec.rb | 246 ++++++++++++++++++ 4 files changed, 469 insertions(+), 12 deletions(-) create mode 100644 lib/jsonschema_serializer/typed_builder.rb create mode 100644 spec/jsonschema_serializer/typed_builder_spec.rb diff --git a/lib/jsonschema_serializer.rb b/lib/jsonschema_serializer.rb index e930e25..9879ed2 100644 --- a/lib/jsonschema_serializer.rb +++ b/lib/jsonschema_serializer.rb @@ -2,6 +2,7 @@ require 'jsonschema_serializer/version' require 'jsonschema_serializer/builder' +require 'jsonschema_serializer/typed_builder' require 'jsonschema_serializer/active_record' require 'jsonschema_serializer/filter_utilities' diff --git a/lib/jsonschema_serializer/builder.rb b/lib/jsonschema_serializer/builder.rb index 12b2a35..c1bf757 100644 --- a/lib/jsonschema_serializer/builder.rb +++ b/lib/jsonschema_serializer/builder.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'json' -require_relative 'types' module JsonschemaSerializer # The +JsonschemaSerializer::Builder+ class provides @@ -22,7 +21,7 @@ def build # The +new+ method creates assigns an empty object # to a +schema+ instance variable def initialize - @schema ||= JsonschemaSerializer::Types::Object.empty + @schema ||= _object end # The +to_json+ method exports the schema as a json string @@ -77,7 +76,7 @@ def properties # A base representation of the +boolean+ type. def _boolean(**opts) - JsonschemaSerializer::Types::Boolean.empty(**opts) + { type: :boolean }.merge(opts) end # A property representation of the +boolean+ type. @@ -91,12 +90,12 @@ def _boolean(**opts) # +title+:: +String+ property title def boolean(name, **opts) - JsonschemaSerializer::Types::Boolean.named(name, **opts) + { name => _boolean(opts) } end # A base representation of the +integer+ type. def _integer(**opts) - JsonschemaSerializer::Types::Integer.empty(**opts) + { type: :integer }.merge(opts) end # A property representation of the +integer+ type. @@ -114,12 +113,12 @@ def _integer(**opts) # +multipleOf+:: +Integer+ property conditional constraint def integer(name, **opts) - JsonschemaSerializer::Types::Integer.named(name, **opts) + { name => _integer(opts) } end # A base representation of the +number+ type. def _number(**opts) - JsonschemaSerializer::Types::Number.empty(**opts) + { type: :number }.merge(opts) end # A property representation of the +number+ type. @@ -137,12 +136,12 @@ def _number(**opts) # +multipleOf+:: +Numeric+ property conditional constraint def number(name, **opts) - JsonschemaSerializer::Types::Number.named(name, **opts) + { name => _number(opts) } end # A base representation of the +string+ type. def _string(**opts) - JsonschemaSerializer::Types::String.empty(**opts) + { type: :string }.merge(opts) end # A property representation of the +string+ type. @@ -158,7 +157,7 @@ def _string(**opts) # +minLength+:: +Int+ property minimum length def string(name, **opts) - JsonschemaSerializer::Types::String.named(name, **opts) + { name => _string(opts) } end # A base representation of the +object+ type. @@ -172,7 +171,7 @@ def string(name, **opts) # end def _object(**opts) - JsonschemaSerializer::Types::Object.empty(**opts).tap do |h| + { type: :object, properties: {} }.merge(opts).tap do |h| yield(h[:properties]) if block_given? end end @@ -203,7 +202,9 @@ def _object(**opts) # end def array(name, items:, **opts) - JsonschemaSerializer::Types::Array.named(name, items: items, **opts) + { + name => { type: :array, items: items }.merge(opts) + } end end end diff --git a/lib/jsonschema_serializer/typed_builder.rb b/lib/jsonschema_serializer/typed_builder.rb new file mode 100644 index 0000000..495a723 --- /dev/null +++ b/lib/jsonschema_serializer/typed_builder.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require 'json' +require_relative 'types' + +module JsonschemaSerializer + # The +JsonschemaSerializer::Builder+ class provides + # an effective DSL to generate a valid json schema. + class TypedBuilder + class << self + # The +build+ class method create a new instance and applies the block + def build + new.tap do |builder| + yield(builder) if block_given? + end + end + end + + # An hash representation of the +schema+ + attr_reader :schema + + # The +new+ method creates assigns an empty object + # to a +schema+ instance variable + def initialize + @schema ||= JsonschemaSerializer::Types::Object.empty + end + + # The +to_json+ method exports the schema as a json string + # By default it would exported with a pretty print + # + # Params: + # +pretty+:: +Boolean+ + + def to_json(pretty: true) + pretty ? JSON.pretty_generate(@schema) : @schema.to_json + end + + # Assigns the +title+ to the root schema object + # + # Params: + # +title+:: +String+ or +Symbol+ title field of the schema object + + def title(title) + @schema[:title] = title + end + + # Assigns the +description+ to the root schema object + # + # params: + # +description+:: +String+ description field of the schema object + + def description(description) + @schema[:description] = description + end + + # The +required+ method allows to provide a list of required properties + # + # params: + # +required+ [Array[String, Symbol]] + + def required(*required) + @schema[:required] = required + end + + # The +properties+ method allows to access object properties + # + # e.g.: + # JsonschemaSerializer::Builder.build do |b| + # b.properties.tap do |p| + # p.merge! {} + # end + # end + + def properties + @schema[:properties] ||= {} + end + + # A base representation of the +boolean+ type. + def _boolean(**opts) + JsonschemaSerializer::Types::Boolean.empty(**opts) + end + + # A property representation of the +boolean+ type. + # + # Params: + # +name+:: +String+ or +Symbol+ + # + # Optional Params: + # +default+:: +Boolean+ default value + # +description+:: +String+ property description + # +title+:: +String+ property title + + def boolean(name, **opts) + JsonschemaSerializer::Types::Boolean.named(name, **opts) + end + + # A base representation of the +integer+ type. + def _integer(**opts) + JsonschemaSerializer::Types::Integer.empty(**opts) + end + + # A property representation of the +integer+ type. + # + # Params: + # +name+:: +String+ or +Symbol+ + # + # Optional Params: + # +default+:: +Integer+ default value + # +description+:: +String+ property description + # +title+:: +String+ property title + # +enum+:: +Array+ property allowed values + # +minimum+:: +Integer+ property minimum value + # +maximum+:: +Integer+ property maximum value + # +multipleOf+:: +Integer+ property conditional constraint + + def integer(name, **opts) + JsonschemaSerializer::Types::Integer.named(name, **opts) + end + + # A base representation of the +number+ type. + def _number(**opts) + JsonschemaSerializer::Types::Number.empty(**opts) + end + + # A property representation of the +number+ type. + # + # Params: + # +name+:: +String+ or +Symbol+ + # + # Optional Params: + # +default+:: +Numeric+ default value + # +description+:: +String+ property description + # +title+:: +String+ property title + # +enum+:: +Array+ property allowed values + # +minimum+:: +Numeric+ property minimum value + # +maximum+:: +Numeric+ property maximum value + # +multipleOf+:: +Numeric+ property conditional constraint + + def number(name, **opts) + JsonschemaSerializer::Types::Number.named(name, **opts) + end + + # A base representation of the +string+ type. + def _string(**opts) + JsonschemaSerializer::Types::String.empty(**opts) + end + + # A property representation of the +string+ type. + # + # Params: + # +name+:: +String+ or +Symbol+ + # + # Optional Params: + # +default+:: +String+ default value + # +description+:: +String+ property description + # +title+:: +String+ property title + # +format+:: +String+ property format for validation + # +minLength+:: +Int+ property minimum length + + def string(name, **opts) + JsonschemaSerializer::Types::String.named(name, **opts) + end + + # A base representation of the +object+ type. + # + # JsonschemaSerializer::Builder.build do |b| + # subscriber = b._object title: :subscriber, required: [:age] do |prop| + # prop.merge! b.string :first_name, title: 'First Name' + # prop.merge! b.string :last_name, title: 'Last Name' + # prop.merge! b.integer :age, title: 'Age' + # end + # end + + def _object(**opts) + JsonschemaSerializer::Types::Object.empty(**opts).tap do |h| + yield(h[:properties]) if block_given? + end + end + + # A property representation of the +array+ type. + # + # Params: + # +name+:: +String+ or +Symbol+ + # +items+:: an object representation or an array of objects + # + # Optional Params: + # +default+:: +Array+ default value + # +description+:: +String+ property description + # +title+:: +String+ property title + # +minItems+:: +Int+ property minimum length + # +maxItems+:: +Int+ property maximum length + # + # JsonschemaSerializer::Builder.build do |b| + # b.array :integers, items: {type: :integer}, minItems:5 + # b.array :strings, items: b._string, default: [] + # + # subscriber = b._object title: :subscriber, required: [:age] do |prop| + # prop.merge! b.string :first_name, title: 'First Name' + # prop.merge! b.string :last_name, title: 'Last Name' + # prop.merge! b.integer :age, title: 'Age' + # end + # b.array :subscribers, items: subscriber + # end + + def array(name, items:, **opts) + JsonschemaSerializer::Types::Array.named(name, items: items, **opts) + end + end +end diff --git a/spec/jsonschema_serializer/typed_builder_spec.rb b/spec/jsonschema_serializer/typed_builder_spec.rb new file mode 100644 index 0000000..6c8ff3b --- /dev/null +++ b/spec/jsonschema_serializer/typed_builder_spec.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +RSpec.describe JsonschemaSerializer::TypedBuilder do + let(:builder) { JsonschemaSerializer::TypedBuilder } + + it 'should generate an empty object' do + expect(builder.build.schema).to eq(type: :object, properties: {}) + end + + it 'should generate add some attributes to the root object' do + actual = builder.build do |b| + b.title 'a title' + b.description 'a description' + b.required :a, :b, :c + end + + expect(actual.schema).to eq( + title: 'a title', + description: 'a description', + required: [:a, :b, :c], + type: :object, + properties: {} + ) + end + + context 'properties on root object' do + it 'should add simple array attributes' do + actual = builder.build do |b| + b.properties.tap do |p| + p.merge! b.array :ary, items: b._string(default: 'foo') + end + end + + expect(actual.schema).to eq( + type: :object, + properties: { + ary: { + type: :array, + items: { + type: :string, + default: 'foo' + } + } + } + ) + end + + it 'should add complex array attributes' do + actual = builder.build do |b| + subscriber = b._object title: :subscriber, required: [:age] do |prop| + prop.merge! b.string :first_name, title: 'First Name' + prop.merge! b.string :last_name, title: 'Last Name' + prop.merge! b.integer :age, title: 'Age' + end + + b.properties.tap do |p| + p.merge! b.array :subscribers, minItems: 1, items: subscriber + end + end + + expect(actual.schema).to eq( + type: :object, + properties: { + subscribers: { + type: :array, + minItems: 1, + items: { + type: :object, + title: :subscriber, + required: [:age], + properties: { + first_name: { + type: :string, + title: 'First Name' + }, + last_name: { + type: :string, + title: 'Last Name' + }, + age: { + type: :integer, + title: 'Age' + } + } + } + } + } + ) + end + + it 'should add boolean attributes' do + actual = builder.build do |b| + b.properties.tap do |p| + p.merge! b.boolean :a, default: true + end + end + + expect(actual.schema).to eq( + type: :object, + properties: { + a: { + type: :boolean, + default: true + } + } + ) + end + + it 'should add integer attributes' do + actual = builder.build do |b| + b.properties.tap do |p| + p.merge! b.integer :b, maximum: 10 + end + end + + expect(actual.schema).to eq( + type: :object, + properties: { + b: { + type: :integer, + maximum: 10 + } + } + ) + end + + it 'should add number attributes' do + actual = builder.build do |b| + b.properties.tap do |p| + p.merge! b.number :c, minimum: 3 + end + end + + expect(actual.schema).to eq( + type: :object, + properties: { + c: { + type: :number, + minimum: 3 + } + } + ) + end + + it 'should add string attributes' do + actual = builder.build do |b| + b.properties.tap do |p| + p.merge! b.string :d, description: 'abc' + end + end + + expect(actual.schema).to eq( + type: :object, + properties: { + d: { + type: :string, + description: 'abc' + } + } + ) + end + end + + describe 'generate valid json schemas' do + let(:schema) do + builder.build do |b| + b.title 'a title' + b.description 'a description' + b.required :a, :b, :c + b.properties.tap do |p| + p.merge! b.string :a, minLength: 2 + p.merge! b.number :b, maximum: 10 + p.merge! b.array :c, minItems: 1, items: b._integer + p.merge! b.boolean :d + p.merge! b.integer :e, enum: [1, 2, 3] + end + end.schema + end + + it 'should fail for empty data' do + expect(JSON::Validator.validate(schema, {})).to eq(false) + end + + it 'should fail for missing required keys' do + expect(JSON::Validator.validate(schema, a: 'ab')).to eq(false) + end + + context 'irrespective of required keys constraints' do + let(:valid_data) { { a: 'ab', b: 9.9, c: [1] } } + + it 'should succeed with valid data' do + expect(JSON::Validator.validate(schema, valid_data)).to eq(true) + end + + it 'should fail for string minLenght' do + min_length = valid_data.merge(a: '') + expect(JSON::Validator.validate(schema, min_length)).to eq(false) + end + + it 'should fail for number maximum' do + maximum = valid_data.merge(b: 10.1) + expect(JSON::Validator.validate(schema, maximum)).to eq(false) + end + + it 'should fail for array minItems' do + min_items = valid_data.merge(c: []) + expect(JSON::Validator.validate(schema, min_items)).to eq(false) + end + + it 'should fail for integer enum' do + enum_fail = valid_data.merge(e: 4) + expect(JSON::Validator.validate(schema, enum_fail)).to eq(false) + end + + it 'should fail for wrong type' do + type_fail = valid_data.merge(d: 4) + expect(JSON::Validator.validate(schema, type_fail)).to eq(false) + end + end + end + + describe 'dsl for empty types' do + subject { builder.new } + + it { expect(subject._boolean).to eq(JsonschemaSerializer::Types::Boolean.empty) } + it { expect(subject._integer).to eq(JsonschemaSerializer::Types::Integer.empty) } + it { expect(subject._number).to eq(JsonschemaSerializer::Types::Number.empty) } + it { expect(subject._object).to eq(JsonschemaSerializer::Types::Object.empty) } + it { expect(subject._string).to eq(JsonschemaSerializer::Types::String.empty) } + end + + context '.to_json' do + let(:instance) { builder.new } + + it 'should dump a prettified json by default' do + # Testing with regex since jruby add more \n + expected = /{\n \"type\": \"object\",\n \"properties\": {\n+ }\n}/ + expect(instance.to_json).to match(expected) + end + + it 'should allow to dump also not prettified json' do + expected = '{"type":"object","properties":{}}' + expect(instance.to_json(pretty: false)).to eq(expected) + end + end +end From 5a1a4d64867fb67d6acaa2f9f2981e1a794c4e2a Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Fri, 13 Jul 2018 00:12:33 +0200 Subject: [PATCH 08/10] refacto(): replaced empty with new and reworked named class method --- lib/jsonschema_serializer/typed_builder.rb | 14 ++-- lib/jsonschema_serializer/types.rb | 65 +++++++++++-------- spec/jsonschema_serializer/builder_spec.rb | 10 --- .../typed_builder_spec.rb | 10 +-- spec/jsonschema_serializer/types_spec.rb | 38 +++++------ 5 files changed, 69 insertions(+), 68 deletions(-) diff --git a/lib/jsonschema_serializer/typed_builder.rb b/lib/jsonschema_serializer/typed_builder.rb index 495a723..437a286 100644 --- a/lib/jsonschema_serializer/typed_builder.rb +++ b/lib/jsonschema_serializer/typed_builder.rb @@ -19,10 +19,10 @@ def build # An hash representation of the +schema+ attr_reader :schema - # The +new+ method creates assigns an empty object + # The +new+ method creates assigns a new object # to a +schema+ instance variable def initialize - @schema ||= JsonschemaSerializer::Types::Object.empty + @schema ||= JsonschemaSerializer::Types::Object.new end # The +to_json+ method exports the schema as a json string @@ -77,7 +77,7 @@ def properties # A base representation of the +boolean+ type. def _boolean(**opts) - JsonschemaSerializer::Types::Boolean.empty(**opts) + JsonschemaSerializer::Types::Boolean.new(**opts) end # A property representation of the +boolean+ type. @@ -96,7 +96,7 @@ def boolean(name, **opts) # A base representation of the +integer+ type. def _integer(**opts) - JsonschemaSerializer::Types::Integer.empty(**opts) + JsonschemaSerializer::Types::Integer.new(**opts) end # A property representation of the +integer+ type. @@ -119,7 +119,7 @@ def integer(name, **opts) # A base representation of the +number+ type. def _number(**opts) - JsonschemaSerializer::Types::Number.empty(**opts) + JsonschemaSerializer::Types::Number.new(**opts) end # A property representation of the +number+ type. @@ -142,7 +142,7 @@ def number(name, **opts) # A base representation of the +string+ type. def _string(**opts) - JsonschemaSerializer::Types::String.empty(**opts) + JsonschemaSerializer::Types::String.new(**opts) end # A property representation of the +string+ type. @@ -172,7 +172,7 @@ def string(name, **opts) # end def _object(**opts) - JsonschemaSerializer::Types::Object.empty(**opts).tap do |h| + JsonschemaSerializer::Types::Object.new(**opts).tap do |h| yield(h[:properties]) if block_given? end end diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index 8743046..e90610a 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -1,36 +1,48 @@ # frozen_string_literal: true module JsonschemaSerializer + # +Hash+ class with some extended methods + class FutureHash < Hash + # Backport ruby 2.5 yield_self method + def yield_self + yield(self) + end + end + # This module contains types declarations module Types # Base type from which all other types inherit - class Base < Hash - # Backport ruby 2.5 yield_self method - def yield_self - yield(self) - end - + class Base < JsonschemaSerializer::FutureHash class << self - # Default Hash structure. This should be implemented in + # Default FutureHash structure. This should be implemented in # every subclass def default_hash - {} + FutureHash.new end - # Initialize an empty object with the default hash attributes - def empty(**opts) - new - .yield_self { |h| h.merge(default_hash) } + # Default Hash structure merged with class attributes and instance options + def processed_hash(**opts) + default_hash .yield_self { |h| class_attributes.reduce(h) { |h1, a| h1.merge(a) } } .merge(opts) end - # Initialize an empty object with name as key - def named(name, **opts) - new - .yield_self do |h| - h.merge(name => empty(**opts)) + # Initialize a new object with the default hash attributes + def new(**opts) + super.yield_self do |h| + if !@name.nil? + # Merge with name and reset it after the creation + h.merge(@name => processed_hash(**opts)).tap { @name = nil } + else + h.merge(processed_hash(**opts)) end + end + end + + # Initialize a new object with name as key + def named(name, **opts) + @name = name + new(**opts) end # Allowed class attributes declaration @@ -77,18 +89,17 @@ class Array < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :array } + FutureHash[{ type: :array }] end - # Default empty array + # Default new array # # Params: # +items+:: an object representation or an array of objects # - def empty(items:, **opts) - super(**opts) - .yield_self { |h| h.merge(items: items) } + def new(items:, **opts) + super(items: items, **opts) end end end @@ -98,7 +109,7 @@ class Boolean < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :boolean } + FutureHash[{ type: :boolean }] end end end @@ -108,7 +119,7 @@ class Integer < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :integer } + FutureHash[{ type: :integer }] end end end @@ -118,7 +129,7 @@ class Number < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :number } + FutureHash[{ type: :number }] end end end @@ -128,7 +139,7 @@ class Object < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :object, properties: {} } + FutureHash[{ type: :object, properties: {} }] end end end @@ -138,7 +149,7 @@ class String < JsonschemaSerializer::Types::Base class << self # Default Hash structure def default_hash - { type: :string } + FutureHash[{ type: :string }] end end end diff --git a/spec/jsonschema_serializer/builder_spec.rb b/spec/jsonschema_serializer/builder_spec.rb index e959339..0f9d39a 100644 --- a/spec/jsonschema_serializer/builder_spec.rb +++ b/spec/jsonschema_serializer/builder_spec.rb @@ -219,16 +219,6 @@ end end - describe 'dsl for empty types' do - subject { builder.new } - - it { expect(subject._boolean).to eq(JsonschemaSerializer::Types::Boolean.empty) } - it { expect(subject._integer).to eq(JsonschemaSerializer::Types::Integer.empty) } - it { expect(subject._number).to eq(JsonschemaSerializer::Types::Number.empty) } - it { expect(subject._object).to eq(JsonschemaSerializer::Types::Object.empty) } - it { expect(subject._string).to eq(JsonschemaSerializer::Types::String.empty) } - end - context '.to_json' do let(:instance) { builder.new } diff --git a/spec/jsonschema_serializer/typed_builder_spec.rb b/spec/jsonschema_serializer/typed_builder_spec.rb index 6c8ff3b..3a41fb4 100644 --- a/spec/jsonschema_serializer/typed_builder_spec.rb +++ b/spec/jsonschema_serializer/typed_builder_spec.rb @@ -222,11 +222,11 @@ describe 'dsl for empty types' do subject { builder.new } - it { expect(subject._boolean).to eq(JsonschemaSerializer::Types::Boolean.empty) } - it { expect(subject._integer).to eq(JsonschemaSerializer::Types::Integer.empty) } - it { expect(subject._number).to eq(JsonschemaSerializer::Types::Number.empty) } - it { expect(subject._object).to eq(JsonschemaSerializer::Types::Object.empty) } - it { expect(subject._string).to eq(JsonschemaSerializer::Types::String.empty) } + it { expect(subject._boolean).to eq(JsonschemaSerializer::Types::Boolean.new) } + it { expect(subject._integer).to eq(JsonschemaSerializer::Types::Integer.new) } + it { expect(subject._number).to eq(JsonschemaSerializer::Types::Number.new) } + it { expect(subject._object).to eq(JsonschemaSerializer::Types::Object.new) } + it { expect(subject._string).to eq(JsonschemaSerializer::Types::String.new) } end context '.to_json' do diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index 20d0f23..58f8516 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -7,9 +7,9 @@ it { subject.respond_to?(:default_hash) } it { expect(subject.default_hash).to eq({}) } - it { subject.respond_to?(:empty) } - it { expect(subject.empty).to eq({}) } - it { expect(subject.empty(a: 1)).to eq(a: 1) } + it { subject.respond_to?(:new) } + it { expect(subject.new).to eq({}) } + it { expect(subject.new(a: 1)).to eq(a: 1) } it { subject.respond_to?(:named) } it { expect(subject.named(:foo)).to eq(foo: {}) } @@ -40,8 +40,8 @@ class PartialSerializer < JsonschemaSerializer::Types::Base .to match_array([{ title: 'a title' }]) end - it 'PartialSerializer.empty' do - expect(PartialSerializer.empty).to eq(title: 'a title') + it 'PartialSerializer.new' do + expect(PartialSerializer.new).to eq(title: 'a title') end end end @@ -53,10 +53,10 @@ class PartialSerializer < JsonschemaSerializer::Types::Base it { expect(subject.default_hash).to eq(type: :array) } it 'needs items key' do - expect { subject.empty }.to raise_error(ArgumentError, /items/) + expect { subject.new }.to raise_error(ArgumentError, /items/) end - it { expect(subject.empty(items: {})).to eq(type: :array, items: {}) } + it { expect(subject.new(items: {})).to eq(type: :array, items: {}) } it do expect(subject.named(:foo, items: {})).to eq( foo: { type: :array, items: {} } @@ -75,8 +75,8 @@ class PartialSerializer < JsonschemaSerializer::Types::Base it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } it { expect(subject.default_hash).to eq(type: :boolean) } - it { expect(subject.empty).to eq(type: :boolean) } - it { expect(subject.empty(a: 1)).to eq(type: :boolean, a: 1) } + it { expect(subject.new).to eq(type: :boolean) } + it { expect(subject.new(a: 1)).to eq(type: :boolean, a: 1) } it { expect(subject.named(:foo)).to eq(foo: { type: :boolean }) } it do expect(subject.named(:foo, bar: :baz)).to eq( @@ -92,7 +92,7 @@ class BooleanSerializer < JsonschemaSerializer::Types::Boolean end it 'should include class level attribute declaration' do - expect(BooleanSerializer.empty).to eq( + expect(BooleanSerializer.new).to eq( title: 'BooleanSerializer', description: 'a dummy serializer for testing purposes', default: true, @@ -107,8 +107,8 @@ class BooleanSerializer < JsonschemaSerializer::Types::Boolean it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } it { expect(subject.default_hash).to eq(type: :integer) } - it { expect(subject.empty).to eq(type: :integer) } - it { expect(subject.empty(a: 1)).to eq(type: :integer, a: 1) } + it { expect(subject.new).to eq(type: :integer) } + it { expect(subject.new(a: 1)).to eq(type: :integer, a: 1) } it { expect(subject.named(:foo)).to eq(foo: { type: :integer }) } it do expect(subject.named(:foo, bar: :baz)).to eq( @@ -122,8 +122,8 @@ class BooleanSerializer < JsonschemaSerializer::Types::Boolean it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } it { expect(subject.default_hash).to eq(type: :number) } - it { expect(subject.empty).to eq(type: :number) } - it { expect(subject.empty(a: 1)).to eq(type: :number, a: 1) } + it { expect(subject.new).to eq(type: :number) } + it { expect(subject.new(a: 1)).to eq(type: :number, a: 1) } it { expect(subject.named(:foo)).to eq(foo: { type: :number }) } it do expect(subject.named(:foo, bar: :baz)).to eq( @@ -137,8 +137,8 @@ class BooleanSerializer < JsonschemaSerializer::Types::Boolean it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } it { expect(subject.default_hash).to eq(type: :object, properties: {}) } - it { expect(subject.empty).to eq(type: :object, properties: {}) } - it { expect(subject.empty(a: 1)).to eq(type: :object, properties: {}, a: 1) } + it { expect(subject.new).to eq(type: :object, properties: {}) } + it { expect(subject.new(a: 1)).to eq(type: :object, properties: {}, a: 1) } it { expect(subject.named(:foo)).to eq(foo: { type: :object, properties: {} }) } it do expect(subject.named(:foo, bar: :baz)).to eq( @@ -154,7 +154,7 @@ class ObjectSerializer < JsonschemaSerializer::Types::Object end it 'should include class level attribute declaration' do - expect(ObjectSerializer.empty).to eq( + expect(ObjectSerializer.new).to eq( title: 'ObjectSerializer', description: 'a dummy serializer for testing purposes', default: '{}', @@ -170,8 +170,8 @@ class ObjectSerializer < JsonschemaSerializer::Types::Object it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } it { expect(subject.default_hash).to eq(type: :string) } - it { expect(subject.empty).to eq(type: :string) } - it { expect(subject.empty(a: 1)).to eq(type: :string, a: 1) } + it { expect(subject.new).to eq(type: :string) } + it { expect(subject.new(a: 1)).to eq(type: :string, a: 1) } it { expect(subject.named(:foo)).to eq(foo: { type: :string }) } it do expect(subject.named(:foo, bar: :baz)).to eq( From 798fc5448f02a6b5341ade2b7310637cfdf9ee6d Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Fri, 13 Jul 2018 00:53:52 +0200 Subject: [PATCH 09/10] chore() wip build broken --- lib/jsonschema_serializer/typed_builder.rb | 28 ++++++---------------- lib/jsonschema_serializer/types.rb | 12 ++++++++++ spec/jsonschema_serializer/types_spec.rb | 3 +++ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/jsonschema_serializer/typed_builder.rb b/lib/jsonschema_serializer/typed_builder.rb index 437a286..8ec0161 100644 --- a/lib/jsonschema_serializer/typed_builder.rb +++ b/lib/jsonschema_serializer/typed_builder.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'json' require_relative 'types' module JsonschemaSerializer # The +JsonschemaSerializer::Builder+ class provides # an effective DSL to generate a valid json schema. - class TypedBuilder + class TypedBuilder < JsonschemaSerializer::Types::Object class << self # The +build+ class method create a new instance and applies the block def build @@ -16,23 +15,10 @@ def build end end - # An hash representation of the +schema+ - attr_reader :schema - # The +new+ method creates assigns a new object # to a +schema+ instance variable - def initialize - @schema ||= JsonschemaSerializer::Types::Object.new - end - - # The +to_json+ method exports the schema as a json string - # By default it would exported with a pretty print - # - # Params: - # +pretty+:: +Boolean+ - - def to_json(pretty: true) - pretty ? JSON.pretty_generate(@schema) : @schema.to_json + def schema + self end # Assigns the +title+ to the root schema object @@ -41,7 +27,7 @@ def to_json(pretty: true) # +title+:: +String+ or +Symbol+ title field of the schema object def title(title) - @schema[:title] = title + schema[:title] = title end # Assigns the +description+ to the root schema object @@ -50,7 +36,7 @@ def title(title) # +description+:: +String+ description field of the schema object def description(description) - @schema[:description] = description + schema[:description] = description end # The +required+ method allows to provide a list of required properties @@ -59,7 +45,7 @@ def description(description) # +required+ [Array[String, Symbol]] def required(*required) - @schema[:required] = required + schema[:required] = required end # The +properties+ method allows to access object properties @@ -72,7 +58,7 @@ def required(*required) # end def properties - @schema[:properties] ||= {} + schema[:properties] ||= {} end # A base representation of the +boolean+ type. diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index e90610a..52f873e 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json' + module JsonschemaSerializer # +Hash+ class with some extended methods class FutureHash < Hash @@ -7,6 +9,16 @@ class FutureHash < Hash def yield_self yield(self) end + + # The +to_json+ method exports the schema as a json string + # By default it would exported with a pretty print + # + # Params: + # +pretty+:: +Boolean+ + + def to_json(pretty: true) + pretty ? JSON.pretty_generate(self.dup) : super + end end # This module contains types declarations diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index 58f8516..8ddd210 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -51,7 +51,10 @@ class PartialSerializer < JsonschemaSerializer::Types::Base it { expect(subject.superclass).to eq(JsonschemaSerializer::Types::Base) } it { expect(subject.default_hash).to eq(type: :array) } + it { expect(subject.new(items: {}).to_json).to eq(type: :array) } + let(:name) { :abc } + it { expect(subject.named(name, items: {})).to eq(name => {type: :array, items: {}}) } it 'needs items key' do expect { subject.new }.to raise_error(ArgumentError, /items/) end From adfccea93c31e74054ef633aa00bc551946b5af6 Mon Sep 17 00:00:00 2001 From: Mauro Berlanda Date: Tue, 10 Jul 2018 18:30:07 +0200 Subject: [PATCH 10/10] feat(): implemented required and properties methods on object type --- lib/jsonschema_serializer/error.rb | 10 ++++++++ lib/jsonschema_serializer/types.rb | 32 +++++++++++++++++++++++- spec/jsonschema_serializer/error_spec.rb | 8 ++++++ spec/jsonschema_serializer/types_spec.rb | 11 +++++++- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/jsonschema_serializer/error.rb b/lib/jsonschema_serializer/error.rb index 977bd03..4531837 100644 --- a/lib/jsonschema_serializer/error.rb +++ b/lib/jsonschema_serializer/error.rb @@ -10,4 +10,14 @@ def initialize(msg = DEFAULT_MESSAGE) super(msg) end end + + # +DuplicatedObjectPropertyError+ is an +ArgumentError+ + class DuplicatedObjectPropertyError < ArgumentError + # Default message for +DuplicatedObjectPropertyError+ instance + DEFAULT_MESSAGE = 'Duplicated declaration for object properties' + + def initialize(msg = DEFAULT_MESSAGE) + super(msg) + end + end end diff --git a/lib/jsonschema_serializer/types.rb b/lib/jsonschema_serializer/types.rb index 52f873e..10ffbcd 100644 --- a/lib/jsonschema_serializer/types.rb +++ b/lib/jsonschema_serializer/types.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'json' +require_relative 'error' module JsonschemaSerializer # +Hash+ class with some extended methods @@ -153,6 +153,36 @@ class << self def default_hash FutureHash[{ type: :object, properties: {} }] end + + # Allowed class attributes declaration + def allowed_class_attributes + super.concat([@required, @properties]) + end + + # The +required+ method allows to provide a list of required properties + # + # params: + # +required+ [Array[String, Symbol]] + + def required(*required) + @required = { required: required } + end + + # The +properties+ method allows to access object properties + # + # e.g.: + # class CustomObject < JsonschemaSerializer::Types::Object + # properties( + # JsonschemaSerializer::Types::String.named(:foo), + # JsonschemaSerializer::Types::Boolean.named(:bar) + # ) + # end + + def properties(*properties) + raise JsonschemaSerializer::DuplicatedObjectPropertyError if @properties + props = properties.reduce({}) { |h, prop| h.merge(prop) } + @properties = { properties: props } + end end end diff --git a/spec/jsonschema_serializer/error_spec.rb b/spec/jsonschema_serializer/error_spec.rb index 292fe27..415df06 100644 --- a/spec/jsonschema_serializer/error_spec.rb +++ b/spec/jsonschema_serializer/error_spec.rb @@ -8,4 +8,12 @@ end.to raise_error(ArgumentError) end end + + context 'DuplicatedObjectPropertyError' do + it 'should be an ArgumentError' do + expect do + raise JsonschemaSerializer::DuplicatedObjectPropertyError, 'a custom message' + end.to raise_error(ArgumentError) + end + end end diff --git a/spec/jsonschema_serializer/types_spec.rb b/spec/jsonschema_serializer/types_spec.rb index 8ddd210..e191233 100644 --- a/spec/jsonschema_serializer/types_spec.rb +++ b/spec/jsonschema_serializer/types_spec.rb @@ -154,6 +154,11 @@ class ObjectSerializer < JsonschemaSerializer::Types::Object title 'ObjectSerializer' description 'a dummy serializer for testing purposes' default '{}' + required :a, :b + properties( + JsonschemaSerializer::Types::Boolean.named(:a), + JsonschemaSerializer::Types::String.named(:b) + ) end it 'should include class level attribute declaration' do @@ -162,7 +167,11 @@ class ObjectSerializer < JsonschemaSerializer::Types::Object description: 'a dummy serializer for testing purposes', default: '{}', type: :object, - properties: {} + properties: { + a: { type: :boolean }, + b: { type: :string } + }, + required: [:a, :b] ) end end