Class: LcpSdk::SchemaValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/schema_validator.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema_documents) ⇒ SchemaValidator

Returns a new instance of SchemaValidator.



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/schema_validator.rb', line 9

def initialize(schema_documents)
  require "json_schemer"
  # Keep a private copy so self-references in core.json can be made
  # fragment-local without mutating the caller's canonical documents.
  @documents = schema_documents.transform_values do |document|
    JSON.parse(JSON.generate(document))
  end
  @documents.each_value do |document|
    rewrite_core_refs(document) if document["$id"] == "https://lcp.dev/schemas/core.json"
  end
  @ids = {}
  @documents.each do |name, document|
    id = document.fetch("$id", name)
    @ids[normalize(name)] = id
    @ids[normalize(id)] = id
  end
end

Class Method Details

.from_directory(root) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/schema_validator.rb', line 27

def self.from_directory(root)
  documents = {}
  Dir.glob(File.join(root, "**", "*.json")).each do |path|
    name = path.delete_prefix("#{root}/")
    documents[name] = JSON.parse(File.read(path))
  end
  if File.basename(root.to_s) == "schemas"
    vertical_root = File.join(File.dirname(root), "verticals")
    Dir.glob(File.join(vertical_root, "**", "*.json")).each do |path|
      name = path.delete_prefix("#{vertical_root}/")
      documents["verticals/#{name}"] = JSON.parse(File.read(path))
    end
  end
  new(documents)
end

Instance Method Details

#validate(schema_name, instance) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/schema_validator.rb', line 43

def validate(schema_name, instance)
  id = @ids.fetch(normalize(schema_name)) { raise KeyError, "unknown LCP schema: #{schema_name}" }
  schema = @documents.values.find { |candidate| candidate["$id"] == id } || @documents.fetch(schema_name)
  errors = JSONSchemer.schema(schema, ref_resolver: lambda do |uri|
    reference = uri.to_s
    @documents.values.find { |candidate| candidate["$id"].to_s == reference } ||
      @documents[reference] ||
      @documents[reference.sub(%r{.*/}, "")]
  end).validate(instance).to_a
  raise SchemaValidationError, "LCP schema validation failed for #{schema_name}: #{errors.first}" unless errors.empty?
  true
end

#validate_envelope(envelope) ⇒ Object



56
57
58
59
60
61
62
63
# File 'lib/schema_validator.rb', line 56

def validate_envelope(envelope)
  validate("schemas/envelope.json", envelope)
  type = envelope.dig("lcp", "message", "type")
  raise SchemaValidationError, "missing lcp.message.type" unless type.is_a?(String)
  payload = envelope.dig("lcp", "payload")
  validate("schemas/#{type}.json", payload)
  validate_vertical_policy(type, payload)
end

#validate_offer(offer) ⇒ Object



97
98
99
# File 'lib/schema_validator.rb', line 97

def validate_offer(offer)
  validate("schemas/offer.json", offer)
end

#validate_ping_safe(vertical, value, schema, path) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/schema_validator.rb', line 84

def validate_ping_safe(vertical, value, schema, path)
  return unless value.is_a?(Hash)
  properties = schema.fetch("properties", {})
  value.each do |name, child|
    next if path == "attributes" && %w[vertical schema_version].include?(name)
    definition = properties[name]
    unless definition.is_a?(Hash) && definition["ping_safe"] == true
      raise SchemaValidationError, "#{path}.#{name} is not tagged ping_safe: true in vertical '#{vertical}'"
    end
    validate_ping_safe(vertical, child, definition, "#{path}.#{name}") if child.is_a?(Hash) && definition["properties"].is_a?(Hash)
  end
end

#validate_vertical(vertical, attributes) ⇒ Object



101
102
103
# File 'lib/schema_validator.rb', line 101

def validate_vertical(vertical, attributes)
  validate("verticals/#{vertical}.json", attributes)
end

#validate_vertical_policy(type, payload) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/schema_validator.rb', line 65

def validate_vertical_policy(type, payload)
  return unless %w[lead call post ping].include?(type) && payload.is_a?(Hash)
  attributes = payload["attributes"]
  return unless attributes.is_a?(Hash)
  vertical = type == "ping" ? payload["vertical"] : attributes["vertical"]
  return unless vertical.is_a?(String) && !vertical.empty?
  schema = @documents[normalize("verticals/#{vertical}.json")] ||
    @documents["verticals/#{vertical}.json"] ||
    @documents["#{vertical}.json"]
  raise SchemaValidationError, "Vertical schema '#{vertical}' not found" unless schema.is_a?(Hash)
  vertical_attributes = JSON.parse(JSON.generate(attributes))
  if type == "ping"
    vertical_attributes["vertical"] ||= vertical
    vertical_attributes["schema_version"] ||= schema.dig("properties", "schema_version", "const") || "1.0.0"
  end
  validate_vertical(vertical, vertical_attributes)
  validate_ping_safe(vertical, attributes, schema, "attributes") if type == "ping"
end