fix: validate template_params for WhatsApp (#6881)
- Add JsonSchemaValidator, which takes a declarative schema and validates it for a given property. - Add specs for JsonSchemaValidator - Enable the validator for template_params
This commit is contained in:
84
app/models/concerns/json_schema_validator.rb
Normal file
84
app/models/concerns/json_schema_validator.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
# This file defines a custom validator class `JsonSchemaValidator` for validating a JSON object against a schema.
|
||||
# To use this validator, define a schema as a Ruby hash and include it in the validation options when validating a model.
|
||||
# The schema should define the expected structure and types of the JSON object, as well as any validation rules.
|
||||
# Here's an example schema:
|
||||
#
|
||||
# schema = {
|
||||
# 'type' => 'object',
|
||||
# 'properties' => {
|
||||
# 'name' => { 'type' => 'string' },
|
||||
# 'age' => { 'type' => 'integer' },
|
||||
# 'is_active' => { 'type' => 'boolean' },
|
||||
# 'tags' => { 'type' => 'array' },
|
||||
# 'address' => {
|
||||
# 'type' => 'object',
|
||||
# 'properties' => {
|
||||
# 'street' => { 'type' => 'string' },
|
||||
# 'city' => { 'type' => 'string' }
|
||||
# },
|
||||
# 'required' => ['street', 'city']
|
||||
# }
|
||||
# },
|
||||
# 'required': ['name', 'age']
|
||||
# }.to_json.freeze
|
||||
#
|
||||
# To validate a model using this schema, include the `JsonSchemaValidator` in the model's validations and pass the schema
|
||||
# as an option:
|
||||
#
|
||||
# class MyModel < ApplicationRecord
|
||||
# validates_with JsonSchemaValidator, schema: schema
|
||||
# end
|
||||
|
||||
class JsonSchemaValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
# Get the attribute resolver function from options or use a default one
|
||||
attribute_resolver = options[:attribute_resolver] || ->(rec) { rec.additional_attributes }
|
||||
|
||||
# Resolve the JSON data to be validated
|
||||
json_data = attribute_resolver.call(record)
|
||||
|
||||
# Get the schema to be used for validation
|
||||
schema = options[:schema]
|
||||
|
||||
# Create a JSONSchemer instance using the schema
|
||||
schemer = JSONSchemer.schema(schema)
|
||||
|
||||
# Validate the JSON data against the schema
|
||||
validation_errors = schemer.validate(json_data)
|
||||
|
||||
# Add validation errors to the record with a formatted statement
|
||||
validation_errors.each do |error|
|
||||
# byebug
|
||||
format_and_append_error(error, record)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def format_and_append_error(error, record)
|
||||
return handle_required(error, record) if error['type'] == 'required'
|
||||
|
||||
type = error['type'] == 'object' ? 'hash' : error['type']
|
||||
|
||||
handle_type(error, record, type)
|
||||
end
|
||||
|
||||
def handle_required(error, record)
|
||||
missing_values = error['details']['missing_keys']
|
||||
missing_values.each do |missing|
|
||||
record.errors.add(missing, 'is required')
|
||||
end
|
||||
end
|
||||
|
||||
def handle_type(error, record, expected_type)
|
||||
data = get_name_from_data_pointer(error)
|
||||
record.errors.add(data, "must be of type #{expected_type}")
|
||||
end
|
||||
|
||||
def get_name_from_data_pointer(error)
|
||||
data = error['data_pointer']
|
||||
|
||||
# if data starts with a "/" remove it
|
||||
data[1..] if data[0] == '/'
|
||||
end
|
||||
end
|
||||
@@ -37,12 +37,33 @@ class Message < ApplicationRecord
|
||||
include Liquidable
|
||||
NUMBER_OF_PERMITTED_ATTACHMENTS = 15
|
||||
|
||||
TEMPLATE_PARAMS_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'template_params': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': { 'type': 'string' },
|
||||
'category': { 'type': 'string' },
|
||||
'language': { 'type': 'string' },
|
||||
'namespace': { 'type': 'string' },
|
||||
'processed_params': { 'type': 'object' }
|
||||
},
|
||||
'required': %w[name category language namespace processed_params]
|
||||
}
|
||||
}
|
||||
}.to_json.freeze
|
||||
|
||||
before_validation :ensure_content_type
|
||||
|
||||
validates :account_id, presence: true
|
||||
validates :inbox_id, presence: true
|
||||
validates :conversation_id, presence: true
|
||||
validates_with ContentAttributeValidator
|
||||
validates_with JsonSchemaValidator,
|
||||
schema: TEMPLATE_PARAMS_SCHEMA,
|
||||
attribute_resolver: ->(record) { record.additional_attributes }
|
||||
|
||||
validates :content_type, presence: true
|
||||
validates :content, length: { maximum: 150_000 }
|
||||
|
||||
|
||||
@@ -28,13 +28,14 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
||||
message.update!(source_id: message_id) if message_id.present?
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def processable_channel_message_template
|
||||
if template_params.present?
|
||||
return [
|
||||
template_params['name'],
|
||||
template_params['namespace'],
|
||||
template_params['language'],
|
||||
template_params['processed_params'].map { |_, value| { type: 'text', text: value } }
|
||||
template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
|
||||
]
|
||||
end
|
||||
|
||||
@@ -55,6 +56,7 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
||||
end
|
||||
[nil, nil, nil, nil]
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def template_match_object(template)
|
||||
body_object = validated_body_object(template)
|
||||
|
||||
112
spec/models/concerns/json_schema_validator_spec.rb
Normal file
112
spec/models/concerns/json_schema_validator_spec.rb
Normal file
@@ -0,0 +1,112 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe JsonSchemaValidator, type: :validator do
|
||||
schema = {
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'name' => { 'type' => 'string' },
|
||||
'age' => { 'type' => 'integer' },
|
||||
'is_active' => { 'type' => 'boolean' },
|
||||
'tags' => { 'type' => 'array' },
|
||||
'address' => {
|
||||
'type' => 'object',
|
||||
'properties' => {
|
||||
'street' => { 'type' => 'string' },
|
||||
'city' => { 'type' => 'string' }
|
||||
},
|
||||
'required' => %w[street city]
|
||||
}
|
||||
},
|
||||
:required => %w[name age]
|
||||
}.to_json.freeze
|
||||
|
||||
# Create a simple test model for validation
|
||||
before_all do
|
||||
# rubocop:disable Lint/ConstantDefinitionInBlock
|
||||
# rubocop:disable RSpec/LeakyConstantDeclaration
|
||||
TestModelForJSONValidation = Struct.new(:additional_attributes) do
|
||||
include ActiveModel::Validations
|
||||
|
||||
validates_with JsonSchemaValidator, schema: schema
|
||||
end
|
||||
# rubocop:enable Lint/ConstantDefinitionInBlock
|
||||
# rubocop:enable RSpec/LeakyConstantDeclaration
|
||||
end
|
||||
|
||||
context 'with valid data' do
|
||||
let(:valid_data) do
|
||||
{
|
||||
'name' => 'John Doe',
|
||||
'age' => 30,
|
||||
'tags' => %w[tag1 tag2],
|
||||
'is_active' => true,
|
||||
'address' => {
|
||||
'street' => '123 Main St',
|
||||
'city' => 'Iceland'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'passes validation' do
|
||||
model = TestModelForJSONValidation.new(valid_data)
|
||||
expect(model.valid?).to be true
|
||||
expect(model.errors.full_messages).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with missing required attributes' do
|
||||
let(:invalid_data) do
|
||||
{
|
||||
'name' => 'John Doe',
|
||||
'address' => {
|
||||
'street' => '123 Main St',
|
||||
'city' => 'Iceland'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'fails validation' do
|
||||
model = TestModelForJSONValidation.new(invalid_data)
|
||||
expect(model.valid?).to be false
|
||||
expect(model.errors.messages).to eq({ :age => ['is required'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with incorrect address hash' do
|
||||
let(:invalid_data) do
|
||||
{
|
||||
'name' => 'John Doe',
|
||||
'age' => 30,
|
||||
'address' => 'not-a-hash'
|
||||
}
|
||||
end
|
||||
|
||||
it 'fails validation' do
|
||||
model = TestModelForJSONValidation.new(invalid_data)
|
||||
expect(model.valid?).to be false
|
||||
expect(model.errors.messages).to eq({ :address => ['must be of type hash'] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with incorrect types' do
|
||||
let(:invalid_data) do
|
||||
{
|
||||
'name' => 'John Doe',
|
||||
'age' => '30',
|
||||
'is_active' => 'some-value',
|
||||
'tags' => 'not-an-array',
|
||||
'address' => {
|
||||
'street' => 123,
|
||||
'city' => 'Iceland'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'fails validation' do
|
||||
model = TestModelForJSONValidation.new(invalid_data)
|
||||
expect(model.valid?).to be false
|
||||
expect(model.errors.messages).to eq({ :age => ['must be of type integer'], :'address/street' => ['must be of type string'],
|
||||
:is_active => ['must be of type boolean'], :tags => ['must be of type array'] })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ describe Whatsapp::SendOnWhatsappService do
|
||||
name: 'sample_shipping_confirmation',
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
language: 'en_US',
|
||||
category: 'Marketing',
|
||||
processed_params: { '1' => '3' }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user