Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes that properties with custom types includes their constraints hash #26

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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