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:
Shivam Mishra
2023-04-25 16:50:36 +05:30
committed by GitHub
parent 0bbb28c432
commit 5600b518ac
5 changed files with 221 additions and 1 deletions

View 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

View File

@@ -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 }

View File

@@ -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)

View 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

View File

@@ -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' }
}