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)
|
||||
|
||||
Reference in New Issue
Block a user