easol-canvas 0.1.1 → 1.2.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/check.rb +4 -1
- data/lib/canvas/checks/required_files_check.rb +3 -0
- data/lib/canvas/checks/valid_block_schemas_check.rb +72 -0
- data/lib/canvas/checks/valid_custom_types_check.rb +36 -0
- data/lib/canvas/checks/valid_footer_schema_check.rb +81 -0
- data/lib/canvas/checks/valid_html_check.rb +3 -0
- data/lib/canvas/checks/valid_json_check.rb +27 -0
- data/lib/canvas/checks/valid_liquid_check.rb +4 -1
- data/lib/canvas/checks/valid_menu_schema_check.rb +81 -0
- data/lib/canvas/checks/valid_sass_check.rb +30 -0
- data/lib/canvas/checks.rb +4 -1
- data/lib/canvas/cli.rb +10 -1
- data/lib/canvas/constants.rb +31 -0
- data/lib/canvas/lint.rb +7 -4
- data/lib/canvas/offense.rb +3 -0
- data/lib/canvas/services/expand_attributes.rb +40 -0
- data/lib/canvas/services/fetch_custom_types.rb +27 -0
- data/lib/canvas/services/front_matter_extractor.rb +1 -0
- data/lib/canvas/validators/block_schema.rb +86 -0
- data/lib/canvas/validators/custom_type.rb +141 -0
- data/lib/canvas/validators/footer_schema.rb +29 -0
- data/lib/canvas/validators/html.rb +5 -2
- data/lib/canvas/validators/json.rb +24 -0
- data/lib/canvas/validators/liquid.rb +4 -1
- data/lib/canvas/validators/menu_schema.rb +95 -0
- data/lib/canvas/validators/sass.rb +26 -0
- data/lib/canvas/validators/schema_attribute.rb +146 -0
- data/lib/canvas/validators/schema_attributes/base.rb +104 -0
- data/lib/canvas/validators/schema_attributes/color.rb +81 -0
- data/lib/canvas/validators/schema_attributes/image.rb +45 -0
- data/lib/canvas/validators/schema_attributes/link.rb +51 -0
- data/lib/canvas/validators/schema_attributes/number.rb +17 -0
- data/lib/canvas/validators/schema_attributes/page.rb +49 -0
- data/lib/canvas/validators/schema_attributes/post.rb +49 -0
- data/lib/canvas/validators/schema_attributes/product.rb +49 -0
- data/lib/canvas/validators/schema_attributes/radio.rb +55 -0
- data/lib/canvas/validators/schema_attributes/range.rb +24 -0
- data/lib/canvas/validators/schema_attributes/select.rb +59 -0
- data/lib/canvas/validators/schema_attributes/variant.rb +49 -0
- data/lib/canvas/version.rb +3 -1
- data/lib/canvas.rb +30 -12
- data/lib/easol/canvas.rb +4 -0
- metadata +62 -19
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Canvas
|
4
|
+
module Validator
|
5
|
+
# :documented:
|
6
|
+
# This class is used to validate the JSON defining a custom type.
|
7
|
+
#
|
8
|
+
# An example of a valid custom type:
|
9
|
+
# {
|
10
|
+
# "key": "faq",
|
11
|
+
# "name": "Faq",
|
12
|
+
# "attributes": [
|
13
|
+
# {
|
14
|
+
# "name": "question",
|
15
|
+
# "label": "Question",
|
16
|
+
# "type": "string"
|
17
|
+
# },
|
18
|
+
# {
|
19
|
+
# "name": "answer",
|
20
|
+
# "label": "Answer",
|
21
|
+
# "type": "text"
|
22
|
+
# }
|
23
|
+
# ]
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
class CustomType
|
27
|
+
REQUIRED_KEYS = %w[key name attributes].freeze
|
28
|
+
|
29
|
+
attr_reader :schema, :errors
|
30
|
+
|
31
|
+
def initialize(schema:)
|
32
|
+
@schema = schema
|
33
|
+
@errors = []
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate
|
37
|
+
ensure_valid_format &&
|
38
|
+
ensure_has_required_keys &&
|
39
|
+
ensure_no_unrecognized_keys &&
|
40
|
+
ensure_keys_are_correct_types &&
|
41
|
+
ensure_key_value_is_not_reserved &&
|
42
|
+
ensure_no_duplicate_attributes &&
|
43
|
+
ensure_attributes_are_valid &&
|
44
|
+
ensure_first_attribute_not_array
|
45
|
+
|
46
|
+
errors.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def ensure_valid_format
|
52
|
+
return true if schema.is_a?(Hash)
|
53
|
+
|
54
|
+
@errors << "Schema is not in a valid format"
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def ensure_has_required_keys
|
59
|
+
missing_keys = REQUIRED_KEYS - schema.keys
|
60
|
+
return true if missing_keys.empty?
|
61
|
+
|
62
|
+
@errors << "Missing required keys: #{missing_keys.join(', ')}"
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def ensure_no_unrecognized_keys
|
67
|
+
unrecognized_keys = schema.keys - REQUIRED_KEYS
|
68
|
+
return true if unrecognized_keys.empty?
|
69
|
+
|
70
|
+
@errors << "Unrecognized keys: #{unrecognized_keys.join(', ')}"
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_keys_are_correct_types
|
75
|
+
if !schema["key"].is_a?(String)
|
76
|
+
@errors << "\"key\" must be a string"
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
|
80
|
+
if !schema["name"].is_a?(String)
|
81
|
+
@errors << "\"name\" must be a string"
|
82
|
+
return false
|
83
|
+
end
|
84
|
+
|
85
|
+
if !schema["attributes"].is_a?(Array) ||
|
86
|
+
schema["attributes"].empty? ||
|
87
|
+
schema["attributes"].any? { |a| !a.is_a?(Hash) }
|
88
|
+
@errors << "\"attributes\" must be an array of objects"
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Ensuring the value for key doesn't clash with our primitive type keys.
|
96
|
+
# See {Canvas::Constants::PRIMITIVE_TYPES}
|
97
|
+
def ensure_key_value_is_not_reserved
|
98
|
+
if schema["key"] && Constants::PRIMITIVE_TYPES.include?(schema["key"].downcase)
|
99
|
+
@errors << "\"key\" can't be one of these reserved words: #{Constants::PRIMITIVE_TYPES.join(', ')}"
|
100
|
+
return false
|
101
|
+
end
|
102
|
+
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def ensure_no_duplicate_attributes
|
107
|
+
attribute_names = schema["attributes"].map { |a| a["name"] }
|
108
|
+
duplicated_names = attribute_names.select { |a| attribute_names.count(a) > 1 }.uniq
|
109
|
+
|
110
|
+
return true if duplicated_names.empty?
|
111
|
+
|
112
|
+
@errors << "Some attributes are duplicated: #{duplicated_names.join(', ')}"
|
113
|
+
false
|
114
|
+
end
|
115
|
+
|
116
|
+
def ensure_attributes_are_valid
|
117
|
+
return true unless schema["attributes"]
|
118
|
+
|
119
|
+
schema["attributes"].each do |attribute_schema|
|
120
|
+
attr_validator = Validator::SchemaAttribute.new(
|
121
|
+
attribute: attribute_schema,
|
122
|
+
custom_types: []
|
123
|
+
)
|
124
|
+
next if attr_validator.validate
|
125
|
+
|
126
|
+
@errors << "Attribute \"#{attribute_schema['name']}\" is invalid "\
|
127
|
+
"- #{attr_validator.errors.join(', ')}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def ensure_first_attribute_not_array
|
132
|
+
first_attribute = schema.fetch("attributes").first
|
133
|
+
|
134
|
+
return true if first_attribute.nil? || first_attribute["array"] != true
|
135
|
+
|
136
|
+
@errors << "The first attribute cannot be an array"
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# We need to require this file t ensure it is loaded first
|
4
|
+
require_relative "menu_schema"
|
5
|
+
|
6
|
+
module Canvas
|
7
|
+
module Validator
|
8
|
+
# :documented:
|
9
|
+
# This class is used to validate a schema for a footer. For now the logic is exactly the
|
10
|
+
# same as the menu schema.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# {
|
14
|
+
# "max_item_levels": 2,
|
15
|
+
# "supports_open_new_tab": "true",
|
16
|
+
# "attributes": {
|
17
|
+
# "fixed": {
|
18
|
+
# "group": "design",
|
19
|
+
# "label": "Fixed when scrolling",
|
20
|
+
# "hint": "The menu will stay fixed to the top when scrolling down the page.",
|
21
|
+
# "type": "boolean",
|
22
|
+
# "default: "false"
|
23
|
+
# }
|
24
|
+
# }
|
25
|
+
# }
|
26
|
+
#
|
27
|
+
class FooterSchema < MenuSchema; end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "nokogiri"
|
2
4
|
require "liquid"
|
3
5
|
|
4
6
|
module Canvas
|
5
7
|
module Validator
|
8
|
+
# :documented:
|
6
9
|
class Html
|
7
|
-
LIQUID_TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
|
8
|
-
LIQUID_VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
|
10
|
+
LIQUID_TAG = /#{::Liquid::TagStart}.*?#{::Liquid::TagEnd}/om
|
11
|
+
LIQUID_VARIABLE = /#{::Liquid::VariableStart}.*?#{::Liquid::VariableEnd}/om
|
9
12
|
LIQUID_TAG_OR_VARIABLE = /#{LIQUID_TAG}|#{LIQUID_VARIABLE}/om
|
10
13
|
|
11
14
|
attr_reader :errors
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Canvas
|
6
|
+
module Validator
|
7
|
+
# :documented:
|
8
|
+
class Json
|
9
|
+
attr_reader :errors
|
10
|
+
|
11
|
+
def initialize(file)
|
12
|
+
@file = file
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate
|
16
|
+
::JSON.parse(@file)
|
17
|
+
true
|
18
|
+
rescue ::JSON::ParserError => e
|
19
|
+
@errors = [e.message]
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "nokogiri"
|
2
4
|
require "liquid"
|
3
5
|
|
4
6
|
module Canvas
|
5
7
|
module Validator
|
8
|
+
# :documented:
|
6
9
|
class Liquid
|
7
10
|
attr_reader :errors
|
8
11
|
|
@@ -19,7 +22,7 @@ module Canvas
|
|
19
22
|
)
|
20
23
|
true
|
21
24
|
rescue ::Liquid::SyntaxError => e
|
22
|
-
@errors = [e]
|
25
|
+
@errors = [e.message]
|
23
26
|
false
|
24
27
|
end
|
25
28
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Canvas
|
4
|
+
module Validator
|
5
|
+
# :documented:
|
6
|
+
# This class is used to validate a schema for a menu.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
# {
|
10
|
+
# "max_item_levels": 2,
|
11
|
+
# "supports_open_new_tab": "true",
|
12
|
+
# "attributes": {
|
13
|
+
# "fixed": {
|
14
|
+
# "group": "design",
|
15
|
+
# "label": "Fixed when scrolling",
|
16
|
+
# "hint": "The menu will stay fixed to the top when scrolling down the page.",
|
17
|
+
# "type": "boolean",
|
18
|
+
# "default: "false"
|
19
|
+
# }
|
20
|
+
# }
|
21
|
+
# }
|
22
|
+
#
|
23
|
+
class MenuSchema
|
24
|
+
PERMITTED_KEYS = %w[max_item_levels supports_open_new_tab attributes].freeze
|
25
|
+
ADDITIONAL_RESERVED_NAMES = %w[items type].freeze
|
26
|
+
|
27
|
+
attr_reader :schema, :errors
|
28
|
+
|
29
|
+
# @param schema [Hash] the schema to be validated
|
30
|
+
# @param custom_types [Array<Hash>] a list of custom types
|
31
|
+
def initialize(schema:, custom_types: [])
|
32
|
+
@schema = schema
|
33
|
+
@custom_types = custom_types
|
34
|
+
@errors = []
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate
|
38
|
+
if ensure_valid_format
|
39
|
+
ensure_no_unrecognized_keys
|
40
|
+
ensure_max_item_levels_is_valid
|
41
|
+
ensure_attributes_are_valid
|
42
|
+
end
|
43
|
+
|
44
|
+
@errors.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def ensure_valid_format
|
50
|
+
return true if schema.is_a?(Hash) &&
|
51
|
+
(schema["attributes"].nil? || attributes_array_of_hashes?(schema))
|
52
|
+
|
53
|
+
@errors << "Schema is not in a valid format"
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
def ensure_no_unrecognized_keys
|
58
|
+
unrecognized_keys = schema.keys - PERMITTED_KEYS
|
59
|
+
return true if unrecognized_keys.empty?
|
60
|
+
|
61
|
+
@errors << "Unrecognized keys: #{unrecognized_keys.join(', ')}"
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
def ensure_max_item_levels_is_valid
|
66
|
+
return true unless schema.key?("max_item_levels")
|
67
|
+
return true if [1, 2, 3].include?(schema["max_item_levels"].to_i)
|
68
|
+
|
69
|
+
@errors << "\"max_item_levels\" must be a number between 1 and 3"
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
73
|
+
def ensure_attributes_are_valid
|
74
|
+
return true unless schema["attributes"]
|
75
|
+
|
76
|
+
schema["attributes"].each do |attribute_schema|
|
77
|
+
attr_validator = Validator::SchemaAttribute.new(
|
78
|
+
attribute: attribute_schema,
|
79
|
+
custom_types: @custom_types,
|
80
|
+
additional_reserved_names: ADDITIONAL_RESERVED_NAMES
|
81
|
+
)
|
82
|
+
next if attr_validator.validate
|
83
|
+
|
84
|
+
@errors << "Attribute \"#{attribute_schema['name']}\" is invalid "\
|
85
|
+
"- #{attr_validator.errors.join(', ')}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def attributes_array_of_hashes?(schema)
|
90
|
+
schema["attributes"].is_a?(Array) &&
|
91
|
+
schema["attributes"].all? { |attr| attr.is_a?(Hash) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sassc"
|
4
|
+
|
5
|
+
module Canvas
|
6
|
+
module Validator
|
7
|
+
# :documented:
|
8
|
+
#
|
9
|
+
# This validator can be used to validate Sass.
|
10
|
+
class Sass
|
11
|
+
attr_reader :errors
|
12
|
+
|
13
|
+
def initialize(file)
|
14
|
+
@file = file
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
SassC::Engine.new(@file, style: :compressed).render
|
19
|
+
true
|
20
|
+
rescue SassC::SyntaxError => e
|
21
|
+
@errors = [e.message]
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
require "liquid"
|
5
|
+
|
6
|
+
module Canvas
|
7
|
+
module Validator
|
8
|
+
# :documented:
|
9
|
+
# This class can be used to validate the format of an attribute that is used
|
10
|
+
# within a schema.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# {
|
14
|
+
# "name" => "headings",
|
15
|
+
# "type" => "string",
|
16
|
+
# "default" => "My Heading",
|
17
|
+
# "array" => true
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
class SchemaAttribute
|
21
|
+
VALIDATORS = {
|
22
|
+
"image" => SchemaAttribute::Image,
|
23
|
+
"product" => SchemaAttribute::Product,
|
24
|
+
"post" => SchemaAttribute::Post,
|
25
|
+
"page" => SchemaAttribute::Page,
|
26
|
+
"link" => SchemaAttribute::Link,
|
27
|
+
"text" => SchemaAttribute::Base,
|
28
|
+
"string" => SchemaAttribute::Base,
|
29
|
+
"boolean" => SchemaAttribute::Base,
|
30
|
+
"number" => SchemaAttribute::Number,
|
31
|
+
"color" => SchemaAttribute::Color,
|
32
|
+
"select" => SchemaAttribute::Select,
|
33
|
+
"range" => SchemaAttribute::Range,
|
34
|
+
"radio" => SchemaAttribute::Radio,
|
35
|
+
"variant" => SchemaAttribute::Variant,
|
36
|
+
}.freeze
|
37
|
+
RESERVED_NAMES = %w[
|
38
|
+
page
|
39
|
+
company
|
40
|
+
cart
|
41
|
+
flash
|
42
|
+
block
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
attr_reader :attribute, :custom_types, :errors, :additional_reserved_names
|
46
|
+
|
47
|
+
def initialize(attribute:, custom_types: [], additional_reserved_names: [])
|
48
|
+
@attribute = attribute
|
49
|
+
@custom_types = custom_types
|
50
|
+
@errors = []
|
51
|
+
@additional_reserved_names = additional_reserved_names
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate
|
55
|
+
ensure_attribute_is_hash &&
|
56
|
+
ensure_not_reserved_name &&
|
57
|
+
ensure_not_boolean_array &&
|
58
|
+
ensure_not_radio_array &&
|
59
|
+
ensure_type_key_is_present &&
|
60
|
+
ensure_valid_for_type &&
|
61
|
+
ensure_composite_array_without_non_array_attributes
|
62
|
+
|
63
|
+
errors.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def attribute_type
|
69
|
+
attribute["type"]&.downcase
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_types
|
73
|
+
Constants::PRIMITIVE_TYPES + custom_type_keys
|
74
|
+
end
|
75
|
+
|
76
|
+
def validator_for_type
|
77
|
+
@_validator ||= VALIDATORS[attribute_type].new(attribute) if VALIDATORS[attribute_type]
|
78
|
+
end
|
79
|
+
|
80
|
+
def custom_type_keys
|
81
|
+
custom_types.map { |type| type["key"]&.downcase }.compact
|
82
|
+
end
|
83
|
+
|
84
|
+
def ensure_type_key_is_present
|
85
|
+
return true if attribute.key?("type")
|
86
|
+
|
87
|
+
@errors << "Missing required keys: type"
|
88
|
+
false
|
89
|
+
end
|
90
|
+
|
91
|
+
def ensure_valid_for_type
|
92
|
+
return true if custom_type_keys.include?(attribute_type)
|
93
|
+
|
94
|
+
if validator_for_type.nil?
|
95
|
+
@errors << "\"type\" must be one of: #{valid_types.join(', ')}"
|
96
|
+
false
|
97
|
+
else
|
98
|
+
validator_for_type.validate
|
99
|
+
errors.concat(validator_for_type.errors)
|
100
|
+
validator_for_type.errors.empty?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def ensure_attribute_is_hash
|
105
|
+
return true if attribute.is_a? Hash
|
106
|
+
|
107
|
+
@errors << "Must be valid JSON"
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def ensure_not_reserved_name
|
112
|
+
all_reserved_names = RESERVED_NAMES + additional_reserved_names
|
113
|
+
return true unless all_reserved_names.include?(attribute["name"])
|
114
|
+
|
115
|
+
@errors << "\"name\" can't be one of these reserved words: #{all_reserved_names.join(', ')}"
|
116
|
+
false
|
117
|
+
end
|
118
|
+
|
119
|
+
def ensure_not_boolean_array
|
120
|
+
return true unless attribute["type"] == "boolean" && attribute["array"] == true
|
121
|
+
|
122
|
+
@errors << "Boolean attributes cannot be arrays"
|
123
|
+
false
|
124
|
+
end
|
125
|
+
|
126
|
+
def ensure_not_radio_array
|
127
|
+
return true unless attribute["type"] == "radio" && attribute["array"] == true
|
128
|
+
|
129
|
+
@errors << "Radio attributes cannot be arrays"
|
130
|
+
false
|
131
|
+
end
|
132
|
+
|
133
|
+
def ensure_composite_array_without_non_array_attributes
|
134
|
+
custom_type = custom_types.find { |type| type["key"]&.downcase == attribute_type }
|
135
|
+
|
136
|
+
return true if !custom_type || attribute["array"] != true
|
137
|
+
|
138
|
+
sub_attributes = custom_type.fetch("attributes", [])
|
139
|
+
return true unless sub_attributes.any? { |attribute| attribute["type"] == "radio" }
|
140
|
+
|
141
|
+
@errors << "Cannot be an array because \"#{custom_type['key']}\" type includes nonarray types"
|
142
|
+
false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Canvas
|
4
|
+
module Validator
|
5
|
+
class SchemaAttribute
|
6
|
+
# :documented:
|
7
|
+
#
|
8
|
+
# Validations to run against an attribute's schema that are shared
|
9
|
+
# across types. This class acts as the base class for type-specific
|
10
|
+
# validators.
|
11
|
+
class Base
|
12
|
+
attr_reader :attribute, :errors
|
13
|
+
|
14
|
+
def initialize(attribute)
|
15
|
+
@attribute = attribute
|
16
|
+
@errors = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate
|
20
|
+
ensure_has_required_keys &&
|
21
|
+
ensure_no_unrecognized_keys &&
|
22
|
+
ensure_keys_are_correct_types
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# The base keys required for this attribute to be valid and their
|
28
|
+
# expected types. Types can also be specified as an array of discrete
|
29
|
+
# expected values.
|
30
|
+
# This can be overwritten in sub-validator class.
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
def required_keys
|
34
|
+
{
|
35
|
+
"name" => String,
|
36
|
+
"type" => String
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Either a class or array of values we expect the default to be.
|
41
|
+
# This can be overwritten in sub-validator class.
|
42
|
+
def permitted_values_for_default_key
|
43
|
+
Object
|
44
|
+
end
|
45
|
+
|
46
|
+
# The optional keys that can be supplied for this attribute and their
|
47
|
+
# expected types. Types can also be specified as an array of discrete
|
48
|
+
# expected values.
|
49
|
+
# This can be overwritten in sub-validator class.
|
50
|
+
#
|
51
|
+
# @return [Hash]
|
52
|
+
def optional_keys
|
53
|
+
{
|
54
|
+
"default" => permitted_values_for_default_key,
|
55
|
+
"array" => [true, false],
|
56
|
+
"label" => String,
|
57
|
+
"hint" => String,
|
58
|
+
"group" => %w[content layout design mobile]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def all_permitted_keys
|
63
|
+
required_keys.merge(optional_keys)
|
64
|
+
end
|
65
|
+
|
66
|
+
def ensure_has_required_keys
|
67
|
+
missing_keys = required_keys.keys - attribute.keys
|
68
|
+
return true if missing_keys.empty?
|
69
|
+
|
70
|
+
@errors << "Missing required keys: #{missing_keys.join(', ')}"
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_no_unrecognized_keys
|
75
|
+
unrecognized_keys = attribute.keys - all_permitted_keys.keys
|
76
|
+
return true if unrecognized_keys.empty?
|
77
|
+
|
78
|
+
@errors << "Unrecognized keys: #{unrecognized_keys.join(', ')}"
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def ensure_keys_are_correct_types
|
83
|
+
all_permitted_keys.each do |key, expected|
|
84
|
+
if expected.is_a?(Class)
|
85
|
+
if attribute.key?(key) && !attribute[key].is_a?(expected)
|
86
|
+
actual = attribute[key].class.name
|
87
|
+
@errors << "\"#{key}\" is a #{actual}, expected #{expected}"
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
else
|
91
|
+
if attribute.key?(key) && !expected.include?(attribute[key])
|
92
|
+
actual = attribute[key].to_s
|
93
|
+
@errors << "\"#{key}\" is '#{actual}', expected one of: #{[*expected].join(', ')}"
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Canvas
|
4
|
+
module Validator
|
5
|
+
class SchemaAttribute
|
6
|
+
# :documented:
|
7
|
+
# Attribute validations specific to color-type variables.
|
8
|
+
class Color < Base
|
9
|
+
def validate
|
10
|
+
super &&
|
11
|
+
ensure_default_keys_are_valid &&
|
12
|
+
ensure_default_values_are_valid
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def permitted_values_for_default_key
|
18
|
+
Hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def ensure_default_keys_are_valid
|
22
|
+
return true unless attribute.key?("default")
|
23
|
+
return true if attribute["default"].key?("palette")
|
24
|
+
|
25
|
+
key_diff = %w[r g b] - attribute["default"].keys
|
26
|
+
if key_diff.any? && key_diff != ["a"]
|
27
|
+
@errors << "\"default\" for color-type variables must include palette or rgba values"
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def ensure_default_values_are_valid
|
35
|
+
return true unless attribute.key?("default")
|
36
|
+
|
37
|
+
if attribute["default"].key?("palette")
|
38
|
+
ensure_palette_value_is_valid
|
39
|
+
else
|
40
|
+
ensure_rgb_value_is_valid
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def ensure_palette_value_is_valid
|
45
|
+
return true if Constants::COLOR_PALETTE_VALUES.include?(attribute["default"]["palette"])
|
46
|
+
|
47
|
+
@errors << "\"default\" value for palette color-type must be one of "\
|
48
|
+
"the following values: #{Constants::COLOR_PALETTE_VALUES.join(', ')}"
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def ensure_rgb_value_is_valid
|
53
|
+
invalid_rgb_error = "\"default\" values for color-type variables must be "\
|
54
|
+
"between 0 and 255 for rgb, and between 0 and 1 for a"
|
55
|
+
|
56
|
+
attribute["default"].each do |key, value|
|
57
|
+
if %w[r g b].include?(key) && !valid_rgb_value?(value)
|
58
|
+
@errors << invalid_rgb_error
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
|
62
|
+
if key == "a" && !valid_alpha_value?(value)
|
63
|
+
@errors << invalid_rgb_error
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid_rgb_value?(value)
|
72
|
+
value.to_s.match?(/\A\d+\z/) && value.to_i.between?(0, 255)
|
73
|
+
end
|
74
|
+
|
75
|
+
def valid_alpha_value?(value)
|
76
|
+
value.to_s.match?(/([0-9]*[.])?[0-9]+/) && value.to_f.between?(0, 1)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|