easol-canvas 0.1.1 → 1.0.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/canvas/checks/valid_block_schemas_check.rb +72 -0
  3. data/lib/canvas/checks/valid_footer_schema_check.rb +81 -0
  4. data/lib/canvas/checks/valid_json_check.rb +24 -0
  5. data/lib/canvas/checks/valid_liquid_check.rb +1 -1
  6. data/lib/canvas/checks/valid_menu_schema_check.rb +81 -0
  7. data/lib/canvas/constants.rb +12 -0
  8. data/lib/canvas/services/expand_attributes.rb +40 -0
  9. data/lib/canvas/services/fetch_custom_types.rb +27 -0
  10. data/lib/canvas/validators/block_schema.rb +86 -0
  11. data/lib/canvas/validators/footer_schema.rb +29 -0
  12. data/lib/canvas/validators/html.rb +2 -2
  13. data/lib/canvas/validators/json.rb +21 -0
  14. data/lib/canvas/validators/liquid.rb +1 -1
  15. data/lib/canvas/validators/menu_schema.rb +95 -0
  16. data/lib/canvas/validators/schema_attribute.rb +147 -0
  17. data/lib/canvas/validators/schema_attributes/base.rb +104 -0
  18. data/lib/canvas/validators/schema_attributes/color.rb +81 -0
  19. data/lib/canvas/validators/schema_attributes/image.rb +45 -0
  20. data/lib/canvas/validators/schema_attributes/link.rb +51 -0
  21. data/lib/canvas/validators/schema_attributes/number.rb +17 -0
  22. data/lib/canvas/validators/schema_attributes/page.rb +49 -0
  23. data/lib/canvas/validators/schema_attributes/post.rb +49 -0
  24. data/lib/canvas/validators/schema_attributes/product.rb +49 -0
  25. data/lib/canvas/validators/schema_attributes/radio.rb +55 -0
  26. data/lib/canvas/validators/schema_attributes/range.rb +24 -0
  27. data/lib/canvas/validators/schema_attributes/select.rb +59 -0
  28. data/lib/canvas/validators/schema_attributes/variant.rb +49 -0
  29. data/lib/canvas/version.rb +1 -1
  30. data/lib/canvas.rb +14 -3
  31. metadata +43 -19
@@ -0,0 +1,147 @@
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
+ PRIMITIVE_TYPES = VALIDATORS.keys
38
+ RESERVED_NAMES = %w[
39
+ page
40
+ company
41
+ cart
42
+ flash
43
+ block
44
+ ].freeze
45
+
46
+ attr_reader :attribute, :custom_types, :errors, :additional_reserved_names
47
+
48
+ def initialize(attribute:, custom_types: [], additional_reserved_names: [])
49
+ @attribute = attribute
50
+ @custom_types = custom_types
51
+ @errors = []
52
+ @additional_reserved_names = additional_reserved_names
53
+ end
54
+
55
+ def validate
56
+ ensure_attribute_is_hash &&
57
+ ensure_not_reserved_name &&
58
+ ensure_not_boolean_array &&
59
+ ensure_not_radio_array &&
60
+ ensure_type_key_is_present &&
61
+ ensure_valid_for_type &&
62
+ ensure_composite_array_without_non_array_attributes
63
+
64
+ errors.empty?
65
+ end
66
+
67
+ private
68
+
69
+ def attribute_type
70
+ attribute["type"]&.downcase
71
+ end
72
+
73
+ def valid_types
74
+ PRIMITIVE_TYPES + custom_type_keys
75
+ end
76
+
77
+ def validator_for_type
78
+ @_validator ||= VALIDATORS[attribute_type].new(attribute) if VALIDATORS[attribute_type]
79
+ end
80
+
81
+ def custom_type_keys
82
+ custom_types.map { |type| type["key"]&.downcase }.compact
83
+ end
84
+
85
+ def ensure_type_key_is_present
86
+ return true if attribute.key?("type")
87
+
88
+ @errors << "Missing required keys: type"
89
+ false
90
+ end
91
+
92
+ def ensure_valid_for_type
93
+ return true if custom_type_keys.include?(attribute_type)
94
+
95
+ if validator_for_type.nil?
96
+ @errors << "\"type\" must be one of: #{valid_types.join(', ')}"
97
+ false
98
+ else
99
+ validator_for_type.validate
100
+ errors.concat(validator_for_type.errors)
101
+ validator_for_type.errors.empty?
102
+ end
103
+ end
104
+
105
+ def ensure_attribute_is_hash
106
+ return true if attribute.is_a? Hash
107
+
108
+ @errors << "Must be valid JSON"
109
+ false
110
+ end
111
+
112
+ def ensure_not_reserved_name
113
+ all_reserved_names = RESERVED_NAMES + additional_reserved_names
114
+ return true unless all_reserved_names.include?(attribute["name"])
115
+
116
+ @errors << "\"name\" can't be one of these reserved words: #{all_reserved_names.join(', ')}"
117
+ false
118
+ end
119
+
120
+ def ensure_not_boolean_array
121
+ return true unless attribute["type"] == "boolean" && attribute["array"] == true
122
+
123
+ @errors << "Boolean attributes cannot be arrays"
124
+ false
125
+ end
126
+
127
+ def ensure_not_radio_array
128
+ return true unless attribute["type"] == "radio" && attribute["array"] == true
129
+
130
+ @errors << "Radio attributes cannot be arrays"
131
+ false
132
+ end
133
+
134
+ def ensure_composite_array_without_non_array_attributes
135
+ custom_type = custom_types.find { |type| type["key"]&.downcase == attribute_type }
136
+
137
+ return true if !custom_type || attribute["array"] != true
138
+
139
+ sub_attributes = custom_type.fetch("attributes", [])
140
+ return true unless sub_attributes.any? { |attribute| attribute["type"] == "radio" }
141
+
142
+ @errors << "Cannot be an array because \"#{custom_type['key']}\" type includes nonarray types"
143
+ false
144
+ end
145
+ end
146
+ end
147
+ 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
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to link-type variables.
8
+ class Image < Base
9
+ ALLOWED_DEFAULT_KEYS = %w[url asset].freeze
10
+
11
+ def validate
12
+ super && ensure_default_key_is_valid
13
+ end
14
+
15
+ private
16
+
17
+ def ensure_default_key_is_valid
18
+ return true unless attribute.key?("default")
19
+
20
+ if attribute["array"]
21
+ attribute["default"].all? { |value| default_value_is_validate(value) }
22
+ else
23
+ default_value_is_validate(attribute["default"])
24
+ end
25
+ end
26
+
27
+ # The default value can be one of 3 different formats:
28
+ # - A string (for backwards compatibility) e.g. "http://my-image.jpg"
29
+ # - A hash with "url" key" e.g. { "url": "http://my-image.jpg" }
30
+ # - A hash with "asset" key" e.g. { "asset": "http://my-image.jpg" }
31
+ def default_value_is_validate(value)
32
+ return true if value.is_a?(String)
33
+
34
+ return true if value.is_a?(Hash) &&
35
+ value.keys.size == 1 &&
36
+ ALLOWED_DEFAULT_KEYS.include?(value.keys.first) &&
37
+ value.values.first.is_a?(String)
38
+
39
+ @errors << "\"default\" for image-type variables must include a single url or asset value"
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to link-type variables.
8
+ class Link < Base
9
+ ALLOWED_DEFAULT_KEYS = %w[url page post product].freeze
10
+ INVALID_DEFAULT_ERROR = "\"default\" for link-type variables must include "\
11
+ "a single url, page, post or product value"
12
+
13
+ def validate
14
+ super &&
15
+ ensure_single_default_provided &&
16
+ ensure_default_key_is_valid
17
+ end
18
+
19
+ private
20
+
21
+ def permitted_values_for_default_key
22
+ Hash
23
+ end
24
+
25
+ def ensure_default_key_is_valid
26
+ return true unless attribute.key?("default")
27
+
28
+ key = attribute["default"].keys.first
29
+
30
+ unless ALLOWED_DEFAULT_KEYS.include?(key)
31
+ @errors << INVALID_DEFAULT_ERROR
32
+ return false
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ def ensure_single_default_provided
39
+ return true unless attribute.key?("default")
40
+
41
+ if attribute["default"].count != 1
42
+ @errors << INVALID_DEFAULT_ERROR
43
+ return false
44
+ end
45
+
46
+ true
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to number-type variables.
8
+ class Number < Base
9
+ private
10
+
11
+ def optional_keys
12
+ super.merge("unit" => String)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to page-type variables.
8
+ class Page < Base
9
+ ALLOWED_DEFAULT_VALUES = %w[ random ].freeze
10
+
11
+ def validate
12
+ super &&
13
+ ensure_default_values_are_valid
14
+ end
15
+
16
+ private
17
+
18
+ def permitted_values_for_default_key
19
+ if attribute["array"]
20
+ Array
21
+ else
22
+ String
23
+ end
24
+ end
25
+
26
+ def ensure_default_values_are_valid
27
+ return true unless attribute.key?("default")
28
+
29
+ if attribute["array"]
30
+ attribute["default"].all? { |value| default_value_is_validate(value) }
31
+ else
32
+ default_value_is_validate(attribute["default"])
33
+ end
34
+ end
35
+
36
+ def default_value_is_validate(value)
37
+ value = value.downcase
38
+ if !ALLOWED_DEFAULT_VALUES.include?(value)
39
+ @errors << "\"default\" for page-type variables must be "\
40
+ "one of: #{ALLOWED_DEFAULT_VALUES.join(', ')}"
41
+ false
42
+ else
43
+ true
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to post-type variables.
8
+ class Post < Base
9
+ ALLOWED_DEFAULT_VALUES = %w[random].freeze
10
+
11
+ def validate
12
+ super &&
13
+ ensure_default_values_are_valid
14
+ end
15
+
16
+ private
17
+
18
+ def permitted_values_for_default_key
19
+ if attribute["array"]
20
+ Array
21
+ else
22
+ String
23
+ end
24
+ end
25
+
26
+ def ensure_default_values_are_valid
27
+ return true unless attribute.key?("default")
28
+
29
+ if attribute["array"]
30
+ attribute["default"].all? { |value| default_value_is_valid?(value) }
31
+ else
32
+ default_value_is_valid?(attribute["default"])
33
+ end
34
+ end
35
+
36
+ def default_value_is_valid?(value)
37
+ value = value.downcase
38
+ if !ALLOWED_DEFAULT_VALUES.include?(value)
39
+ @errors << "\"default\" for post-type variables must be "\
40
+ "one of: #{ALLOWED_DEFAULT_VALUES.join(', ')}"
41
+ false
42
+ else
43
+ true
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to product-type variables.
8
+ class Product < Base
9
+ ALLOWED_DEFAULT_VALUES = %w[random].freeze
10
+
11
+ def validate
12
+ super &&
13
+ ensure_default_values_are_valid
14
+ end
15
+
16
+ private
17
+
18
+ def permitted_values_for_default_key
19
+ if attribute["array"]
20
+ Array
21
+ else
22
+ String
23
+ end
24
+ end
25
+
26
+ def ensure_default_values_are_valid
27
+ return true unless attribute.key?("default")
28
+
29
+ if attribute["array"]
30
+ attribute["default"].all? { |value| default_value_is_valid?(value) }
31
+ else
32
+ default_value_is_valid?(attribute["default"])
33
+ end
34
+ end
35
+
36
+ def default_value_is_valid?(value)
37
+ value = value.downcase
38
+ if !ALLOWED_DEFAULT_VALUES.include?(value)
39
+ @errors << "\"default\" for product-type variables must be "\
40
+ "one of: #{ALLOWED_DEFAULT_VALUES.join(', ')}"
41
+ false
42
+ else
43
+ true
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canvas
4
+ module Validator
5
+ class SchemaAttribute
6
+ # :documented:
7
+ # Attribute validations specific to radio-type variables.
8
+ class Radio < Base
9
+ def validate
10
+ super &&
11
+ ensure_at_least_one_option &&
12
+ ensure_options_are_valid
13
+ end
14
+
15
+ private
16
+
17
+ def permitted_values_for_default_key
18
+ if attribute["options"].is_a?(Array)
19
+ attribute["options"].map { |option| option["value"] }
20
+ else
21
+ []
22
+ end
23
+ end
24
+
25
+ def required_keys
26
+ super.merge(
27
+ "options" => Array
28
+ )
29
+ end
30
+
31
+ def ensure_at_least_one_option
32
+ return true if attribute["options"].length.positive?
33
+
34
+ @errors << "Must provide at least 1 option for radio type variable"
35
+ false
36
+ end
37
+
38
+ def ensure_options_are_valid
39
+ return true if attribute["options"].all? { |option|
40
+ select_option_valid?(option)
41
+ }
42
+
43
+ @errors << "All options for radio type variable must specify a label and value"
44
+ false
45
+ end
46
+
47
+ def select_option_valid?(option)
48
+ option.is_a?(Hash) &&
49
+ option.key?("value") &&
50
+ option.key?("label")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Canvas
6
+ module Validator
7
+ class SchemaAttribute
8
+ # :documented:
9
+ # Attribute validations specific to range-type variables.
10
+ class Range < Base
11
+ private
12
+
13
+ def optional_keys
14
+ super.merge(
15
+ "min" => Object,
16
+ "max" => Object,
17
+ "step" => Object,
18
+ "unit" => String
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end