easol-canvas 0.1.1 → 1.2.0

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