easol-canvas 1.2.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97ee44a992eb400c6fe2eedd35e6bb395ebc05e2f59f039ea9335194ff64fe83
4
- data.tar.gz: 2684ff4ca3910fab9f0cd3bc36cb0c847f31565b1d3f8576964102a28773f247
3
+ metadata.gz: c61a6785fbf34da110e1fe2b01a9f096ff4aaf8ea3cfbef780491b5e94e68998
4
+ data.tar.gz: 3acb2a3215a5fd874ad5e54cc719204cb0a4698218935f2546ce5cacf3f73777
5
5
  SHA512:
6
- metadata.gz: 28c155af546e6898977757280894cba8ab26942aa1883c5b3ff752f3d7256b68c0631b9d18b6e29df8629d79e94a488739d2c9d0d1d1563dc8f4672f91d164ff
7
- data.tar.gz: 3f743a067e8621d1b0893fe41afa8746b60dcc87a4398214b7d8b45ae66648088c22db84e2ed23b26df72c586f19b0547df3a6c8c354f36a005556262fef5a98
6
+ metadata.gz: 660512a56bea758cae95483fdaefd41a75156a7c15723e661749a64feb6dbc847bab31e279fecd733af807c33491a138b9c4afc850bfd2db84e8b1ae5d3c848a
7
+ data.tar.gz: 75fdbbefd7cfccc1849bf26e1c42e9f24f274c1d1ed27774d6b1a5085a5caafd30b4b8a0499d418686320157b6335116e0a25bf973b24fedee60210bb64c8d0d
@@ -14,11 +14,18 @@ module Canvas
14
14
 
15
15
  def run
16
16
  REQUIRED_FILES.each do |filename|
17
- next unless Dir.glob(filename).empty?
17
+ file_paths = Dir.glob(filename)
18
18
 
19
- @offenses << Offense.new(
20
- message: "Missing file: #{filename}"
21
- )
19
+
20
+ if file_paths.empty?
21
+ @offenses << Offense.new(
22
+ message: "Missing file: #{filename}"
23
+ )
24
+ elsif File.zero?(file_paths.first)
25
+ @offenses << Offense.new(
26
+ message: "Empty file: #{file_paths.first}"
27
+ )
28
+ end
22
29
  end
23
30
  end
24
31
  end
@@ -37,7 +37,8 @@ module Canvas
37
37
 
38
38
  def validate_format(filename, front_matter)
39
39
  return true if front_matter.is_a?(Hash) &&
40
- front_matter.values.all? { |attr| attr.is_a?(Hash) }
40
+ (front_matter.key?("attributes") ? front_matter["attributes"] : front_matter).values.all? { |attr| attr.is_a?(Hash) }
41
+
41
42
 
42
43
  @offenses << Offense.new(
43
44
  message: "Invalid Block Schema: #{filename} - \nSchema is not in a valid format"
@@ -45,8 +46,7 @@ module Canvas
45
46
  false
46
47
  end
47
48
 
48
- def validate_schema(filename, front_matter, custom_types)
49
- schema = extract_schema(front_matter)
49
+ def validate_schema(filename, schema, custom_types)
50
50
  validator = Validator::BlockSchema.new(schema: schema, custom_types: custom_types)
51
51
  return if validator.validate
52
52
 
@@ -62,11 +62,5 @@ module Canvas
62
62
  front_matter = extractor.front_matter
63
63
  front_matter.nil? ? {} : YAML.safe_load(front_matter)
64
64
  end
65
-
66
- def extract_schema(front_matter)
67
- {
68
- "attributes" => Canvas::ExpandAttributes.call(front_matter)
69
- }
70
- end
71
65
  end
72
66
  end
@@ -30,6 +30,7 @@ module Canvas
30
30
  # @return [Array<Hash>] array of hashes that represent each attribute
31
31
  def call(attributes_hash)
32
32
  return [] if attributes_hash.nil?
33
+ return attributes_hash if attributes_hash.is_a?(Array)
33
34
 
34
35
  attributes_hash.each_with_object([]) do |(name, attribute_hash), attrs|
35
36
  attrs << attribute_hash.merge("name" => name)
@@ -23,21 +23,24 @@ module Canvas
23
23
  # ]
24
24
  # }
25
25
  class BlockSchema
26
- PERMITTED_KEYS = %w[attributes].freeze
26
+ PERMITTED_KEYS = %w[attributes layout].freeze
27
27
 
28
28
  attr_reader :errors, :schema
29
29
 
30
30
  # @param schema [Hash] the schema to be validated
31
31
  # @param custom_types [Array<Hash>] a list of custom types
32
32
  def initialize(schema:, custom_types: [])
33
- @schema = schema
33
+ @schema = normalize_schema(schema)
34
34
  @custom_types = custom_types
35
35
  @errors = []
36
36
  end
37
37
 
38
38
  def validate
39
+ @errors = []
40
+
39
41
  if ensure_valid_format
40
42
  ensure_no_unrecognized_keys
43
+ ensure_layout_is_valid
41
44
  ensure_attributes_are_valid
42
45
  end
43
46
 
@@ -54,6 +57,16 @@ module Canvas
54
57
  false
55
58
  end
56
59
 
60
+ def ensure_layout_is_valid
61
+ return true unless schema["layout"]
62
+
63
+ layout_validator = LayoutSchema.new(schema: @schema)
64
+ return true if layout_validator.validate
65
+
66
+ @errors += layout_validator.errors
67
+ false
68
+ end
69
+
57
70
  def ensure_no_unrecognized_keys
58
71
  unrecognized_keys = schema.keys - PERMITTED_KEYS
59
72
  return true if unrecognized_keys.empty?
@@ -81,6 +94,19 @@ module Canvas
81
94
  schema["attributes"].is_a?(Array) &&
82
95
  schema["attributes"].all? { |attr| attr.is_a?(Hash) }
83
96
  end
97
+
98
+ def normalize_schema(schema)
99
+ if schema.key?("attributes")
100
+ {
101
+ **schema,
102
+ "attributes" => Canvas::ExpandAttributes.call(schema["attributes"])
103
+ }
104
+ else
105
+ {
106
+ "attributes" => Canvas::ExpandAttributes.call(schema)
107
+ }
108
+ end
109
+ end
84
110
  end
85
111
  end
86
112
  end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "json-schema"
5
+
6
+ module Canvas
7
+ module Validator
8
+ # :documented:
9
+ # This class is used to validate a layout definition, part of block schema.
10
+ # Example of a valid layout definition:
11
+ # {
12
+ # "attributes" => [
13
+ # {
14
+ # "name" => "title",
15
+ # "type" => "string"
16
+ # }
17
+ # ...
18
+ # ],
19
+ # "layout" => [
20
+ # {
21
+ # "label" => "Design",
22
+ # "type" => "tab",
23
+ # "elements" => [
24
+ # "heading",
25
+ # {
26
+ # "type" => "accordion",
27
+ # "label" => "Logo",
28
+ # "elements" => [
29
+ # "description",
30
+ # { "type" => "attribute", "name" => "logo_alt" },
31
+ # "title"
32
+ # ]
33
+ # },
34
+ # {
35
+ # "type" => "accordion_toggle",
36
+ # "toggle_attribute" => "cta_enabled",
37
+ # "elements" => [
38
+ # "cta_text",
39
+ # "cta_target"
40
+ # ]
41
+ # }
42
+ # ]
43
+ # }
44
+ # ]
45
+ # }
46
+ class LayoutSchema
47
+ attr_reader :errors
48
+
49
+ def initialize(schema:)
50
+ @schema = schema
51
+ @errors = []
52
+ end
53
+
54
+ def validate
55
+ @errors = []
56
+
57
+ if ensure_valid_format
58
+ ensure_no_unrecognized_keys
59
+ ensure_no_duplicate_keys
60
+ ensure_accordion_toggles_are_valid
61
+ end
62
+
63
+ @errors.empty?
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :schema
69
+
70
+ def ensure_no_duplicate_keys
71
+ attributes = fetch_all_attribute_names
72
+ duplicates =
73
+ attributes
74
+ .group_by { |(key)| key }
75
+ .filter { |key, usage| usage.size > 1 }
76
+
77
+ unless duplicates.empty?
78
+ duplicates.each do |attribute, usage|
79
+ @errors << "Duplicated attribute key `#{attribute}` found. Location: #{usage.map { |(_, location)| location }.join(", ")}"
80
+ end
81
+ end
82
+ end
83
+
84
+ def ensure_no_unrecognized_keys
85
+ attributes = fetch_all_attribute_names
86
+ defined_attributes = schema["attributes"]&.map { |definition| normalize_attribute(definition["name"]) } || []
87
+
88
+ attributes.each do |attribute, location|
89
+ @errors << "Unrecognized attribute `#{attribute}`. Location: #{location}" unless defined_attributes.include?(attribute)
90
+ end
91
+ end
92
+
93
+ # @return [Array<Array(String, String)>] an array of all the attribute names that
94
+ # are mentioned in the layout schema, along with its path. The names are
95
+ # normalized, i.e. downcased.
96
+ def fetch_all_attribute_names
97
+ attributes = fetch_elements_of_type("attribute")
98
+ attributes.map do |(node, path)|
99
+ [
100
+ normalize_attribute(node.is_a?(Hash) ? node["name"] : node),
101
+ path
102
+ ]
103
+ end
104
+ end
105
+
106
+ # @param type [String] the element type to fetch
107
+ # @return [Array<Array(<Hash, String>, String)] an array of elements that match
108
+ # the given type. Each element is an array containing the node and its path.
109
+ def fetch_elements_of_type(type)
110
+ elements = []
111
+
112
+ fetch_element = ->(node, path) {
113
+ if type == "attribute" && node.is_a?(String)
114
+ elements << [node, path]
115
+ elsif node["type"] == type
116
+ elements << [node, path]
117
+ end
118
+
119
+ if node.is_a?(Hash) && node.key?("elements")
120
+ node["elements"].each_with_index do |element, i|
121
+ current_path = "#{path}/elements/#{i}"
122
+ fetch_element.call(element, current_path)
123
+ end
124
+ end
125
+ }
126
+
127
+ layout_schema.each_with_index do |tab, i|
128
+ current_path = "layout/#{i}"
129
+ fetch_element.call(tab, current_path)
130
+ end
131
+
132
+ elements
133
+ end
134
+
135
+ def ensure_valid_format
136
+ result = JSON::Validator.fully_validate(schema_definition, { "layout" => layout_schema }, strict: true, clear_cache: true)
137
+
138
+ return true if result.empty?
139
+
140
+ @errors += result
141
+ false
142
+ end
143
+
144
+ def ensure_accordion_toggles_are_valid
145
+ accordion_toggles = fetch_elements_of_type("accordion_toggle")
146
+ accordion_toggles.each do |accordion_toggle, location|
147
+ toggle_attribute = schema["attributes"]&.detect { |attr|
148
+ attr["name"] == accordion_toggle["toggle_attribute"]
149
+ }
150
+
151
+ if toggle_attribute.nil?
152
+ @errors << "The toggle_attribute in accordion_toggle is unrecognized. Location: #{location}"
153
+ elsif toggle_attribute["type"] != "boolean"
154
+ @errors << "The toggle_attribute in accordion_toggle must be a boolean. Location: #{location}"
155
+ end
156
+ end
157
+ end
158
+
159
+ def layout_schema
160
+ schema["layout"] || []
161
+ end
162
+
163
+ def schema_definition
164
+ File.read(
165
+ File.join(File.dirname(__FILE__), "../../../", "schema_definitions", "block_layout.json")
166
+ )
167
+ end
168
+
169
+ def normalize_attribute(name)
170
+ name.strip.downcase
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canvas
4
- VERSION = "1.2.0"
4
+ VERSION = "1.5.0"
5
5
  end
@@ -0,0 +1,87 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "layout": {
5
+ "type": "array",
6
+ "items": { "$ref": "#/$defs/tab" }
7
+ }
8
+ },
9
+ "$defs": {
10
+ "tab": {
11
+ "type": "object",
12
+ "required": ["type", "label"],
13
+ "properties": {
14
+ "type": {
15
+ "type": "string",
16
+ "const": "tab"
17
+ },
18
+ "label": {
19
+ "type": "string"
20
+ },
21
+ "elements": {
22
+ "type": "array",
23
+ "minItems": 1,
24
+ "items": {
25
+ "oneOf": [
26
+ { "type": "string" },
27
+ { "$ref": "#/$defs/accordion" },
28
+ { "$ref": "#/$defs/accordion_toggle" },
29
+ { "$ref": "#/$defs/attribute" }
30
+ ]
31
+ }
32
+ }
33
+ }
34
+ },
35
+ "accordion": {
36
+ "type": "object",
37
+ "required": ["label", "type"],
38
+ "properties": {
39
+ "type": {
40
+ "type": "string",
41
+ "const": "accordion"
42
+ },
43
+ "label": {
44
+ "type": "string"
45
+ },
46
+ "elements": {
47
+ "type": "array",
48
+ "items": {
49
+ "oneOf": [{ "type": "string" }, { "$ref": "#/$defs/attribute" }]
50
+ }
51
+ }
52
+ }
53
+ },
54
+ "accordion_toggle": {
55
+ "type": "object",
56
+ "required": ["type", "toggle_attribute", "elements"],
57
+ "properties": {
58
+ "type": {
59
+ "type": "string",
60
+ "const": "accordion_toggle"
61
+ },
62
+ "toggle_attribute": {
63
+ "type": "string"
64
+ },
65
+ "elements": {
66
+ "type": "array",
67
+ "items": {
68
+ "oneOf": [{ "type": "string" }, { "$ref": "#/$defs/attribute" }]
69
+ }
70
+ }
71
+ }
72
+ },
73
+ "attribute": {
74
+ "type": "object",
75
+ "required": ["name", "type"],
76
+ "properties": {
77
+ "type": {
78
+ "type": "string",
79
+ "const": "attribute"
80
+ },
81
+ "name": {
82
+ "type": "string"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easol-canvas
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle Byrne
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-06-07 00:00:00.000000000 Z
12
+ date: 2022-07-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -81,6 +81,20 @@ dependencies:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
83
  version: '2.4'
84
+ - !ruby/object:Gem::Dependency
85
+ name: json-schema
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '3'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3'
84
98
  description: |
85
99
  Canvas is a command line tool to help with building themes for Easol.
86
100
  It provides tooling to check theme directories for errors and to make sure
@@ -117,6 +131,7 @@ files:
117
131
  - lib/canvas/validators/footer_schema.rb
118
132
  - lib/canvas/validators/html.rb
119
133
  - lib/canvas/validators/json.rb
134
+ - lib/canvas/validators/layout_schema.rb
120
135
  - lib/canvas/validators/liquid.rb
121
136
  - lib/canvas/validators/menu_schema.rb
122
137
  - lib/canvas/validators/sass.rb
@@ -135,6 +150,7 @@ files:
135
150
  - lib/canvas/validators/schema_attributes/variant.rb
136
151
  - lib/canvas/version.rb
137
152
  - lib/easol/canvas.rb
153
+ - schema_definitions/block_layout.json
138
154
  homepage: https://rubygems.org/gems/easol-canvas
139
155
  licenses:
140
156
  - MIT