easol-canvas 1.4.1 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cc9522b87ab434f7121dfd557aa987b820c9934486da6c96e8c7706ac4f92b4
4
- data.tar.gz: e922ef25a0d8a916a82a7b9c4d8b4faf342088f6da65077518d72b68bf64eeab
3
+ metadata.gz: b20a9e978f946b59d3d3c9e2ee0b6763948c59278cc839bff63931879e3f18bb
4
+ data.tar.gz: 4dc2f01cbc06e1d60c99fd72211442bddfae47011cae0f237bec5eea12f2dddc
5
5
  SHA512:
6
- metadata.gz: c33da855f7ad39a4dad8172fc0942c9c60d4af81acf6c677bddb9c55dcacd9759392fa9c24e65910d76228a760bad31c244b865e8f83e52623ffdf26282f99b6
7
- data.tar.gz: e4d2f3ebae85a1ed14716a1f5514bd3211ae130b973475bbcaf3b52c946cb25db90cdcfbd75143db9984600e3604071d153eb1766e79388e172a2307e09abcfb
6
+ metadata.gz: 4ce1b8bab498d701f7ba028879060c7113ae231e59c9eeb2f1efb5d8ec6930043aeb1e6b061a5b121fa7f89ae470520482f8d2521aac7de606c3f2ef45be093e
7
+ data.tar.gz: cb1d28f8904587f6061ff31914f167cc055bc6bd95eb96f8acd8ff40550e5e2673f4d2386a12cf554f4976d8a5f425453a8e3e9c9ab24260ba05cbdbf93e6c5e
@@ -22,8 +22,9 @@ module Canvas
22
22
  def run
23
23
  custom_types = Canvas::FetchCustomTypes.call
24
24
  block_files.each do |filename|
25
- file = File.read(filename)
26
- front_matter = extract_front_matter(file)
25
+ front_matter = extract_front_matter(filename)
26
+ next unless front_matter
27
+
27
28
  validate_format(filename, front_matter) &&
28
29
  validate_schema(filename, front_matter, custom_types)
29
30
  end
@@ -57,10 +58,18 @@ module Canvas
57
58
  end
58
59
  end
59
60
 
60
- def extract_front_matter(file)
61
+ def extract_front_matter(filename)
62
+ file = File.read(filename)
63
+
61
64
  extractor = Canvas::FrontMatterExtractor.new(file)
62
65
  front_matter = extractor.front_matter
63
66
  front_matter.nil? ? {} : YAML.safe_load(front_matter)
67
+ rescue Psych::SyntaxError
68
+ @offenses << Offense.new(
69
+ message: "Invalid Block Schema: #{filename} - \nFront matter's YAML is not in a valid format"
70
+ )
71
+
72
+ nil
64
73
  end
65
74
  end
66
75
  end
@@ -51,8 +51,7 @@ module Canvas
51
51
  front_matter["attributes"].values.all? { |attr| attr.is_a?(Hash) }
52
52
  end
53
53
 
54
- def validate_schema(front_matter)
55
- schema = extract_schema(front_matter)
54
+ def validate_schema(schema)
56
55
  validator = Validator::FooterSchema.new(
57
56
  schema: schema,
58
57
  custom_types: Canvas::FetchCustomTypes.call
@@ -71,11 +70,5 @@ module Canvas
71
70
  front_matter = extractor.front_matter
72
71
  front_matter.nil? ? {} : YAML.safe_load(front_matter)
73
72
  end
74
-
75
- def extract_schema(front_matter)
76
- front_matter.merge(
77
- "attributes" => Canvas::ExpandAttributes.call(front_matter["attributes"])
78
- )
79
- end
80
73
  end
81
74
  end
@@ -51,8 +51,7 @@ module Canvas
51
51
  front_matter["attributes"].values.all? { |attr| attr.is_a?(Hash) }
52
52
  end
53
53
 
54
- def validate_schema(front_matter)
55
- schema = extract_schema(front_matter)
54
+ def validate_schema(schema)
56
55
  validator = Validator::MenuSchema.new(
57
56
  schema: schema,
58
57
  custom_types: Canvas::FetchCustomTypes.call
@@ -71,11 +70,5 @@ module Canvas
71
70
  front_matter = extractor.front_matter
72
71
  front_matter.nil? ? {} : YAML.safe_load(front_matter)
73
72
  end
74
-
75
- def extract_schema(front_matter)
76
- front_matter.merge(
77
- "attributes" => Canvas::ExpandAttributes.call(front_matter["attributes"])
78
- )
79
- end
80
73
  end
81
74
  end
@@ -9,17 +9,25 @@ module Canvas
9
9
  # This class is used to validate a schema for a block.
10
10
  # Example of a valid block schema:
11
11
  # {
12
- # "attributes" => [
13
- # {
14
- # "name" => "my_title",
12
+ # "attributes" => {
13
+ # "my_title" => {
15
14
  # "type" => "string"
16
15
  # },
17
- # {
18
- # "name" => "my_color",
16
+ # "my_color" => {
19
17
  # "type" => "color",
20
18
  # "label" => "My color",
21
19
  # "hint" => "Select your favourite color"
22
20
  # }
21
+ # },
22
+ # "layout" => [
23
+ # {
24
+ # "type" => "tab",
25
+ # "label" => "Content",
26
+ # "elements" => [
27
+ # "my_title",
28
+ # "my_color"
29
+ # ]
30
+ # }
23
31
  # ]
24
32
  # }
25
33
  class BlockSchema
@@ -51,7 +59,7 @@ module Canvas
51
59
 
52
60
  def ensure_valid_format
53
61
  return true if schema.is_a?(Hash) &&
54
- (schema["attributes"].nil? || attributes_array_of_hashes?(schema))
62
+ (schema["attributes"].nil? || attributes_hash_of_hashes?(schema))
55
63
 
56
64
  @errors << "Schema is not in a valid format"
57
65
  false
@@ -78,7 +86,8 @@ module Canvas
78
86
  def ensure_attributes_are_valid
79
87
  return true unless schema["attributes"]
80
88
 
81
- schema["attributes"].each do |attribute_schema|
89
+ attributes = Canvas::ExpandAttributes.call(schema["attributes"])
90
+ attributes.each do |attribute_schema|
82
91
  attr_validator = Validator::SchemaAttribute.new(
83
92
  attribute: attribute_schema,
84
93
  custom_types: @custom_types
@@ -90,22 +99,17 @@ module Canvas
90
99
  end
91
100
  end
92
101
 
93
- def attributes_array_of_hashes?(schema)
94
- schema["attributes"].is_a?(Array) &&
95
- schema["attributes"].all? { |attr| attr.is_a?(Hash) }
102
+ def attributes_hash_of_hashes?(schema)
103
+ schema["attributes"].is_a?(Hash) &&
104
+ schema["attributes"].values.all? { |attr| attr.is_a?(Hash) }
96
105
  end
97
106
 
107
+ # To support older schemas that do not nest the attributes
108
+ # under the `attributes` key.
98
109
  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
110
+ return schema if schema.key?("attributes")
111
+
112
+ { "attributes" => schema }
109
113
  end
110
114
  end
111
115
  end
@@ -11,19 +11,27 @@ module Canvas
11
11
  #
12
12
  # Example:
13
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"
14
+ # "max_item_levels" => 2,
15
+ # "supports_open_new_tab" => "true",
16
+ # "attributes" => {
17
+ # "text_color" => {
18
+ # "type" => "color"
19
+ # },
20
+ # "background_color" => {
21
+ # "type" => "color"
23
22
  # }
24
- # }
23
+ # },
24
+ # "layout" => [
25
+ # {
26
+ # "type" => "tab",
27
+ # "label" => "Content",
28
+ # "elements" => [
29
+ # "text_color",
30
+ # "background_color"
31
+ # ]
32
+ # }
33
+ # ]
25
34
  # }
26
- #
27
35
  class FooterSchema < MenuSchema; end
28
36
  end
29
37
  end
@@ -9,23 +9,38 @@ module Canvas
9
9
  # This class is used to validate a layout definition, part of block schema.
10
10
  # Example of a valid layout definition:
11
11
  # {
12
+ # "attributes" => {
13
+ # "title" => {
14
+ # "type" => "string"
15
+ # }
16
+ # ...
17
+ # },
12
18
  # "layout" => [
13
- # {
14
- # "label" => "Design",
15
- # "type" => "tab",
16
- # "elements" => [
17
- # "heading",
18
- # {
19
- # "type" => "accordion",
20
- # "label" => "Logo",
19
+ # {
20
+ # "label" => "Design",
21
+ # "type" => "tab",
21
22
  # "elements" => [
22
- # "description",
23
- # { "type" => "attribute", "name" => "logo_alt" },
24
- # "title"
25
- # ]
26
- # }
27
- # ]
28
- # }]
23
+ # "heading",
24
+ # {
25
+ # "type" => "accordion",
26
+ # "label" => "Logo",
27
+ # "elements" => [
28
+ # "description",
29
+ # { "type" => "attribute", "name" => "logo_alt" },
30
+ # "title"
31
+ # ]
32
+ # },
33
+ # {
34
+ # "type" => "accordion_toggle",
35
+ # "toggle_attribute" => "cta_enabled",
36
+ # "elements" => [
37
+ # "cta_text",
38
+ # "cta_target"
39
+ # ]
40
+ # }
41
+ # ]
42
+ # }
43
+ # ]
29
44
  # }
30
45
  class LayoutSchema
31
46
  attr_reader :errors
@@ -41,6 +56,8 @@ module Canvas
41
56
  if ensure_valid_format
42
57
  ensure_no_unrecognized_keys
43
58
  ensure_no_duplicate_keys
59
+ ensure_accordion_toggles_are_valid
60
+ ensure_unique_tabs
44
61
  end
45
62
 
46
63
  @errors.empty?
@@ -48,8 +65,25 @@ module Canvas
48
65
 
49
66
  private
50
67
 
68
+ attr_reader :schema
69
+
70
+ def ensure_unique_tabs
71
+ tabs = fetch_elements_of_type("tab")
72
+ duplicates =
73
+ tabs
74
+ .map { |item| [item[0]["label"], item[1]] }
75
+ .group_by { |(key, _)| key }
76
+ .filter { |key, usage| usage.size > 1 }
77
+
78
+ unless duplicates.empty?
79
+ duplicates.each do |tab, usage|
80
+ @errors << "Duplicated tab label `#{tab}` found. Location: #{usage.map { |(_, location)| location }.join(", ")}"
81
+ end
82
+ end
83
+ end
84
+
51
85
  def ensure_no_duplicate_keys
52
- attributes = gather_attributes_from_layout_schema
86
+ attributes = fetch_all_attribute_names
53
87
  duplicates =
54
88
  attributes
55
89
  .group_by { |(key)| key }
@@ -63,37 +97,54 @@ module Canvas
63
97
  end
64
98
 
65
99
  def ensure_no_unrecognized_keys
66
- attributes = gather_attributes_from_layout_schema
67
- defined_attributes = @schema["attributes"]&.map { |definition| normalize_attribute(definition["name"]) } || []
100
+ attributes = fetch_all_attribute_names
101
+ defined_attributes = expanded_attributes.map { |definition| normalize_attribute(definition["name"]) } || []
68
102
 
69
103
  attributes.each do |attribute, location|
70
104
  @errors << "Unrecognized attribute `#{attribute}`. Location: #{location}" unless defined_attributes.include?(attribute)
71
105
  end
72
106
  end
73
107
 
74
- def gather_attributes_from_layout_schema
75
- attribute_keys = []
108
+ # @return [Array<Array(String, String)>] an array of all the attribute names that
109
+ # are mentioned in the layout schema, along with its path. The names are
110
+ # normalized, i.e. downcased.
111
+ def fetch_all_attribute_names
112
+ attributes = fetch_elements_of_type("attribute")
113
+ attributes.map do |(node, path)|
114
+ [
115
+ normalize_attribute(node.is_a?(Hash) ? node["name"] : node),
116
+ path
117
+ ]
118
+ end
119
+ end
120
+
121
+ # @param type [String] the element type to fetch
122
+ # @return [Array<Array(<Hash, String>, String)] an array of elements that match
123
+ # the given type. Each element is an array containing the node and its path.
124
+ def fetch_elements_of_type(type)
125
+ elements = []
126
+
127
+ fetch_element = ->(node, path) {
128
+ if type == "attribute" && node.is_a?(String)
129
+ elements << [node, path]
130
+ elsif node["type"] == type
131
+ elements << [node, path]
132
+ end
76
133
 
77
- fetch_attribute_type = ->(node, path) {
78
134
  if node.is_a?(Hash) && node.key?("elements")
79
135
  node["elements"].each_with_index do |element, i|
80
136
  current_path = "#{path}/elements/#{i}"
81
- fetch_attribute_type.call(element, current_path)
137
+ fetch_element.call(element, current_path)
82
138
  end
83
- else
84
- attribute_keys << [
85
- normalize_attribute(node.is_a?(Hash) ? node["name"] : node),
86
- path
87
- ]
88
139
  end
89
140
  }
90
141
 
91
142
  layout_schema.each_with_index do |tab, i|
92
143
  current_path = "layout/#{i}"
93
- fetch_attribute_type.call(tab, current_path)
144
+ fetch_element.call(tab, current_path)
94
145
  end
95
146
 
96
- attribute_keys
147
+ elements
97
148
  end
98
149
 
99
150
  def ensure_valid_format
@@ -105,19 +156,40 @@ module Canvas
105
156
  false
106
157
  end
107
158
 
159
+ def ensure_accordion_toggles_are_valid
160
+ accordion_toggles = fetch_elements_of_type("accordion_toggle")
161
+ accordion_toggles.each do |accordion_toggle, location|
162
+ toggle_attribute = expanded_attributes.detect { |attr|
163
+ attr["name"] == accordion_toggle["toggle_attribute"]
164
+ }
165
+
166
+ if toggle_attribute.nil?
167
+ @errors << "The toggle_attribute in accordion_toggle is unrecognized. Location: #{location}"
168
+ elsif toggle_attribute["type"]&.downcase != "boolean"
169
+ @errors << "The toggle_attribute in accordion_toggle must be a boolean. Location: #{location}"
170
+ end
171
+ end
172
+ end
173
+
108
174
  def layout_schema
109
- @schema["layout"] || []
175
+ schema["layout"] || []
110
176
  end
111
177
 
112
178
  def schema_definition
113
179
  File.read(
114
- File.join(File.dirname(__FILE__), "../../../", "schema_definitions", "block_layout.json")
180
+ File.join(File.dirname(__FILE__), "../../../", "schema_definitions", "layout.json")
115
181
  )
116
182
  end
117
183
 
118
184
  def normalize_attribute(name)
119
185
  name.strip.downcase
120
186
  end
187
+
188
+ def expanded_attributes
189
+ return [] if schema["attributes"].nil?
190
+
191
+ @_expanded_attributes ||= Canvas::ExpandAttributes.call(schema["attributes"])
192
+ end
121
193
  end
122
194
  end
123
195
  end
@@ -7,21 +7,33 @@ module Canvas
7
7
  #
8
8
  # Example:
9
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"
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
+ # "background_color" => {
21
+ # "type" => "color"
19
22
  # }
20
- # }
23
+ # },
24
+ # "layout" => [
25
+ # {
26
+ # "type" => "tab",
27
+ # "label" => "Content",
28
+ # "elements" => [
29
+ # "fixed",
30
+ # "background_color"
31
+ # ]
32
+ # }
33
+ # ]
21
34
  # }
22
- #
23
35
  class MenuSchema
24
- PERMITTED_KEYS = %w[max_item_levels supports_open_new_tab attributes].freeze
36
+ PERMITTED_KEYS = %w[max_item_levels supports_open_new_tab attributes layout].freeze
25
37
  ADDITIONAL_RESERVED_NAMES = %w[items type].freeze
26
38
 
27
39
  attr_reader :schema, :errors
@@ -38,6 +50,7 @@ module Canvas
38
50
  if ensure_valid_format
39
51
  ensure_no_unrecognized_keys
40
52
  ensure_max_item_levels_is_valid
53
+ ensure_layout_is_valid
41
54
  ensure_attributes_are_valid
42
55
  end
43
56
 
@@ -48,7 +61,7 @@ module Canvas
48
61
 
49
62
  def ensure_valid_format
50
63
  return true if schema.is_a?(Hash) &&
51
- (schema["attributes"].nil? || attributes_array_of_hashes?(schema))
64
+ (schema["attributes"].nil? || attributes_hash_of_hashes?(schema))
52
65
 
53
66
  @errors << "Schema is not in a valid format"
54
67
  false
@@ -70,10 +83,21 @@ module Canvas
70
83
  false
71
84
  end
72
85
 
86
+ def ensure_layout_is_valid
87
+ return true unless schema["layout"]
88
+
89
+ layout_validator = LayoutSchema.new(schema: @schema)
90
+ return true if layout_validator.validate
91
+
92
+ @errors += layout_validator.errors
93
+ false
94
+ end
95
+
73
96
  def ensure_attributes_are_valid
74
97
  return true unless schema["attributes"]
75
98
 
76
- schema["attributes"].each do |attribute_schema|
99
+ attributes = Canvas::ExpandAttributes.call(schema["attributes"])
100
+ attributes.each do |attribute_schema|
77
101
  attr_validator = Validator::SchemaAttribute.new(
78
102
  attribute: attribute_schema,
79
103
  custom_types: @custom_types,
@@ -86,9 +110,9 @@ module Canvas
86
110
  end
87
111
  end
88
112
 
89
- def attributes_array_of_hashes?(schema)
90
- schema["attributes"].is_a?(Array) &&
91
- schema["attributes"].all? { |attr| attr.is_a?(Hash) }
113
+ def attributes_hash_of_hashes?(schema)
114
+ schema["attributes"].is_a?(Hash) &&
115
+ schema["attributes"].values.all? { |attr| attr.is_a?(Hash) }
92
116
  end
93
117
  end
94
118
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canvas
4
- VERSION = "1.4.1"
4
+ VERSION = "2.1.0"
5
5
  end
@@ -24,20 +24,21 @@
24
24
  "items": {
25
25
  "oneOf": [
26
26
  { "type": "string" },
27
- { "$ref": "#/$defs/component" },
27
+ { "$ref": "#/$defs/accordion" },
28
+ { "$ref": "#/$defs/accordion_toggle" },
28
29
  { "$ref": "#/$defs/attribute" }
29
30
  ]
30
31
  }
31
32
  }
32
33
  }
33
34
  },
34
- "component": {
35
+ "accordion": {
35
36
  "type": "object",
36
37
  "required": ["label", "type"],
37
38
  "properties": {
38
39
  "type": {
39
40
  "type": "string",
40
- "enum": ["accordion"]
41
+ "const": "accordion"
41
42
  },
42
43
  "label": {
43
44
  "type": "string"
@@ -50,6 +51,25 @@
50
51
  }
51
52
  }
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
+ },
53
73
  "attribute": {
54
74
  "type": "object",
55
75
  "required": ["name", "type"],
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.4.1
4
+ version: 2.1.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-07-15 00:00:00.000000000 Z
12
+ date: 2022-08-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -150,7 +150,7 @@ files:
150
150
  - lib/canvas/validators/schema_attributes/variant.rb
151
151
  - lib/canvas/version.rb
152
152
  - lib/easol/canvas.rb
153
- - schema_definitions/block_layout.json
153
+ - schema_definitions/layout.json
154
154
  homepage: https://rubygems.org/gems/easol-canvas
155
155
  licenses:
156
156
  - MIT
@@ -170,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
170
  - !ruby/object:Gem::Version
171
171
  version: '0'
172
172
  requirements: []
173
- rubygems_version: 3.2.33
173
+ rubygems_version: 3.3.11
174
174
  signing_key:
175
175
  specification_version: 4
176
176
  summary: CLI to help with building themes for Easol