easol-canvas 4.18.0 → 4.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/canvas/checks/valid_custom_types_check.rb +3 -1
- data/lib/canvas/checks/valid_liquid_check.rb +18 -15
- data/lib/canvas/validators/custom_type.rb +18 -5
- data/lib/canvas/validators/custom_type_layout_schema.rb +172 -0
- data/lib/canvas/version.rb +1 -1
- data/schema_definitions/custom_type_layout.json +79 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c37703170f2a9f0870de51fa994e8929d1ed3a53ebe7f510c27ab943bddf9b6c
|
|
4
|
+
data.tar.gz: 1ccfe78c1b4aaaf9cb4bcbbd1d0afc062b63e94d93b41e162174aedff5fe9832
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f2ca1fc7300356060311c3e1622b05df28e9d9e48985a8d50a40319c83d15a882a4ac7174c9f39bdc501f47e2988e8a9e9980187dbf513d03b6c8193f927f62
|
|
7
|
+
data.tar.gz: 883efd325fda12742d09a6ae3b30c102c51406fec1caf10f112d1ee8ce6ea9f145de80e9c1a4d6dd2f8562c7b5909aee99895a72207108c610d735a89a318db6
|
|
@@ -6,9 +6,11 @@ module Canvas
|
|
|
6
6
|
# custom types that are defined in the /types directory.
|
|
7
7
|
class ValidCustomTypesCheck < Check
|
|
8
8
|
def run
|
|
9
|
+
custom_types = Canvas::FetchCustomTypes.call
|
|
10
|
+
|
|
9
11
|
custom_type_files.each do |filename|
|
|
10
12
|
schema = extract_json(filename)
|
|
11
|
-
validator = Validator::CustomType.new(schema: schema)
|
|
13
|
+
validator = Validator::CustomType.new(schema: schema, custom_types: custom_types)
|
|
12
14
|
|
|
13
15
|
next if validator.validate
|
|
14
16
|
|
|
@@ -30,27 +30,30 @@ module Canvas
|
|
|
30
30
|
::Liquid::Template.register_tag(name, klass)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# These are the tags that we define in the Rails app. If we add a
|
|
34
|
-
# in the Rails app, we'll need to register it here.
|
|
33
|
+
# These are the tags that we define in the Rails app. If we add a
|
|
34
|
+
# new custom tag in the Rails app, we'll need to register it here.
|
|
35
35
|
def register_tags!
|
|
36
|
-
|
|
37
|
-
register_tag("product_search", ::Liquid::Block)
|
|
38
|
-
register_tag("easol_badge", ::Liquid::Tag)
|
|
39
|
-
register_tag("accommodation_availability", ::Liquid::Block)
|
|
40
|
-
register_tag("cache", ::Liquid::Block)
|
|
41
|
-
register_tag("currency_switcher", ::Liquid::Tag)
|
|
42
|
-
register_tag("json", ::Liquid::Block)
|
|
43
|
-
register_tag("variant_pricing", ::Liquid::Tag)
|
|
44
|
-
register_tag("dynamic_package_booking_price", ::Liquid::Tag)
|
|
36
|
+
# Single statement non-block tags, no closing tag required.
|
|
45
37
|
register_tag("cart_timer", ::Liquid::Tag)
|
|
46
38
|
register_tag("cart_timer_add_time", ::Liquid::Tag)
|
|
47
|
-
register_tag("
|
|
48
|
-
register_tag("
|
|
49
|
-
register_tag("
|
|
50
|
-
register_tag("package_price", Liquid::Block)
|
|
39
|
+
register_tag("currency_switcher", ::Liquid::Tag)
|
|
40
|
+
register_tag("dynamic_package_booking_price", ::Liquid::Tag)
|
|
41
|
+
register_tag("easol_badge", ::Liquid::Tag)
|
|
51
42
|
register_tag("input", ::Liquid::Tag)
|
|
52
43
|
register_tag("label", ::Liquid::Tag)
|
|
44
|
+
register_tag("variant_pricing", ::Liquid::Tag)
|
|
45
|
+
# Tags wrapping nested content with a closing tag.
|
|
46
|
+
register_tag("accommodation_availability", ::Liquid::Block)
|
|
47
|
+
register_tag("cache", ::Liquid::Block)
|
|
48
|
+
register_tag("experience_slot_calendar", ::Liquid::Block)
|
|
49
|
+
register_tag("experience_slot_search", ::Liquid::Block)
|
|
50
|
+
register_tag("form", ::Liquid::Block)
|
|
51
|
+
register_tag("json", ::Liquid::Block)
|
|
52
|
+
register_tag("package_availability", ::Liquid::Block)
|
|
53
|
+
register_tag("package_price", ::Liquid::Block)
|
|
53
54
|
register_tag("package_step_product_search", ::Liquid::Block)
|
|
55
|
+
register_tag("packages_search_and_calendar", ::Liquid::Block)
|
|
56
|
+
register_tag("product_search", ::Liquid::Block)
|
|
54
57
|
end
|
|
55
58
|
end
|
|
56
59
|
end
|
|
@@ -25,11 +25,13 @@ module Canvas
|
|
|
25
25
|
#
|
|
26
26
|
class CustomType
|
|
27
27
|
REQUIRED_KEYS = %w[key name attributes].freeze
|
|
28
|
+
OPTIONAL_KEYS = %w[layout].freeze
|
|
28
29
|
|
|
29
|
-
attr_reader :schema, :errors
|
|
30
|
+
attr_reader :schema, :errors, :custom_types
|
|
30
31
|
|
|
31
|
-
def initialize(schema:)
|
|
32
|
+
def initialize(schema:, custom_types: [])
|
|
32
33
|
@schema = schema
|
|
34
|
+
@custom_types = custom_types
|
|
33
35
|
@errors = []
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -42,7 +44,8 @@ module Canvas
|
|
|
42
44
|
ensure_key_value_is_not_reserved &&
|
|
43
45
|
ensure_no_duplicate_attributes &&
|
|
44
46
|
ensure_attributes_are_valid &&
|
|
45
|
-
ensure_first_attribute_not_array
|
|
47
|
+
ensure_first_attribute_not_array &&
|
|
48
|
+
ensure_layout_is_valid
|
|
46
49
|
|
|
47
50
|
errors.empty?
|
|
48
51
|
end
|
|
@@ -65,7 +68,7 @@ module Canvas
|
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
def ensure_no_unrecognized_keys
|
|
68
|
-
unrecognized_keys = schema.keys - REQUIRED_KEYS
|
|
71
|
+
unrecognized_keys = schema.keys - REQUIRED_KEYS - OPTIONAL_KEYS
|
|
69
72
|
return true if unrecognized_keys.empty?
|
|
70
73
|
|
|
71
74
|
@errors << "Unrecognized keys: #{unrecognized_keys.join(', ')}"
|
|
@@ -127,7 +130,7 @@ module Canvas
|
|
|
127
130
|
schema["attributes"].each do |attribute_schema|
|
|
128
131
|
attr_validator = Validator::SchemaAttribute.new(
|
|
129
132
|
attribute: attribute_schema,
|
|
130
|
-
custom_types:
|
|
133
|
+
custom_types: custom_types
|
|
131
134
|
)
|
|
132
135
|
next if attr_validator.validate
|
|
133
136
|
|
|
@@ -144,6 +147,16 @@ module Canvas
|
|
|
144
147
|
@errors << "The first attribute cannot be an array"
|
|
145
148
|
false
|
|
146
149
|
end
|
|
150
|
+
|
|
151
|
+
def ensure_layout_is_valid
|
|
152
|
+
return true unless schema["layout"]
|
|
153
|
+
|
|
154
|
+
layout_validator = CustomTypeLayoutSchema.new(schema:)
|
|
155
|
+
return true if layout_validator.validate
|
|
156
|
+
|
|
157
|
+
@errors += layout_validator.errors
|
|
158
|
+
false
|
|
159
|
+
end
|
|
147
160
|
end
|
|
148
161
|
end
|
|
149
162
|
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "json-schema"
|
|
5
|
+
|
|
6
|
+
module Canvas
|
|
7
|
+
module Validator
|
|
8
|
+
# :documented:
|
|
9
|
+
# This class is used to validate a layout definition for a custom type.
|
|
10
|
+
# Custom type layouts use a flat array of elements (unlike block layouts which use tabs).
|
|
11
|
+
#
|
|
12
|
+
# Example of a valid custom type layout definition:
|
|
13
|
+
# {
|
|
14
|
+
# "attributes" => [
|
|
15
|
+
# { "name" => "question", "type" => "string" },
|
|
16
|
+
# { "name" => "answer", "type" => "text" },
|
|
17
|
+
# { "name" => "show_icon", "type" => "boolean" }
|
|
18
|
+
# ],
|
|
19
|
+
# "layout" => [
|
|
20
|
+
# "question",
|
|
21
|
+
# {
|
|
22
|
+
# "type" => "accordion",
|
|
23
|
+
# "label" => "Advanced",
|
|
24
|
+
# "elements" => [
|
|
25
|
+
# "answer",
|
|
26
|
+
# { "type" => "attribute", "name" => "show_icon" }
|
|
27
|
+
# ]
|
|
28
|
+
# }
|
|
29
|
+
# ]
|
|
30
|
+
# }
|
|
31
|
+
class CustomTypeLayoutSchema
|
|
32
|
+
attr_reader :errors
|
|
33
|
+
|
|
34
|
+
def initialize(schema:)
|
|
35
|
+
@schema = schema
|
|
36
|
+
@errors = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate
|
|
40
|
+
@errors = []
|
|
41
|
+
|
|
42
|
+
if ensure_valid_format
|
|
43
|
+
ensure_no_unrecognized_keys
|
|
44
|
+
ensure_no_duplicate_keys
|
|
45
|
+
ensure_accordion_toggles_are_valid
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@errors.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :schema
|
|
54
|
+
|
|
55
|
+
def ensure_no_duplicate_keys
|
|
56
|
+
attributes = fetch_all_attribute_names
|
|
57
|
+
duplicates =
|
|
58
|
+
attributes
|
|
59
|
+
.group_by { |(key)| key }
|
|
60
|
+
.filter { |_key, usage| usage.size > 1 }
|
|
61
|
+
|
|
62
|
+
return if duplicates.empty?
|
|
63
|
+
|
|
64
|
+
duplicates.each do |attribute, usage|
|
|
65
|
+
@errors << "Duplicated attribute key `#{attribute}` found. "\
|
|
66
|
+
"Location: #{usage.map { |(_, location)| location }.join(', ')}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ensure_no_unrecognized_keys
|
|
71
|
+
attributes = fetch_all_attribute_names
|
|
72
|
+
defined_attributes = schema_attributes.map { |attr| normalize_attribute(attr["name"]) }
|
|
73
|
+
|
|
74
|
+
attributes.each do |attribute, location|
|
|
75
|
+
unless defined_attributes.include?(attribute)
|
|
76
|
+
@errors << "Unrecognized attribute `#{attribute}`. Location: #{location}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Array<Array(String, String)>] an array of all the attribute names that
|
|
82
|
+
# are mentioned in the layout schema, along with its path. The names are
|
|
83
|
+
# normalized, i.e. downcased.
|
|
84
|
+
def fetch_all_attribute_names
|
|
85
|
+
attributes = fetch_elements_of_type("attribute")
|
|
86
|
+
attributes.map do |(node, path)|
|
|
87
|
+
[
|
|
88
|
+
normalize_attribute(node.is_a?(Hash) ? node["name"] : node),
|
|
89
|
+
path
|
|
90
|
+
]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param type [String] the element type to fetch
|
|
95
|
+
# @return [Array<Array(<Hash, String>, String)] an array of elements that match
|
|
96
|
+
# the given type. Each element is an array containing the node and its path.
|
|
97
|
+
def fetch_elements_of_type(type)
|
|
98
|
+
elements = []
|
|
99
|
+
|
|
100
|
+
fetch_element = lambda { |node, path|
|
|
101
|
+
if type == "attribute" && node.is_a?(String)
|
|
102
|
+
elements << [node, path]
|
|
103
|
+
elsif node.is_a?(Hash) && node["type"] == type
|
|
104
|
+
elements << [node, path]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if node.is_a?(Hash) && node.key?("elements")
|
|
108
|
+
node["elements"].each_with_index do |element, i|
|
|
109
|
+
current_path = "#{path}/elements/#{i}"
|
|
110
|
+
fetch_element.call(element, current_path)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Flat layout: iterate directly over layout elements
|
|
116
|
+
layout_schema.each_with_index do |element, i|
|
|
117
|
+
current_path = "layout/#{i}"
|
|
118
|
+
fetch_element.call(element, current_path)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
elements
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ensure_valid_format
|
|
125
|
+
result = JSON::Validator.fully_validate(
|
|
126
|
+
schema_definition,
|
|
127
|
+
{ "layout" => layout_schema },
|
|
128
|
+
strict: true,
|
|
129
|
+
clear_cache: true
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return true if result.empty?
|
|
133
|
+
|
|
134
|
+
@errors += result
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ensure_accordion_toggles_are_valid
|
|
139
|
+
accordion_toggles = fetch_elements_of_type("accordion_toggle")
|
|
140
|
+
accordion_toggles.each do |accordion_toggle, location|
|
|
141
|
+
toggle_attribute = schema_attributes.detect { |attr|
|
|
142
|
+
attr["name"] == accordion_toggle["toggle_attribute"]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if toggle_attribute.nil?
|
|
146
|
+
@errors << "The toggle_attribute in accordion_toggle is unrecognized. Location: #{location}"
|
|
147
|
+
elsif toggle_attribute["type"]&.downcase != "boolean"
|
|
148
|
+
@errors << "The toggle_attribute in accordion_toggle must be a boolean. Location: #{location}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def layout_schema
|
|
154
|
+
schema["layout"] || []
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def schema_definition
|
|
158
|
+
File.read(
|
|
159
|
+
File.join(File.dirname(__FILE__), "../../../", "schema_definitions", "custom_type_layout.json")
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def normalize_attribute(name)
|
|
164
|
+
name.to_s.strip.downcase
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def schema_attributes
|
|
168
|
+
schema["attributes"] || []
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/canvas/version.rb
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"layout": {
|
|
5
|
+
"type": "array",
|
|
6
|
+
"items": {
|
|
7
|
+
"oneOf": [
|
|
8
|
+
{ "type": "string" },
|
|
9
|
+
{ "$ref": "#/$defs/accordion" },
|
|
10
|
+
{ "$ref": "#/$defs/accordion_toggle" },
|
|
11
|
+
{ "$ref": "#/$defs/attribute" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"$defs": {
|
|
17
|
+
"accordion": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["label", "type"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"const": "accordion"
|
|
24
|
+
},
|
|
25
|
+
"label": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"elements": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"oneOf": [
|
|
32
|
+
{ "type": "string" },
|
|
33
|
+
{ "$ref": "#/$defs/accordion_toggle" },
|
|
34
|
+
{ "$ref": "#/$defs/attribute" }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"additionalProperties": false
|
|
40
|
+
},
|
|
41
|
+
"accordion_toggle": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"required": ["type", "toggle_attribute", "elements"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"type": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"const": "accordion_toggle"
|
|
48
|
+
},
|
|
49
|
+
"toggle_attribute": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
},
|
|
52
|
+
"elements": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"items": {
|
|
55
|
+
"oneOf": [
|
|
56
|
+
{ "type": "string" },
|
|
57
|
+
{ "$ref": "#/$defs/attribute" }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"additionalProperties": false
|
|
63
|
+
},
|
|
64
|
+
"attribute": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"required": ["name", "type"],
|
|
67
|
+
"properties": {
|
|
68
|
+
"type": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"const": "attribute"
|
|
71
|
+
},
|
|
72
|
+
"name": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"additionalProperties": false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easol-canvas
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kyle Byrne
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2026-01-27 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: thor
|
|
@@ -129,6 +129,7 @@ files:
|
|
|
129
129
|
- lib/canvas/services/front_matter_extractor.rb
|
|
130
130
|
- lib/canvas/validators/block_schema.rb
|
|
131
131
|
- lib/canvas/validators/custom_type.rb
|
|
132
|
+
- lib/canvas/validators/custom_type_layout_schema.rb
|
|
132
133
|
- lib/canvas/validators/footer_schema.rb
|
|
133
134
|
- lib/canvas/validators/html.rb
|
|
134
135
|
- lib/canvas/validators/json.rb
|
|
@@ -157,6 +158,7 @@ files:
|
|
|
157
158
|
- lib/canvas/validators/schema_attributes/variant.rb
|
|
158
159
|
- lib/canvas/version.rb
|
|
159
160
|
- lib/easol/canvas.rb
|
|
161
|
+
- schema_definitions/custom_type_layout.json
|
|
160
162
|
- schema_definitions/layout.json
|
|
161
163
|
homepage: https://rubygems.org/gems/easol-canvas
|
|
162
164
|
licenses:
|