easol-canvas 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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