Skip to content

Commit

Permalink
fixes that properties with custom types includes their constraints ha…
Browse files Browse the repository at this point in the history
…sh (#26)

* fixes that properties with custom types includes their constraints hash

* release prep

* releasing manually for now

* rubocop fixes
  • Loading branch information
sergiobayona authored Jan 9, 2025
1 parent c502d02 commit 0cecd50
Show file tree
Hide file tree
Showing 12 changed files with 79 additions and 106 deletions.
28 changes: 0 additions & 28 deletions .github/workflows/gem-release.yml

This file was deleted.

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## [1.0.1] - 2024-09-01
- Fixed that property with custom type does not ignore the constraints hash https://github.com/sergiobayona/easy_talk/issues/17
## [1.0.0] - 2024-06-01
- Use `Hash` instead of `:object` for inline object schema definition.
example:
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ source 'https://rubygems.org'

gemspec

gem "dartsass-rails", ">= 0.5.0"
gem 'dartsass-rails', '>= 0.5.0'
10 changes: 5 additions & 5 deletions lib/easy_talk/property.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ def initialize(name, type = nil, constraints = {})
#
# @return [Object] The built property.
def build
# return type.respond_to?(:schema) ? type.schema : 'object' unless builder

# args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
# builder.new(*args).build
if builder
args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
builder.new(*args).build
elsif type.respond_to?(:schema)
# merge the top-level constraints from *this* property
# e.g. :title, :description, :default, etc
type.schema.merge!(constraints)
else
type.respond_to?(:schema) ? type.schema : 'object'
'object'
end
end

Expand Down
9 changes: 5 additions & 4 deletions lib/easy_talk/schema_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

module EasyTalk
class InvalidPropertyNameError < StandardError; end

#
#= EasyTalk \SchemaDefinition
# SchemaDefinition provides the methods for defining a schema within the define_schema block.
# The @schema is a hash that contains the unvalidated schema definition for the model.
# A SchemaDefinition instanace is the passed to the Builder.build_schema method to validate and compile the schema.
class SchemaDefinition

extend T::Sig
extend T::AnyOf
extend T::OneOf
Expand Down Expand Up @@ -56,9 +56,10 @@ def property(name, type, constraints = {}, &blk)
end

def validate_property_name(name)
unless name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)
raise InvalidPropertyNameError, "Invalid property name '#{name}'. Must start with letter/underscore and contain only letters, numbers, underscores"
end
return if name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)

raise InvalidPropertyNameError,
"Invalid property name '#{name}'. Must start with letter/underscore and contain only letters, numbers, underscores"
end

def optional?
Expand Down
2 changes: 1 addition & 1 deletion lib/easy_talk/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module EasyTalk
VERSION = '1.0.0'
VERSION = '1.0.1'
end
46 changes: 22 additions & 24 deletions spec/easy_talk/builders/boolean_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,55 +37,53 @@

it 'combines multiple constraints' do
builder = described_class.new(:active,
title: 'Account Status',
description: 'Whether the account is active',
default: true,
enum: [true, false]
)
title: 'Account Status',
description: 'Whether the account is active',
default: true,
enum: [true, false])

expect(builder.build).to eq({
type: 'boolean',
title: 'Account Status',
description: 'Whether the account is active',
default: true,
enum: [true, false]
})
type: 'boolean',
title: 'Account Status',
description: 'Whether the account is active',
default: true,
enum: [true, false]
})
end
end

context 'with invalid configurations' do
it 'raises ArgumentError for unknown constraints' do
expect {
expect do
described_class.new(:active, invalid_option: 'value').build
}.to raise_error(ArgumentError, /Unknown key/)
end.to raise_error(ArgumentError, /Unknown key/)
end

it 'raises TypeError when enum contains non-boolean values' do
expect {
expect do
described_class.new(:active, enum: [true, 'false']).build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when default is not a boolean' do
expect {
expect do
described_class.new(:active, default: 'true').build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when enum is not an array' do
expect {
expect do
described_class.new(:active, enum: 'true,false').build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end
end

context 'with nil values' do
it 'excludes constraints with nil values' do
builder = described_class.new(:active,
default: nil,
enum: nil,
description: nil
)
default: nil,
enum: nil,
description: nil)
expect(builder.build).to eq({ type: 'boolean' })
end
end
Expand All @@ -98,7 +96,7 @@

it 'excludes optional flag when false' do
builder = described_class.new(:subscribed, optional: false)
expect(builder.build).to eq({ type: 'boolean', optional: false})
expect(builder.build).to eq({ type: 'boolean', optional: false })
end
end

Expand Down
72 changes: 35 additions & 37 deletions spec/easy_talk/builders/string_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
end

it 'includes enum constraint' do
builder = described_class.new(:status, enum: ['active', 'inactive', 'pending'])
expect(builder.build).to eq({ type: 'string', enum: ['active', 'inactive', 'pending'] })
builder = described_class.new(:status, enum: %w[active inactive pending])
expect(builder.build).to eq({ type: 'string', enum: %w[active inactive pending] })
end

it 'includes const constraint' do
Expand All @@ -57,95 +57,93 @@

it 'combines multiple constraints' do
builder = described_class.new(:password,
min_length: 8,
max_length: 32,
pattern: '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$',
description: 'Must contain letters and numbers'
)
min_length: 8,
max_length: 32,
pattern: '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$',
description: 'Must contain letters and numbers')

expect(builder.build).to eq({
type: 'string',
minLength: 8,
maxLength: 32,
pattern: '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$',
description: 'Must contain letters and numbers'
})
type: 'string',
minLength: 8,
maxLength: 32,
pattern: '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$',
description: 'Must contain letters and numbers'
})
end
end

context 'with invalid configurations' do
it 'raises ArgumentError for unknown constraints' do
expect {
expect do
described_class.new(:name, invalid_option: 'value').build
}.to raise_error(ArgumentError, /Unknown key: :invalid_option/)
end.to raise_error(ArgumentError, /Unknown key: :invalid_option/)
end

it 'raises TypeError when format is not a string' do
expect {
expect do
described_class.new(:email, format: 123).build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when pattern is not a string' do
expect {
expect do
described_class.new(:zip, pattern: 123).build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when minLength is not an integer' do
expect {
expect do
described_class.new(:name, min_length: '8').build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when maxLength is not an integer' do
expect {
expect do
described_class.new(:name, max_length: '20').build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when enum contains non-string values' do
expect {
expect do
described_class.new(:status, enum: ['active', 123, 'pending']).build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises TypeError when const is not a string' do
expect {
expect do
described_class.new(:type, const: 123).build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end
end

context 'with nil values' do
it 'excludes constraints with nil values' do
builder = described_class.new(:name,
min_length: nil,
max_length: nil,
pattern: nil,
format: nil
)
min_length: nil,
max_length: nil,
pattern: nil,
format: nil)
expect(builder.build).to eq({ type: 'string' })
end
end

context 'with empty values on lenght validators' do
it 'raises a type error' do
builder = described_class.new(:name, min_length: '')
expect {
expect do
builder.build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end

it 'raises a type error' do
builder = described_class.new(:name, max_length: '')
expect {
expect do
builder.build
}.to raise_error(TypeError)
end.to raise_error(TypeError)
end
end

context "with empty values on pattern" do
context 'with empty values on pattern' do
it 'returns empty pattern' do
# this is invalid in json schema but there is not practical way to validate non empty strings.
builder = described_class.new(:name, pattern: '')
Expand Down
4 changes: 3 additions & 1 deletion spec/easy_talk/examples/company_employees_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def self.name
},
"employees": {
"type": 'array',
"title": 'Company Employees',
"description": 'A list of company employees',
"items": {
"type": 'object',
"title": 'Employee',
Expand Down Expand Up @@ -149,7 +151,7 @@ def self.name
Company.define_schema do
title 'Company'
property :name, String
property :employees, T::Array[Employee]
property :employees, T::Array[Employee], title: 'Company Employees', description: 'A list of company employees'
end

expect(company.json_schema).to include_json(expected_json_schema)
Expand Down
7 changes: 4 additions & 3 deletions spec/easy_talk/examples/company_owner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,20 @@ def self.name
Company.define_schema do
title 'Company'
property :name, String
property :owner, Owner
property :owner, Owner, title: 'Owner', description: 'The company owner'
end


expected_schema = {
'type' => 'object',
'title' => 'Company',
'properties' => {
'name' => {
'type' => 'string'
},
},
'owner' => {
'type' => 'object',
'title' => 'Owner',
'description' => 'The company owner',
'properties' => {
'first_name' => {
'type' => 'string'
Expand Down
2 changes: 1 addition & 1 deletion spec/easy_talk/property_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def self.name
})
end

pending 'returns a custom class type with options' do
it 'returns a custom class type with options' do
prop = described_class.new(:name, custom_class, title: 'Custom Class', description: 'some description').as_json
expect(prop).to include_json({
'type': 'object',
Expand Down
1 change: 0 additions & 1 deletion spec/easy_talk/schema_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def self.name

pending 'errors on invalid age' do
jim = user.new(name: 'Jim', age: 'thirty', height: 4.5, email: { address: '[email protected]', verified: 'true' })
# binding.pry
expect(jim.valid?).to be false
expect(jim.errors.size).to eq(1)
expect(jim.errors[:age]).to eq(['is not a valid integer'])
Expand Down

0 comments on commit 0cecd50

Please sign in to comment.