diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa63f9..e90f526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. +# [0.6.0] - 2021-02-05 +### Added +- New types of `SmartCore::Types::Struct` category: + - `SmartCore::Types::Struct::StrictHash` (`Hash` with nested types validation); + # [0.5.0] - 2021-01-28 ### Added - New types of `SmartCore::Types::Variadic` category: diff --git a/Gemfile.lock b/Gemfile.lock index 9a7d32c..85c6788 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - smart_types (0.5.0) + smart_types (0.6.0) smart_engine (~> 0.11) GEM @@ -89,6 +89,7 @@ GEM zeitwerk (2.4.2) PLATFORMS + x86_64-darwin-18 x86_64-darwin-19 x86_64-darwin-20 diff --git a/README.md b/README.md index eba4d1c..ff80640 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,20 @@ SmartCore::Types::Protocol::InstanceOf(::Time, ::DateTime, ::Date) # time or dat --- +#### Struct: + +```ruby +SmartCore::Types::Protocol::Struct +``` + +```ruby +# examples (SmartCore::Types::Struct::StrictHash): +SmartCore::Types::Struct::StrictHash(int: SmartCore::Types::Value::Integer, str: SmartCore::Types::Value::String) # hash with the following schema: {int: , str: } +SmartCore::Types::Struct::StrictHash(klass: SmartCore::Types::Protocol::InstanceOf(::Class)) # hash with the following schema: {klass: } +``` + +--- + #### Variadic: ```ruby diff --git a/lib/smart_core/types.rb b/lib/smart_core/types.rb index d930a95..1922bed 100644 --- a/lib/smart_core/types.rb +++ b/lib/smart_core/types.rb @@ -4,12 +4,14 @@ # @api public # @since 0.1.0 +# @version 0.6.0 module SmartCore::Types - require_relative 'types/version' require_relative 'types/errors' - require_relative 'types/system' require_relative 'types/primitive' - require_relative 'types/value' require_relative 'types/protocol' + require_relative 'types/struct' + require_relative 'types/system' + require_relative 'types/value' require_relative 'types/variadic' + require_relative 'types/version' end diff --git a/lib/smart_core/types/struct.rb b/lib/smart_core/types/struct.rb index 5b15057..dbbedec 100644 --- a/lib/smart_core/types/struct.rb +++ b/lib/smart_core/types/struct.rb @@ -2,5 +2,7 @@ # @api public # @since 0.1.0 +# @version 0.6.0 class SmartCore::Types::Struct < SmartCore::Types::Primitive + require_relative 'struct/strict_hash' end diff --git a/lib/smart_core/types/struct/strict_hash.rb b/lib/smart_core/types/struct/strict_hash.rb new file mode 100644 index 0000000..e85e34b --- /dev/null +++ b/lib/smart_core/types/struct/strict_hash.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +using SmartCore::Ext::BasicObjectAsObject + +# @api public +# @since 0.6.0 +SmartCore::Types::Struct.define_type(:StrictHash) do |type| + type.runtime_attributes_checker do |runtime_attrs| + schema = runtime_attrs.first + + next false unless runtime_attrs.size == 1 + next false unless schema.is_a?(::Hash) + + schema.all? { |_key, value| value.is_a?(SmartCore::Types::Primitive) } + end + + type.define_checker do |value, schema_values| + schema = schema_values.first + + next false unless SmartCore::Types::Value::Hash.valid?(value) + next false unless value.keys == schema.keys + + value.all? { |k, v| schema[k].valid?(v) } + end +end diff --git a/lib/smart_core/types/version.rb b/lib/smart_core/types/version.rb index 9e7629e..341abfa 100644 --- a/lib/smart_core/types/version.rb +++ b/lib/smart_core/types/version.rb @@ -6,7 +6,7 @@ module Types # # @api public # @since 0.1.0 - # @version 0.4.0 - VERSION = '0.4.0' + # @version 0.6.0 + VERSION = '0.6.0' end end diff --git a/spec/types/struct/strict_hash_spec.rb b/spec/types/struct/strict_hash_spec.rb new file mode 100644 index 0000000..3104f7b --- /dev/null +++ b/spec/types/struct/strict_hash_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +RSpec.describe 'SmartCore::Types::Struct::StrictHash' do + describe 'runtime-based behavior' do + specify 'fails on extra attributes' do + expect do + SmartCore::Types::Struct::StrictHash({ a: SmartCore::Types::Value::String }, :excessive) + end.to raise_error(SmartCore::Types::IncorrectRuntimeAttributesError) + end + + specify 'fails when schema attribute is not hash object' do + expect do + SmartCore::Types::Struct::StrictHash(%i[bad master]) + end.to raise_error(SmartCore::Types::IncorrectRuntimeAttributesError) + end + + specify 'fails when schema values are not types' do + expect do + SmartCore::Types::Struct::StrictHash({ a: String }) + end.to raise_error(SmartCore::Types::IncorrectRuntimeAttributesError) + end + + specify 'requires schema (runtime attribute)' do + expect do + SmartCore::Types::Struct::StrictHash() + end.to raise_error(SmartCore::Types::IncorrectRuntimeAttributesError) + end + end + + describe 'logic' do + let(:correct_value) { Hash[int: 10, str: '20', bool: false] } + + let(:type) do + SmartCore::Types::Struct::StrictHash( + int: SmartCore::Types::Value::Integer, + str: SmartCore::Types::Value::String, + bool: SmartCore::Types::Value::Boolean + ) + end + + specify 'type-checking' do + expect(type.valid?(correct_value)).to eq(true) + + expect(type.valid?(nil)).to eq(false) + expect(type.valid?({})).to eq(false) + expect(type.valid?(correct_value.merge(int: nil))).to eq(false) + expect(type.valid?(correct_value.merge(int: :test))).to eq(false) + expect(type.valid?(correct_value.merge(str: []))).to eq(false) + expect(type.valid?(correct_value.merge(bool: {}))).to eq(false) + end + + specify 'type-validation' do + expect { type.validate!(correct_value) }.not_to raise_error + + expect { type.validate!(nil) }.to raise_error(SmartCore::Types::TypeError) + expect { type.validate!({}) }.to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(int: nil)) } + .to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(int: :test)) } + .to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(str: [])) } + .to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(bool: {})) } + .to raise_error(SmartCore::Types::TypeError) + end + + context 'nilable type' do + let(:type) do + SmartCore::Types::Struct::StrictHash( + int: SmartCore::Types::Value::Integer, + str: SmartCore::Types::Value::String, + bool: SmartCore::Types::Value::Boolean + ).nilable + end + + specify 'type-checking' do + expect(type.valid?(correct_value)).to eq(true) + expect(type.valid?(nil)).to eq(true) + + expect(type.valid?({})).to eq(false) + expect(type.valid?(correct_value.merge(int: nil))).to eq(false) + expect(type.valid?(correct_value.merge(int: :test))).to eq(false) + expect(type.valid?(correct_value.merge(str: []))).to eq(false) + expect(type.valid?(correct_value.merge(bool: {}))).to eq(false) + end + + specify 'type-validation' do + expect { type.validate!(correct_value) }.not_to raise_error + expect { type.validate!(nil) }.not_to raise_error + + expect { type.validate!({}) }.to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(int: nil)) } + .to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(int: :test)) } + .to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(str: [])) } + .to raise_error(SmartCore::Types::TypeError) + expect { type.validate!(correct_value.merge(bool: {})) } + .to raise_error(SmartCore::Types::TypeError) + end + end + end +end