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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/canvas/check.rb +4 -1
  3. data/lib/canvas/checks/required_files_check.rb +3 -0
  4. data/lib/canvas/checks/valid_block_schemas_check.rb +72 -0
  5. data/lib/canvas/checks/valid_custom_types_check.rb +36 -0
  6. data/lib/canvas/checks/valid_footer_schema_check.rb +81 -0
  7. data/lib/canvas/checks/valid_html_check.rb +3 -0
  8. data/lib/canvas/checks/valid_json_check.rb +27 -0
  9. data/lib/canvas/checks/valid_liquid_check.rb +4 -1
  10. data/lib/canvas/checks/valid_menu_schema_check.rb +81 -0
  11. data/lib/canvas/checks/valid_sass_check.rb +30 -0
  12. data/lib/canvas/checks.rb +4 -1
  13. data/lib/canvas/cli.rb +10 -1
  14. data/lib/canvas/constants.rb +31 -0
  15. data/lib/canvas/lint.rb +7 -4
  16. data/lib/canvas/offense.rb +3 -0
  17. data/lib/canvas/services/expand_attributes.rb +40 -0
  18. data/lib/canvas/services/fetch_custom_types.rb +27 -0
  19. data/lib/canvas/services/front_matter_extractor.rb +1 -0
  20. data/lib/canvas/validators/block_schema.rb +86 -0
  21. data/lib/canvas/validators/custom_type.rb +141 -0
  22. data/lib/canvas/validators/footer_schema.rb +29 -0
  23. data/lib/canvas/validators/html.rb +5 -2
  24. data/lib/canvas/validators/json.rb +24 -0
  25. data/lib/canvas/validators/liquid.rb +4 -1
  26. data/lib/canvas/validators/menu_schema.rb +95 -0
  27. data/lib/canvas/validators/sass.rb +26 -0
  28. data/lib/canvas/validators/schema_attribute.rb +146 -0
  29. data/lib/canvas/validators/schema_attributes/base.rb +104 -0
  30. data/lib/canvas/validators/schema_attributes/color.rb +81 -0
  31. data/lib/canvas/validators/schema_attributes/image.rb +45 -0
  32. data/lib/canvas/validators/schema_attributes/link.rb +51 -0
  33. data/lib/canvas/validators/schema_attributes/number.rb +17 -0
  34. data/lib/canvas/validators/schema_attributes/page.rb +49 -0
  35. data/lib/canvas/validators/schema_attributes/post.rb +49 -0
  36. data/lib/canvas/validators/schema_attributes/product.rb +49 -0
  37. data/lib/canvas/validators/schema_attributes/radio.rb +55 -0
  38. data/lib/canvas/validators/schema_attributes/range.rb +24 -0
  39. data/lib/canvas/validators/schema_attributes/select.rb +59 -0
  40. data/lib/canvas/validators/schema_attributes/variant.rb +49 -0
  41. data/lib/canvas/version.rb +3 -1
  42. data/lib/canvas.rb +30 -12
  43. data/lib/easol/canvas.rb +4 -0
  44. 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