Skip to content

Commit

Permalink
Merge pull request #16 from sergiobayona/use_activemodel
Browse files Browse the repository at this point in the history
Use activemodel
  • Loading branch information
sergiobayona authored May 4, 2024
2 parents 3d1afdd + 072dd13 commit ea34070
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 52 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [0.2.0] - 2024-05-01
- Added ActiveModel::API functionality to EasyTalk::Model module. That means you get all the benefits of ActiveModel::API including attribute assignment, introspections, validations, translation (i18n) and more. See https://api.rubyonrails.org/classes/ActiveModel/API.html for more information.

## [0.1.10] - 2024-04-29
- Accept `:optional` key as constraint which excludes property from required node.
- Spec fixes
Expand Down
2 changes: 1 addition & 1 deletion easy_talk.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Gem::Specification.new do |spec|

spec.add_dependency 'activemodel', '~> 7.0'
spec.add_dependency 'activesupport', '~> 7.0'
spec.add_dependency 'json-schema', '~> 4'
spec.add_dependency 'json_schemer'
spec.add_dependency 'sorbet-runtime', '~> 0.5'
spec.add_development_dependency 'pry-byebug', '>= 3.10'
spec.add_development_dependency 'rake', '~> 13.1'
Expand Down
79 changes: 39 additions & 40 deletions lib/easy_talk/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,55 @@
require 'active_support/concern'
require 'active_support/json'
require 'active_model'
require 'json-schema'
require 'json_schemer'
require_relative 'schema_errors_mapper'
require_relative 'builders/object_builder'
require_relative 'schema_definition'

module EasyTalk
# The Model module can be included in a class to add JSON schema definition and generation support.
# The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
#
# It includes methods for defining the schema, retrieving the schema definition,
# and generating the JSON schema for the model.
#
# Example usage:
#
# class Person
# include EasyTalk::Model
#
# define_schema do
# property :name, String, description: 'The person\'s name'
# property :age, Integer, description: 'The person\'s age'
# end
# end
#
# Person.json_schema #=> returns the JSON schema for Person
# jim = Person.new(name: 'Jim', age: 30)
# jim.valid? #=> returns true
#
# @see SchemaDefinition
module Model
# The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
#
# It includes methods for defining the schema, retrieving the schema definition,
# and generating the JSON schema for the model.
#
# Example usage:
#
# class Person
# include EasyTalk::Model
#
# define_schema do
# property :name, String, description: 'The person\'s name'
# property :age, Integer, description: 'The person\'s age'
# end
# end
#
# Person.json_schema #=> returns the JSON schema for Person
# jim = Person.new(name: 'Jim', age: 30)
# jim.valid? #=> returns true
#
# @see SchemaDefinition
#
def self.included(base)
base.include ActiveModel::API # Include ActiveModel::API in the class including EasyTalk::Model
base.include ActiveModel::Validations
base.extend ActiveModel::Callbacks
base.validates_with SchemaValidator
base.extend(ClassMethods)
end

# Checks if the model is valid.
#
# This method calls the `validate_json` class method on the current class,
# passing the `properties` as the argument.
#
# @return [Boolean] true if the model is valid, false otherwise.
def valid?
self.class.validate_json(properties)
class SchemaValidator < ActiveModel::Validator
def validate(record)
result = schema_validation(record)
result.errors.each do |key, error_msg|
record.errors.add key.to_sym, error_msg
end
end

def schema_validation(record)
schema = JSONSchemer.schema(record.class.json_schema)
errors = schema.validate(record.properties)
SchemaErrorsMapper.new(errors)
end
end

# Returns the properties of the model as a hash with symbolized keys.
Expand Down Expand Up @@ -94,14 +101,6 @@ def properties
end
end

# Validates the given JSON against the model's JSON schema.
#
# @param json [Hash] The JSON to validate.
# @return [Boolean] `true` if the JSON is valid, `false` otherwise.
def validate_json(json)
JSON::Validator.validate(json_schema, json)
end

# Returns the JSON schema for the model.
#
# @return [Hash] The JSON schema for the model.
Expand Down
21 changes: 21 additions & 0 deletions lib/easy_talk/schema_errors_mapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module EasyTalk
class SchemaErrorsMapper
def initialize(errors)
@errors = errors.to_a
end

def errors
@errors.each_with_object({}) do |error, hash|
if error['data_pointer'].present?
key = error['data_pointer'].split('/').compact_blank.join('.')
hash[key] = error['error']
else
error['details']['missing_keys'].each do |missing_key|
message = "#{error['error'].split(':').first}: #{missing_key}"
hash[missing_key] = message
end
end
end
end
end
end
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 = '0.1.10'
VERSION = '0.2.0'
end
67 changes: 67 additions & 0 deletions spec/easy_talk/activemodel_integration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'validing json' do
let(:user) do
Class.new do
include EasyTalk::Model

validates :age, comparison: { greater_than: 21 }

validate do |person|
MyValidator.new(person).validate
end

class MyValidator
def initialize(person)
@person = person
end

def validate
@person.errors.add(:email, 'must end with @test.com') unless @person.email[:address].ends_with?('@test.com')
end
end

def self.name
'User'
end

define_schema do
property :name, String
property :age, Integer
property :height, Float
property :email, :object do
property :address, String
property :verified, T::Boolean
end
end
end
end

it 'errors on missing email' do
jim = user.new(name: 'Jim', age: 30, height: 5.9, email: { address: '[email protected]', verified: false })
expect(jim.valid?).to be true
end

it 'errors on invalid age' do
jim = user.new(name: 'Jim', age: 18, height: 5.9, email: { address: '[email protected]', verified: false })
expect(jim.valid?).to be false
expect(jim.errors.size).to eq(1)
expect(jim.errors[:age]).to eq(['must be greater than 21'])
end

it 'errors on invalid email' do
jim = user.new(name: 'Jim', age: 30, height: 5.9, email: { address: '[email protected]', verified: false })
expect(jim.valid?).to be false
expect(jim.errors.size).to eq(1)
expect(jim.errors[:email]).to eq(['must end with @test.com'])
end

it 'runs after validation callback' do
jim = user.new(name: 'Jim', age: 30, height: 5.9, email: { address: '[email protected]', verified: false })
expect(jim.age).to eq(30)
expect(jim.valid?).to be true
expect(jim.age).to eq(500)
end
end
10 changes: 0 additions & 10 deletions spec/easy_talk/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,6 @@ def self.name
end
end

describe 'validating a JSON object' do
it 'validates the JSON object against the schema' do
expect(user.validate_json({ name: 'John', age: 21, email: { address: '[email protected]', verified: 'false' } })).to eq(true)
end

it 'fails validation of the JSON object against the schema' do
expect(user.validate_json({ name: 'John', age: '21' })).to eq(false)
end
end

context 'when the class name is nil' do
let(:user) do
Class.new do
Expand Down
61 changes: 61 additions & 0 deletions spec/easy_talk/schema_validation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'validing json' do
let(:user) do
Class.new do
include EasyTalk::Model

def self.name
'User'
end

define_schema do
property :name, String
property :age, Integer
property :height, Float
property :email, :object do
property :address, String
property :verified, String
end
end
end
end

it 'errors on missing email' do
jim = user.new(name: 'Jim', age: 30, height: 5.9)
expect(jim.valid?).to be false
expect(jim.errors.size).to eq(1)
expect(jim.errors[:email]).to eq(['object at root is missing required properties: email'])
end

it 'errors on invalid age, missing email' do
jim = user.new(name: 'Jim', age: 'thirty', height: 4.5, email: { address: '[email protected]', verified: 'true' })
expect(jim.valid?).to be false
expect(jim.errors.size).to eq(1)
expect(jim.errors[:age]).to eq(['value at `/age` is not an integer'])
end

it 'errors on missing age email and height' do
jim = user.new(name: 'Jim', email: { address: '[email protected]', verified: 'true' })
expect(jim.valid?).to be false
expect(jim.errors.size).to eq(2)
expect(jim.errors[:age]).to eq(['object at root is missing required properties: age'])
expect(jim.errors[:height]).to eq(['object at root is missing required properties: height'])
end

it 'errors on invalid name, email and age' do
jim = user.new(name: nil, email: 'test@jim', age: 'thirty')
expect(jim.valid?).to be false
expect(jim.errors[:name]).to eq(['value at `/name` is not a string'])
expect(jim.errors[:email]).to eq(['value at `/email` is not an object'])
expect(jim.errors[:age]).to eq(['value at `/age` is not an integer'])
end

it 'errors on verified' do
jim = user.new(name: 'Jim', email: { address: '[email protected]', verified: false }, age: 21, height: 5.9)
expect(jim.valid?).to be(false)
expect(jim.errors['email.verified']).to eq(['value at `/email/verified` is not a string'])
end
end

0 comments on commit ea34070

Please sign in to comment.