easol-canvas 1.2.0 → 1.5.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/required_files_check.rb +11 -4
- data/lib/canvas/checks/valid_block_schemas_check.rb +3 -9
- data/lib/canvas/services/expand_attributes.rb +1 -0
- data/lib/canvas/validators/block_schema.rb +28 -2
- data/lib/canvas/validators/layout_schema.rb +174 -0
- data/lib/canvas/version.rb +1 -1
- data/schema_definitions/block_layout.json +87 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c61a6785fbf34da110e1fe2b01a9f096ff4aaf8ea3cfbef780491b5e94e68998
|
|
4
|
+
data.tar.gz: 3acb2a3215a5fd874ad5e54cc719204cb0a4698218935f2546ce5cacf3f73777
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 660512a56bea758cae95483fdaefd41a75156a7c15723e661749a64feb6dbc847bab31e279fecd733af807c33491a138b9c4afc850bfd2db84e8b1ae5d3c848a
|
|
7
|
+
data.tar.gz: 75fdbbefd7cfccc1849bf26e1c42e9f24f274c1d1ed27774d6b1a5085a5caafd30b4b8a0499d418686320157b6335116e0a25bf973b24fedee60210bb64c8d0d
|
|
@@ -14,11 +14,18 @@ module Canvas
|
|
|
14
14
|
|
|
15
15
|
def run
|
|
16
16
|
REQUIRED_FILES.each do |filename|
|
|
17
|
-
|
|
17
|
+
file_paths = Dir.glob(filename)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
if file_paths.empty?
|
|
21
|
+
@offenses << Offense.new(
|
|
22
|
+
message: "Missing file: #{filename}"
|
|
23
|
+
)
|
|
24
|
+
elsif File.zero?(file_paths.first)
|
|
25
|
+
@offenses << Offense.new(
|
|
26
|
+
message: "Empty file: #{file_paths.first}"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
end
|
|
24
31
|
end
|
|
@@ -37,7 +37,8 @@ module Canvas
|
|
|
37
37
|
|
|
38
38
|
def validate_format(filename, front_matter)
|
|
39
39
|
return true if front_matter.is_a?(Hash) &&
|
|
40
|
-
front_matter.values.all? { |attr| attr.is_a?(Hash) }
|
|
40
|
+
(front_matter.key?("attributes") ? front_matter["attributes"] : front_matter).values.all? { |attr| attr.is_a?(Hash) }
|
|
41
|
+
|
|
41
42
|
|
|
42
43
|
@offenses << Offense.new(
|
|
43
44
|
message: "Invalid Block Schema: #{filename} - \nSchema is not in a valid format"
|
|
@@ -45,8 +46,7 @@ module Canvas
|
|
|
45
46
|
false
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
def validate_schema(filename,
|
|
49
|
-
schema = extract_schema(front_matter)
|
|
49
|
+
def validate_schema(filename, schema, custom_types)
|
|
50
50
|
validator = Validator::BlockSchema.new(schema: schema, custom_types: custom_types)
|
|
51
51
|
return if validator.validate
|
|
52
52
|
|
|
@@ -62,11 +62,5 @@ module Canvas
|
|
|
62
62
|
front_matter = extractor.front_matter
|
|
63
63
|
front_matter.nil? ? {} : YAML.safe_load(front_matter)
|
|
64
64
|
end
|
|
65
|
-
|
|
66
|
-
def extract_schema(front_matter)
|
|
67
|
-
{
|
|
68
|
-
"attributes" => Canvas::ExpandAttributes.call(front_matter)
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
65
|
end
|
|
72
66
|
end
|
|
@@ -30,6 +30,7 @@ module Canvas
|
|
|
30
30
|
# @return [Array<Hash>] array of hashes that represent each attribute
|
|
31
31
|
def call(attributes_hash)
|
|
32
32
|
return [] if attributes_hash.nil?
|
|
33
|
+
return attributes_hash if attributes_hash.is_a?(Array)
|
|
33
34
|
|
|
34
35
|
attributes_hash.each_with_object([]) do |(name, attribute_hash), attrs|
|
|
35
36
|
attrs << attribute_hash.merge("name" => name)
|
|
@@ -23,21 +23,24 @@ module Canvas
|
|
|
23
23
|
# ]
|
|
24
24
|
# }
|
|
25
25
|
class BlockSchema
|
|
26
|
-
PERMITTED_KEYS = %w[attributes].freeze
|
|
26
|
+
PERMITTED_KEYS = %w[attributes layout].freeze
|
|
27
27
|
|
|
28
28
|
attr_reader :errors, :schema
|
|
29
29
|
|
|
30
30
|
# @param schema [Hash] the schema to be validated
|
|
31
31
|
# @param custom_types [Array<Hash>] a list of custom types
|
|
32
32
|
def initialize(schema:, custom_types: [])
|
|
33
|
-
@schema = schema
|
|
33
|
+
@schema = normalize_schema(schema)
|
|
34
34
|
@custom_types = custom_types
|
|
35
35
|
@errors = []
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def validate
|
|
39
|
+
@errors = []
|
|
40
|
+
|
|
39
41
|
if ensure_valid_format
|
|
40
42
|
ensure_no_unrecognized_keys
|
|
43
|
+
ensure_layout_is_valid
|
|
41
44
|
ensure_attributes_are_valid
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -54,6 +57,16 @@ module Canvas
|
|
|
54
57
|
false
|
|
55
58
|
end
|
|
56
59
|
|
|
60
|
+
def ensure_layout_is_valid
|
|
61
|
+
return true unless schema["layout"]
|
|
62
|
+
|
|
63
|
+
layout_validator = LayoutSchema.new(schema: @schema)
|
|
64
|
+
return true if layout_validator.validate
|
|
65
|
+
|
|
66
|
+
@errors += layout_validator.errors
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
57
70
|
def ensure_no_unrecognized_keys
|
|
58
71
|
unrecognized_keys = schema.keys - PERMITTED_KEYS
|
|
59
72
|
return true if unrecognized_keys.empty?
|
|
@@ -81,6 +94,19 @@ module Canvas
|
|
|
81
94
|
schema["attributes"].is_a?(Array) &&
|
|
82
95
|
schema["attributes"].all? { |attr| attr.is_a?(Hash) }
|
|
83
96
|
end
|
|
97
|
+
|
|
98
|
+
def normalize_schema(schema)
|
|
99
|
+
if schema.key?("attributes")
|
|
100
|
+
{
|
|
101
|
+
**schema,
|
|
102
|
+
"attributes" => Canvas::ExpandAttributes.call(schema["attributes"])
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
{
|
|
106
|
+
"attributes" => Canvas::ExpandAttributes.call(schema)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
84
110
|
end
|
|
85
111
|
end
|
|
86
112
|
end
|
|
@@ -0,0 +1,174 @@
|
|
|
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, part of block schema.
|
|
10
|
+
# Example of a valid layout definition:
|
|
11
|
+
# {
|
|
12
|
+
# "attributes" => [
|
|
13
|
+
# {
|
|
14
|
+
# "name" => "title",
|
|
15
|
+
# "type" => "string"
|
|
16
|
+
# }
|
|
17
|
+
# ...
|
|
18
|
+
# ],
|
|
19
|
+
# "layout" => [
|
|
20
|
+
# {
|
|
21
|
+
# "label" => "Design",
|
|
22
|
+
# "type" => "tab",
|
|
23
|
+
# "elements" => [
|
|
24
|
+
# "heading",
|
|
25
|
+
# {
|
|
26
|
+
# "type" => "accordion",
|
|
27
|
+
# "label" => "Logo",
|
|
28
|
+
# "elements" => [
|
|
29
|
+
# "description",
|
|
30
|
+
# { "type" => "attribute", "name" => "logo_alt" },
|
|
31
|
+
# "title"
|
|
32
|
+
# ]
|
|
33
|
+
# },
|
|
34
|
+
# {
|
|
35
|
+
# "type" => "accordion_toggle",
|
|
36
|
+
# "toggle_attribute" => "cta_enabled",
|
|
37
|
+
# "elements" => [
|
|
38
|
+
# "cta_text",
|
|
39
|
+
# "cta_target"
|
|
40
|
+
# ]
|
|
41
|
+
# }
|
|
42
|
+
# ]
|
|
43
|
+
# }
|
|
44
|
+
# ]
|
|
45
|
+
# }
|
|
46
|
+
class LayoutSchema
|
|
47
|
+
attr_reader :errors
|
|
48
|
+
|
|
49
|
+
def initialize(schema:)
|
|
50
|
+
@schema = schema
|
|
51
|
+
@errors = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate
|
|
55
|
+
@errors = []
|
|
56
|
+
|
|
57
|
+
if ensure_valid_format
|
|
58
|
+
ensure_no_unrecognized_keys
|
|
59
|
+
ensure_no_duplicate_keys
|
|
60
|
+
ensure_accordion_toggles_are_valid
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@errors.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :schema
|
|
69
|
+
|
|
70
|
+
def ensure_no_duplicate_keys
|
|
71
|
+
attributes = fetch_all_attribute_names
|
|
72
|
+
duplicates =
|
|
73
|
+
attributes
|
|
74
|
+
.group_by { |(key)| key }
|
|
75
|
+
.filter { |key, usage| usage.size > 1 }
|
|
76
|
+
|
|
77
|
+
unless duplicates.empty?
|
|
78
|
+
duplicates.each do |attribute, usage|
|
|
79
|
+
@errors << "Duplicated attribute key `#{attribute}` found. Location: #{usage.map { |(_, location)| location }.join(", ")}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def ensure_no_unrecognized_keys
|
|
85
|
+
attributes = fetch_all_attribute_names
|
|
86
|
+
defined_attributes = schema["attributes"]&.map { |definition| normalize_attribute(definition["name"]) } || []
|
|
87
|
+
|
|
88
|
+
attributes.each do |attribute, location|
|
|
89
|
+
@errors << "Unrecognized attribute `#{attribute}`. Location: #{location}" unless defined_attributes.include?(attribute)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Array<Array(String, String)>] an array of all the attribute names that
|
|
94
|
+
# are mentioned in the layout schema, along with its path. The names are
|
|
95
|
+
# normalized, i.e. downcased.
|
|
96
|
+
def fetch_all_attribute_names
|
|
97
|
+
attributes = fetch_elements_of_type("attribute")
|
|
98
|
+
attributes.map do |(node, path)|
|
|
99
|
+
[
|
|
100
|
+
normalize_attribute(node.is_a?(Hash) ? node["name"] : node),
|
|
101
|
+
path
|
|
102
|
+
]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @param type [String] the element type to fetch
|
|
107
|
+
# @return [Array<Array(<Hash, String>, String)] an array of elements that match
|
|
108
|
+
# the given type. Each element is an array containing the node and its path.
|
|
109
|
+
def fetch_elements_of_type(type)
|
|
110
|
+
elements = []
|
|
111
|
+
|
|
112
|
+
fetch_element = ->(node, path) {
|
|
113
|
+
if type == "attribute" && node.is_a?(String)
|
|
114
|
+
elements << [node, path]
|
|
115
|
+
elsif node["type"] == type
|
|
116
|
+
elements << [node, path]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if node.is_a?(Hash) && node.key?("elements")
|
|
120
|
+
node["elements"].each_with_index do |element, i|
|
|
121
|
+
current_path = "#{path}/elements/#{i}"
|
|
122
|
+
fetch_element.call(element, current_path)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
layout_schema.each_with_index do |tab, i|
|
|
128
|
+
current_path = "layout/#{i}"
|
|
129
|
+
fetch_element.call(tab, current_path)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
elements
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ensure_valid_format
|
|
136
|
+
result = JSON::Validator.fully_validate(schema_definition, { "layout" => layout_schema }, strict: true, clear_cache: true)
|
|
137
|
+
|
|
138
|
+
return true if result.empty?
|
|
139
|
+
|
|
140
|
+
@errors += result
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def ensure_accordion_toggles_are_valid
|
|
145
|
+
accordion_toggles = fetch_elements_of_type("accordion_toggle")
|
|
146
|
+
accordion_toggles.each do |accordion_toggle, location|
|
|
147
|
+
toggle_attribute = schema["attributes"]&.detect { |attr|
|
|
148
|
+
attr["name"] == accordion_toggle["toggle_attribute"]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if toggle_attribute.nil?
|
|
152
|
+
@errors << "The toggle_attribute in accordion_toggle is unrecognized. Location: #{location}"
|
|
153
|
+
elsif toggle_attribute["type"] != "boolean"
|
|
154
|
+
@errors << "The toggle_attribute in accordion_toggle must be a boolean. Location: #{location}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def layout_schema
|
|
160
|
+
schema["layout"] || []
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def schema_definition
|
|
164
|
+
File.read(
|
|
165
|
+
File.join(File.dirname(__FILE__), "../../../", "schema_definitions", "block_layout.json")
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def normalize_attribute(name)
|
|
170
|
+
name.strip.downcase
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/canvas/version.rb
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"layout": {
|
|
5
|
+
"type": "array",
|
|
6
|
+
"items": { "$ref": "#/$defs/tab" }
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"$defs": {
|
|
10
|
+
"tab": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["type", "label"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"type": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"const": "tab"
|
|
17
|
+
},
|
|
18
|
+
"label": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
},
|
|
21
|
+
"elements": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"minItems": 1,
|
|
24
|
+
"items": {
|
|
25
|
+
"oneOf": [
|
|
26
|
+
{ "type": "string" },
|
|
27
|
+
{ "$ref": "#/$defs/accordion" },
|
|
28
|
+
{ "$ref": "#/$defs/accordion_toggle" },
|
|
29
|
+
{ "$ref": "#/$defs/attribute" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"accordion": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["label", "type"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"type": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"const": "accordion"
|
|
42
|
+
},
|
|
43
|
+
"label": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"elements": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": {
|
|
49
|
+
"oneOf": [{ "type": "string" }, { "$ref": "#/$defs/attribute" }]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"accordion_toggle": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"required": ["type", "toggle_attribute", "elements"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"type": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"const": "accordion_toggle"
|
|
61
|
+
},
|
|
62
|
+
"toggle_attribute": {
|
|
63
|
+
"type": "string"
|
|
64
|
+
},
|
|
65
|
+
"elements": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": {
|
|
68
|
+
"oneOf": [{ "type": "string" }, { "$ref": "#/$defs/attribute" }]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"attribute": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"required": ["name", "type"],
|
|
76
|
+
"properties": {
|
|
77
|
+
"type": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"const": "attribute"
|
|
80
|
+
},
|
|
81
|
+
"name": {
|
|
82
|
+
"type": "string"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
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: 1.
|
|
4
|
+
version: 1.5.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: 2022-
|
|
12
|
+
date: 2022-07-21 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: thor
|
|
@@ -81,6 +81,20 @@ dependencies:
|
|
|
81
81
|
- - "~>"
|
|
82
82
|
- !ruby/object:Gem::Version
|
|
83
83
|
version: '2.4'
|
|
84
|
+
- !ruby/object:Gem::Dependency
|
|
85
|
+
name: json-schema
|
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - "~>"
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '3'
|
|
91
|
+
type: :runtime
|
|
92
|
+
prerelease: false
|
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - "~>"
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '3'
|
|
84
98
|
description: |
|
|
85
99
|
Canvas is a command line tool to help with building themes for Easol.
|
|
86
100
|
It provides tooling to check theme directories for errors and to make sure
|
|
@@ -117,6 +131,7 @@ files:
|
|
|
117
131
|
- lib/canvas/validators/footer_schema.rb
|
|
118
132
|
- lib/canvas/validators/html.rb
|
|
119
133
|
- lib/canvas/validators/json.rb
|
|
134
|
+
- lib/canvas/validators/layout_schema.rb
|
|
120
135
|
- lib/canvas/validators/liquid.rb
|
|
121
136
|
- lib/canvas/validators/menu_schema.rb
|
|
122
137
|
- lib/canvas/validators/sass.rb
|
|
@@ -135,6 +150,7 @@ files:
|
|
|
135
150
|
- lib/canvas/validators/schema_attributes/variant.rb
|
|
136
151
|
- lib/canvas/version.rb
|
|
137
152
|
- lib/easol/canvas.rb
|
|
153
|
+
- schema_definitions/block_layout.json
|
|
138
154
|
homepage: https://rubygems.org/gems/easol-canvas
|
|
139
155
|
licenses:
|
|
140
156
|
- MIT
|