easol-canvas 4.20.0 → 4.21.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: fa515bf37be7d80d67de3a67e92d6097e17d7298fc436a916acf0ca04687d78c
4
- data.tar.gz: bbc64745d8df45188a081795995e0c92fc86b99f53beef20a69b72741657dd91
3
+ metadata.gz: c37703170f2a9f0870de51fa994e8929d1ed3a53ebe7f510c27ab943bddf9b6c
4
+ data.tar.gz: 1ccfe78c1b4aaaf9cb4bcbbd1d0afc062b63e94d93b41e162174aedff5fe9832
5
5
  SHA512:
6
- metadata.gz: c91c9b333de32d036428dd9bd0c88840e57ea37c3509bb266440ffa6df89fd50d7161edf155ea40f4e22898a0ff1e0a55e0c9900f03fc67a33e0eebf3dbaed19
7
- data.tar.gz: 2a8d559a57048b770104a5a3faf5536539c081571f37f44717b2850bd4f9ab7e2975f897f87da1b34014e42d5de7ed11712a9ba90b062a8cb6b3cd6676dafd98
6
+ metadata.gz: 9f2ca1fc7300356060311c3e1622b05df28e9d9e48985a8d50a40319c83d15a882a4ac7174c9f39bdc501f47e2988e8a9e9980187dbf513d03b6c8193f927f62
7
+ data.tar.gz: 883efd325fda12742d09a6ae3b30c102c51406fec1caf10f112d1ee8ce6ea9f145de80e9c1a4d6dd2f8562c7b5909aee99895a72207108c610d735a89a318db6
@@ -25,6 +25,7 @@ module Canvas
25
25
  #
26
26
  class CustomType
27
27
  REQUIRED_KEYS = %w[key name attributes].freeze
28
+ OPTIONAL_KEYS = %w[layout].freeze
28
29
 
29
30
  attr_reader :schema, :errors, :custom_types
30
31
 
@@ -43,7 +44,8 @@ module Canvas
43
44
  ensure_key_value_is_not_reserved &&
44
45
  ensure_no_duplicate_attributes &&
45
46
  ensure_attributes_are_valid &&
46
- ensure_first_attribute_not_array
47
+ ensure_first_attribute_not_array &&
48
+ ensure_layout_is_valid
47
49
 
48
50
  errors.empty?
49
51
  end
@@ -66,7 +68,7 @@ module Canvas
66
68
  end
67
69
 
68
70
  def ensure_no_unrecognized_keys
69
- unrecognized_keys = schema.keys - REQUIRED_KEYS
71
+ unrecognized_keys = schema.keys - REQUIRED_KEYS - OPTIONAL_KEYS
70
72
  return true if unrecognized_keys.empty?
71
73
 
72
74
  @errors << "Unrecognized keys: #{unrecognized_keys.join(', ')}"
@@ -145,6 +147,16 @@ module Canvas
145
147
  @errors << "The first attribute cannot be an array"
146
148
  false
147
149
  end
150
+
151
+ def ensure_layout_is_valid
152
+ return true unless schema["layout"]
153
+
154
+ layout_validator = CustomTypeLayoutSchema.new(schema:)
155
+ return true if layout_validator.validate
156
+
157
+ @errors += layout_validator.errors
158
+ false
159
+ end
148
160
  end
149
161
  end
150
162
  end
@@ -0,0 +1,172 @@
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 for a custom type.
10
+ # Custom type layouts use a flat array of elements (unlike block layouts which use tabs).
11
+ #
12
+ # Example of a valid custom type layout definition:
13
+ # {
14
+ # "attributes" => [
15
+ # { "name" => "question", "type" => "string" },
16
+ # { "name" => "answer", "type" => "text" },
17
+ # { "name" => "show_icon", "type" => "boolean" }
18
+ # ],
19
+ # "layout" => [
20
+ # "question",
21
+ # {
22
+ # "type" => "accordion",
23
+ # "label" => "Advanced",
24
+ # "elements" => [
25
+ # "answer",
26
+ # { "type" => "attribute", "name" => "show_icon" }
27
+ # ]
28
+ # }
29
+ # ]
30
+ # }
31
+ class CustomTypeLayoutSchema
32
+ attr_reader :errors
33
+
34
+ def initialize(schema:)
35
+ @schema = schema
36
+ @errors = []
37
+ end
38
+
39
+ def validate
40
+ @errors = []
41
+
42
+ if ensure_valid_format
43
+ ensure_no_unrecognized_keys
44
+ ensure_no_duplicate_keys
45
+ ensure_accordion_toggles_are_valid
46
+ end
47
+
48
+ @errors.empty?
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :schema
54
+
55
+ def ensure_no_duplicate_keys
56
+ attributes = fetch_all_attribute_names
57
+ duplicates =
58
+ attributes
59
+ .group_by { |(key)| key }
60
+ .filter { |_key, usage| usage.size > 1 }
61
+
62
+ return if duplicates.empty?
63
+
64
+ duplicates.each do |attribute, usage|
65
+ @errors << "Duplicated attribute key `#{attribute}` found. "\
66
+ "Location: #{usage.map { |(_, location)| location }.join(', ')}"
67
+ end
68
+ end
69
+
70
+ def ensure_no_unrecognized_keys
71
+ attributes = fetch_all_attribute_names
72
+ defined_attributes = schema_attributes.map { |attr| normalize_attribute(attr["name"]) }
73
+
74
+ attributes.each do |attribute, location|
75
+ unless defined_attributes.include?(attribute)
76
+ @errors << "Unrecognized attribute `#{attribute}`. Location: #{location}"
77
+ end
78
+ end
79
+ end
80
+
81
+ # @return [Array<Array(String, String)>] an array of all the attribute names that
82
+ # are mentioned in the layout schema, along with its path. The names are
83
+ # normalized, i.e. downcased.
84
+ def fetch_all_attribute_names
85
+ attributes = fetch_elements_of_type("attribute")
86
+ attributes.map do |(node, path)|
87
+ [
88
+ normalize_attribute(node.is_a?(Hash) ? node["name"] : node),
89
+ path
90
+ ]
91
+ end
92
+ end
93
+
94
+ # @param type [String] the element type to fetch
95
+ # @return [Array<Array(<Hash, String>, String)] an array of elements that match
96
+ # the given type. Each element is an array containing the node and its path.
97
+ def fetch_elements_of_type(type)
98
+ elements = []
99
+
100
+ fetch_element = lambda { |node, path|
101
+ if type == "attribute" && node.is_a?(String)
102
+ elements << [node, path]
103
+ elsif node.is_a?(Hash) && node["type"] == type
104
+ elements << [node, path]
105
+ end
106
+
107
+ if node.is_a?(Hash) && node.key?("elements")
108
+ node["elements"].each_with_index do |element, i|
109
+ current_path = "#{path}/elements/#{i}"
110
+ fetch_element.call(element, current_path)
111
+ end
112
+ end
113
+ }
114
+
115
+ # Flat layout: iterate directly over layout elements
116
+ layout_schema.each_with_index do |element, i|
117
+ current_path = "layout/#{i}"
118
+ fetch_element.call(element, current_path)
119
+ end
120
+
121
+ elements
122
+ end
123
+
124
+ def ensure_valid_format
125
+ result = JSON::Validator.fully_validate(
126
+ schema_definition,
127
+ { "layout" => layout_schema },
128
+ strict: true,
129
+ clear_cache: true
130
+ )
131
+
132
+ return true if result.empty?
133
+
134
+ @errors += result
135
+ false
136
+ end
137
+
138
+ def ensure_accordion_toggles_are_valid
139
+ accordion_toggles = fetch_elements_of_type("accordion_toggle")
140
+ accordion_toggles.each do |accordion_toggle, location|
141
+ toggle_attribute = schema_attributes.detect { |attr|
142
+ attr["name"] == accordion_toggle["toggle_attribute"]
143
+ }
144
+
145
+ if toggle_attribute.nil?
146
+ @errors << "The toggle_attribute in accordion_toggle is unrecognized. Location: #{location}"
147
+ elsif toggle_attribute["type"]&.downcase != "boolean"
148
+ @errors << "The toggle_attribute in accordion_toggle must be a boolean. Location: #{location}"
149
+ end
150
+ end
151
+ end
152
+
153
+ def layout_schema
154
+ schema["layout"] || []
155
+ end
156
+
157
+ def schema_definition
158
+ File.read(
159
+ File.join(File.dirname(__FILE__), "../../../", "schema_definitions", "custom_type_layout.json")
160
+ )
161
+ end
162
+
163
+ def normalize_attribute(name)
164
+ name.to_s.strip.downcase
165
+ end
166
+
167
+ def schema_attributes
168
+ schema["attributes"] || []
169
+ end
170
+ end
171
+ end
172
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canvas
4
- VERSION = "4.20.0"
4
+ VERSION = "4.21.0"
5
5
  end
@@ -0,0 +1,79 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "layout": {
5
+ "type": "array",
6
+ "items": {
7
+ "oneOf": [
8
+ { "type": "string" },
9
+ { "$ref": "#/$defs/accordion" },
10
+ { "$ref": "#/$defs/accordion_toggle" },
11
+ { "$ref": "#/$defs/attribute" }
12
+ ]
13
+ }
14
+ }
15
+ },
16
+ "$defs": {
17
+ "accordion": {
18
+ "type": "object",
19
+ "required": ["label", "type"],
20
+ "properties": {
21
+ "type": {
22
+ "type": "string",
23
+ "const": "accordion"
24
+ },
25
+ "label": {
26
+ "type": "string"
27
+ },
28
+ "elements": {
29
+ "type": "array",
30
+ "items": {
31
+ "oneOf": [
32
+ { "type": "string" },
33
+ { "$ref": "#/$defs/accordion_toggle" },
34
+ { "$ref": "#/$defs/attribute" }
35
+ ]
36
+ }
37
+ }
38
+ },
39
+ "additionalProperties": false
40
+ },
41
+ "accordion_toggle": {
42
+ "type": "object",
43
+ "required": ["type", "toggle_attribute", "elements"],
44
+ "properties": {
45
+ "type": {
46
+ "type": "string",
47
+ "const": "accordion_toggle"
48
+ },
49
+ "toggle_attribute": {
50
+ "type": "string"
51
+ },
52
+ "elements": {
53
+ "type": "array",
54
+ "items": {
55
+ "oneOf": [
56
+ { "type": "string" },
57
+ { "$ref": "#/$defs/attribute" }
58
+ ]
59
+ }
60
+ }
61
+ },
62
+ "additionalProperties": false
63
+ },
64
+ "attribute": {
65
+ "type": "object",
66
+ "required": ["name", "type"],
67
+ "properties": {
68
+ "type": {
69
+ "type": "string",
70
+ "const": "attribute"
71
+ },
72
+ "name": {
73
+ "type": "string"
74
+ }
75
+ },
76
+ "additionalProperties": false
77
+ }
78
+ }
79
+ }
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: 4.20.0
4
+ version: 4.21.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: 2026-01-15 00:00:00.000000000 Z
12
+ date: 2026-01-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -129,6 +129,7 @@ files:
129
129
  - lib/canvas/services/front_matter_extractor.rb
130
130
  - lib/canvas/validators/block_schema.rb
131
131
  - lib/canvas/validators/custom_type.rb
132
+ - lib/canvas/validators/custom_type_layout_schema.rb
132
133
  - lib/canvas/validators/footer_schema.rb
133
134
  - lib/canvas/validators/html.rb
134
135
  - lib/canvas/validators/json.rb
@@ -157,6 +158,7 @@ files:
157
158
  - lib/canvas/validators/schema_attributes/variant.rb
158
159
  - lib/canvas/version.rb
159
160
  - lib/easol/canvas.rb
161
+ - schema_definitions/custom_type_layout.json
160
162
  - schema_definitions/layout.json
161
163
  homepage: https://rubygems.org/gems/easol-canvas
162
164
  licenses: