trevl 0.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 +7 -0
- data/LICENSE +21 -0
- data/NOTICE +9 -0
- data/README.md +431 -0
- data/Rakefile +10 -0
- data/assets/trendence-logo.svg +17 -0
- data/assets/trevl-logo.svg +20 -0
- data/docs/screenshots/01_bar_chart.png +0 -0
- data/docs/screenshots/02_column_chart.png +0 -0
- data/docs/screenshots/03_line_chart.png +0 -0
- data/docs/screenshots/04_pie_chart.png +0 -0
- data/docs/screenshots/05_computed_fields.png +0 -0
- data/docs/screenshots/06_postprocess_top_n.png +0 -0
- data/docs/screenshots/10_multi_series.png +0 -0
- data/examples/01_bar_chart.yml +16 -0
- data/examples/02_column_chart.yml +19 -0
- data/examples/03_line_chart.yml +21 -0
- data/examples/04_pie_chart.yml +15 -0
- data/examples/05_computed_fields.yml +26 -0
- data/examples/06_postprocess_top_n.yml +21 -0
- data/examples/07_score_kpi.yml +13 -0
- data/examples/08_table.yml +14 -0
- data/examples/09_template_inheritance.yml +25 -0
- data/examples/10_multi_series.yml +23 -0
- data/lib/trevl/auth/bearer_token.rb +23 -0
- data/lib/trevl/configuration.rb +24 -0
- data/lib/trevl/core_ext/hash.rb +37 -0
- data/lib/trevl/data_source/api.rb +86 -0
- data/lib/trevl/data_source/base.rb +19 -0
- data/lib/trevl/data_source/cube.rb +70 -0
- data/lib/trevl/data_source/static.rb +30 -0
- data/lib/trevl/data_source.rb +38 -0
- data/lib/trevl/errors.rb +9 -0
- data/lib/trevl/highcharts_asset.rb +45 -0
- data/lib/trevl/html_renderer.rb +91 -0
- data/lib/trevl/notebook/display.rb +53 -0
- data/lib/trevl/notebook.rb +41 -0
- data/lib/trevl/processor.rb +106 -0
- data/lib/trevl/renderer/data_fetcher.rb +63 -0
- data/lib/trevl/renderer/ref_parser.rb +61 -0
- data/lib/trevl/renderer/transform.rb +104 -0
- data/lib/trevl/renderer.rb +345 -0
- data/lib/trevl/schema/component.json +337 -0
- data/lib/trevl/schema/document.json +41 -0
- data/lib/trevl/template_store.rb +38 -0
- data/lib/trevl/validator.rb +96 -0
- data/lib/trevl/version.rb +5 -0
- data/lib/trevl.rb +99 -0
- data/llms.txt +171 -0
- data/notebooks/demo.ipynb +312 -0
- data/notebooks/exploration.ipynb +164 -0
- data/scripts/generate_screenshots.rb +107 -0
- metadata +137 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://trevl.trendence.com/schema/component.json",
|
|
4
|
+
"title": "TREVL Component",
|
|
5
|
+
"description": "Schema for a single TREVL visualization component",
|
|
6
|
+
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["id", "type"],
|
|
9
|
+
|
|
10
|
+
"properties": {
|
|
11
|
+
"id": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Unique component identifier"
|
|
14
|
+
},
|
|
15
|
+
"type": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["chart", "score", "table", "text", "filter"],
|
|
18
|
+
"description": "Component type"
|
|
19
|
+
},
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Human-readable component name"
|
|
23
|
+
},
|
|
24
|
+
"api": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Registered data source name"
|
|
27
|
+
},
|
|
28
|
+
"api-parameters": {
|
|
29
|
+
"$ref": "#/$defs/apiParameters"
|
|
30
|
+
},
|
|
31
|
+
"template": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Name of a registered template to inherit from"
|
|
34
|
+
},
|
|
35
|
+
"computed": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": { "$ref": "#/$defs/computedField" },
|
|
38
|
+
"description": "Per-row JavaScript transformations"
|
|
39
|
+
},
|
|
40
|
+
"postprocess": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "JavaScript code to transform the full dataset. Must reassign $result to an Array."
|
|
43
|
+
},
|
|
44
|
+
"queries": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "$ref": "#/$defs/query" },
|
|
47
|
+
"description": "Query definitions (for Cube/X-Middle data sources)"
|
|
48
|
+
},
|
|
49
|
+
"highchartsData": {
|
|
50
|
+
"$ref": "#/$defs/highchartsData"
|
|
51
|
+
},
|
|
52
|
+
"display": {
|
|
53
|
+
"$ref": "#/$defs/display"
|
|
54
|
+
},
|
|
55
|
+
"tableData": {
|
|
56
|
+
"$ref": "#/$defs/tableData"
|
|
57
|
+
},
|
|
58
|
+
"filters": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": { "$ref": "#/$defs/filterDefinition" },
|
|
61
|
+
"description": "Filter definitions (for filter components)"
|
|
62
|
+
},
|
|
63
|
+
"text": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "HTML content (for text components)"
|
|
66
|
+
},
|
|
67
|
+
"subheader": {
|
|
68
|
+
"type": "string"
|
|
69
|
+
},
|
|
70
|
+
"body": {
|
|
71
|
+
"type": "string"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"allOf": [
|
|
76
|
+
{
|
|
77
|
+
"if": { "properties": { "type": { "const": "chart" } } },
|
|
78
|
+
"then": {
|
|
79
|
+
"required": ["api", "highchartsData"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"highchartsData": {
|
|
82
|
+
"required": ["series"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"if": { "properties": { "type": { "const": "score" } } },
|
|
89
|
+
"then": {
|
|
90
|
+
"required": ["api", "display"]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"if": { "properties": { "type": { "const": "table" } } },
|
|
95
|
+
"then": {
|
|
96
|
+
"required": ["api", "tableData"]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"if": { "properties": { "type": { "const": "filter" } } },
|
|
101
|
+
"then": {
|
|
102
|
+
"required": ["api", "filters"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
|
|
107
|
+
"$defs": {
|
|
108
|
+
"computedField": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"required": ["name", "code"],
|
|
111
|
+
"properties": {
|
|
112
|
+
"name": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"description": "Field name added to each row"
|
|
115
|
+
},
|
|
116
|
+
"code": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"description": "JavaScript expression. Return value is assigned to the field."
|
|
119
|
+
},
|
|
120
|
+
"arguments": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"additionalProperties": {
|
|
123
|
+
"type": ["string", "number", "boolean"]
|
|
124
|
+
},
|
|
125
|
+
"description": "Named arguments. Use $endpoint.data.field references as values."
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"additionalProperties": false
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
"query": {
|
|
132
|
+
"type": "object",
|
|
133
|
+
"properties": {
|
|
134
|
+
"context": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"description": "Query context (e.g. target_group, compare_group)"
|
|
137
|
+
},
|
|
138
|
+
"dimensions": {
|
|
139
|
+
"type": "array",
|
|
140
|
+
"items": { "type": "string" }
|
|
141
|
+
},
|
|
142
|
+
"measures": {
|
|
143
|
+
"type": "array",
|
|
144
|
+
"items": { "type": "string" }
|
|
145
|
+
},
|
|
146
|
+
"filters": {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"items": { "$ref": "#/$defs/queryFilter" }
|
|
149
|
+
},
|
|
150
|
+
"computed": {
|
|
151
|
+
"type": "array",
|
|
152
|
+
"items": { "$ref": "#/$defs/computedField" }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
"queryFilter": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"required": ["member", "operator"],
|
|
160
|
+
"properties": {
|
|
161
|
+
"member": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"description": "Cube dimension/measure to filter on"
|
|
164
|
+
},
|
|
165
|
+
"operator": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"enum": ["equals", "notEquals", "gt", "gte", "lt", "lte", "afterDate", "beforeDate", "set", "notSet", "contains", "notContains"],
|
|
168
|
+
"description": "Filter operator"
|
|
169
|
+
},
|
|
170
|
+
"values": {
|
|
171
|
+
"type": "array",
|
|
172
|
+
"items": { "type": ["string", "number"] },
|
|
173
|
+
"description": "Static filter values"
|
|
174
|
+
},
|
|
175
|
+
"parameter": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"description": "Runtime parameter reference ($param_name)"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
"apiParameters": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"description": "Parameters passed to the data source",
|
|
185
|
+
"additionalProperties": true
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
"highchartsData": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"description": "Highcharts configuration object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"chart": {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"properties": {
|
|
195
|
+
"type": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"enum": ["bar", "column", "line", "area", "pie", "scatter", "spline", "areaspline", "heatmap", "bubble", "waterfall", "funnel", "gauge", "solidgauge", "bullet"],
|
|
198
|
+
"description": "Highcharts chart type"
|
|
199
|
+
},
|
|
200
|
+
"height": { "type": ["number", "string"] },
|
|
201
|
+
"backgroundColor": { "type": "string" }
|
|
202
|
+
},
|
|
203
|
+
"additionalProperties": true
|
|
204
|
+
},
|
|
205
|
+
"title": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"properties": {
|
|
208
|
+
"text": { "type": "string" }
|
|
209
|
+
},
|
|
210
|
+
"additionalProperties": true
|
|
211
|
+
},
|
|
212
|
+
"series": {
|
|
213
|
+
"type": "array",
|
|
214
|
+
"items": { "$ref": "#/$defs/seriesDefinition" },
|
|
215
|
+
"description": "Data series definitions"
|
|
216
|
+
},
|
|
217
|
+
"xAxis": { "additionalProperties": true },
|
|
218
|
+
"yAxis": { "additionalProperties": true },
|
|
219
|
+
"colors": {
|
|
220
|
+
"type": "array",
|
|
221
|
+
"items": { "type": "string" }
|
|
222
|
+
},
|
|
223
|
+
"plotOptions": { "type": "object", "additionalProperties": true },
|
|
224
|
+
"legend": { "type": "object", "additionalProperties": true },
|
|
225
|
+
"tooltip": { "type": "object", "additionalProperties": true },
|
|
226
|
+
"credits": { "type": "object", "additionalProperties": true }
|
|
227
|
+
},
|
|
228
|
+
"additionalProperties": true
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
"seriesDefinition": {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"properties": {
|
|
234
|
+
"name": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"description": "Series name. Can be a $reference."
|
|
237
|
+
},
|
|
238
|
+
"type": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
"description": "Override chart type for this series"
|
|
241
|
+
},
|
|
242
|
+
"color": {
|
|
243
|
+
"type": "string"
|
|
244
|
+
},
|
|
245
|
+
"data": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"description": "Data point mapping. Keys (x, y, color, etc.) map to $references.",
|
|
248
|
+
"additionalProperties": {
|
|
249
|
+
"type": ["string", "number", "object"]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"additionalProperties": true
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
"display": {
|
|
257
|
+
"type": "object",
|
|
258
|
+
"description": "Display configuration (primarily for score components)",
|
|
259
|
+
"properties": {
|
|
260
|
+
"value": {
|
|
261
|
+
"type": ["string", "number"],
|
|
262
|
+
"description": "Value to display. Can be a $reference."
|
|
263
|
+
},
|
|
264
|
+
"unit": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"description": "Unit suffix (e.g. '%', ' EUR')"
|
|
267
|
+
},
|
|
268
|
+
"header": {
|
|
269
|
+
"type": "object",
|
|
270
|
+
"properties": {
|
|
271
|
+
"title": { "type": "string" },
|
|
272
|
+
"icon": { "type": "string" }
|
|
273
|
+
},
|
|
274
|
+
"additionalProperties": true
|
|
275
|
+
},
|
|
276
|
+
"format": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"properties": {
|
|
279
|
+
"minimumFractionDigits": { "type": "integer" },
|
|
280
|
+
"maximumFractionDigits": { "type": "integer" }
|
|
281
|
+
},
|
|
282
|
+
"additionalProperties": true
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
"additionalProperties": true
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
"tableData": {
|
|
289
|
+
"type": "object",
|
|
290
|
+
"required": ["columns"],
|
|
291
|
+
"properties": {
|
|
292
|
+
"headers": {
|
|
293
|
+
"type": "array",
|
|
294
|
+
"items": { "type": "string" },
|
|
295
|
+
"description": "Column header labels"
|
|
296
|
+
},
|
|
297
|
+
"columns": {
|
|
298
|
+
"type": "array",
|
|
299
|
+
"items": {
|
|
300
|
+
"type": "object",
|
|
301
|
+
"required": ["identifier", "value"],
|
|
302
|
+
"properties": {
|
|
303
|
+
"identifier": { "type": "string" },
|
|
304
|
+
"value": { "type": "string", "description": "Value or $reference" }
|
|
305
|
+
},
|
|
306
|
+
"additionalProperties": true
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"index": { "type": "string" }
|
|
310
|
+
},
|
|
311
|
+
"additionalProperties": true
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
"filterDefinition": {
|
|
315
|
+
"type": "object",
|
|
316
|
+
"properties": {
|
|
317
|
+
"id": { "type": "string" },
|
|
318
|
+
"type": {
|
|
319
|
+
"type": "string",
|
|
320
|
+
"enum": ["select", "multiselect", "range", "date"],
|
|
321
|
+
"description": "Filter input type"
|
|
322
|
+
},
|
|
323
|
+
"label": { "type": "string" },
|
|
324
|
+
"options": {
|
|
325
|
+
"type": "object",
|
|
326
|
+
"description": "Option mapping with $references for value/label",
|
|
327
|
+
"additionalProperties": { "type": "string" }
|
|
328
|
+
},
|
|
329
|
+
"queries": {
|
|
330
|
+
"type": "array",
|
|
331
|
+
"items": { "$ref": "#/$defs/query" }
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
"additionalProperties": true
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://trevl.trendence.com/schema/document.json",
|
|
4
|
+
"title": "TREVL Document",
|
|
5
|
+
"description": "Schema for a TREVL YAML document — either a hash with 'components' key or a bare array of components",
|
|
6
|
+
|
|
7
|
+
"oneOf": [
|
|
8
|
+
{
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": ["components"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"components": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": { "$ref": "component.json" },
|
|
15
|
+
"minItems": 1,
|
|
16
|
+
"description": "Array of TREVL components"
|
|
17
|
+
},
|
|
18
|
+
"parameters": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["id"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"id": { "type": "string" },
|
|
25
|
+
"type": { "type": "string", "enum": ["single", "multi"] },
|
|
26
|
+
"sources": { "type": "array" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"description": "Runtime parameter definitions"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"additionalProperties": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "$ref": "component.json" },
|
|
37
|
+
"minItems": 1,
|
|
38
|
+
"description": "Bare array of components (shorthand)"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Trevl
|
|
4
|
+
class TemplateStore
|
|
5
|
+
def initialize
|
|
6
|
+
@templates = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def register(name, template_hash)
|
|
10
|
+
@templates[name.to_s.downcase] = template_hash
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def find(name)
|
|
14
|
+
@templates[name.to_s.downcase]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_yaml(yaml_string)
|
|
18
|
+
data = YAML.safe_load(yaml_string, permitted_classes: [Hash], aliases: true)
|
|
19
|
+
return unless data.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
data.each do |name, template_hash|
|
|
22
|
+
register(name, template_hash) if template_hash.is_a?(Hash)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_file(path)
|
|
27
|
+
load_yaml(File.read(path))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def names
|
|
31
|
+
@templates.keys
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear
|
|
35
|
+
@templates.clear
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json_schemer"
|
|
4
|
+
|
|
5
|
+
module Trevl
|
|
6
|
+
class Validator
|
|
7
|
+
SCHEMA_DIR = File.expand_path("schema", __dir__)
|
|
8
|
+
COMPONENT_SCHEMA_PATH = File.join(SCHEMA_DIR, "component.json")
|
|
9
|
+
DOCUMENT_SCHEMA_PATH = File.join(SCHEMA_DIR, "document.json")
|
|
10
|
+
|
|
11
|
+
Result = Struct.new(:valid?, :errors, keyword_init: true) do
|
|
12
|
+
def to_s
|
|
13
|
+
return "Valid" if valid?
|
|
14
|
+
errors.map { |e| "- #{e}" }.join("\n")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.validate(yaml_string)
|
|
19
|
+
new.validate(yaml_string)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.validate_component(component_hash)
|
|
23
|
+
new.validate_component(component_hash)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate(yaml_string)
|
|
27
|
+
parsed = YAML.safe_load(yaml_string, permitted_classes: [Hash, Symbol], aliases: true)
|
|
28
|
+
validate_document(parsed)
|
|
29
|
+
rescue Psych::SyntaxError => e
|
|
30
|
+
Result.new(valid?: false, errors: ["YAML syntax error: #{e.message}"])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_component(component_hash)
|
|
34
|
+
schema_errors = component_schemer.validate(component_hash).map { |e| format_error(e) }
|
|
35
|
+
Result.new(valid?: schema_errors.empty?, errors: schema_errors)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_document(parsed)
|
|
39
|
+
if parsed.is_a?(Array)
|
|
40
|
+
validate_component_array(parsed)
|
|
41
|
+
elsif parsed.is_a?(Hash) && parsed["components"].is_a?(Array)
|
|
42
|
+
validate_component_array(parsed["components"])
|
|
43
|
+
elsif parsed.is_a?(Hash)
|
|
44
|
+
Result.new(valid?: false, errors: ["Document must have a 'components' array"])
|
|
45
|
+
else
|
|
46
|
+
Result.new(valid?: false, errors: ["Document must be a Hash or Array, got #{parsed.class}"])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def validate_component_array(components)
|
|
53
|
+
all_errors = []
|
|
54
|
+
components.each_with_index do |comp, idx|
|
|
55
|
+
schema_errors = component_schemer.validate(comp).map { |e| format_error(e, component_index: idx, component_id: comp["id"]) }
|
|
56
|
+
all_errors.concat(schema_errors)
|
|
57
|
+
end
|
|
58
|
+
Result.new(valid?: all_errors.empty?, errors: all_errors)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def component_schemer
|
|
62
|
+
@component_schemer ||= JSONSchemer.schema(
|
|
63
|
+
JSON.parse(File.read(COMPONENT_SCHEMA_PATH))
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_error(error, component_index: nil, component_id: nil)
|
|
68
|
+
path = error["data_pointer"]
|
|
69
|
+
msg = error["type"]
|
|
70
|
+
details = error["details"] || {}
|
|
71
|
+
|
|
72
|
+
prefix = if component_id
|
|
73
|
+
"[#{component_id}]"
|
|
74
|
+
elsif component_index
|
|
75
|
+
"[component ##{component_index}]"
|
|
76
|
+
else
|
|
77
|
+
""
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
case msg
|
|
81
|
+
when "required"
|
|
82
|
+
missing = details["missing_keys"]&.join(", ") || "unknown"
|
|
83
|
+
"#{prefix} Missing required field(s): #{missing} (at #{path})"
|
|
84
|
+
when "enum"
|
|
85
|
+
"#{prefix} Invalid value at #{path}: must be one of #{error["schema"]["enum"].join(", ")}"
|
|
86
|
+
when "type"
|
|
87
|
+
"#{prefix} Wrong type at #{path}: expected #{error["schema"]["type"]}"
|
|
88
|
+
when "object"
|
|
89
|
+
"#{prefix} Schema error at #{path}: #{error["error"]}"
|
|
90
|
+
else
|
|
91
|
+
detail = error["error"] || error.fetch("message", msg)
|
|
92
|
+
"#{prefix} #{detail} (at #{path})"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/trevl.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "json"
|
|
5
|
+
require "logger"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
require_relative "trevl/version"
|
|
9
|
+
require_relative "trevl/errors"
|
|
10
|
+
require_relative "trevl/configuration"
|
|
11
|
+
require_relative "trevl/highcharts_asset"
|
|
12
|
+
require_relative "trevl/core_ext/hash"
|
|
13
|
+
require_relative "trevl/template_store"
|
|
14
|
+
require_relative "trevl/data_source"
|
|
15
|
+
require_relative "trevl/data_source/base"
|
|
16
|
+
require_relative "trevl/data_source/static"
|
|
17
|
+
require_relative "trevl/data_source/api"
|
|
18
|
+
require_relative "trevl/data_source/cube"
|
|
19
|
+
require_relative "trevl/auth/bearer_token"
|
|
20
|
+
require_relative "trevl/processor"
|
|
21
|
+
require_relative "trevl/renderer"
|
|
22
|
+
require_relative "trevl/validator"
|
|
23
|
+
require_relative "trevl/html_renderer"
|
|
24
|
+
|
|
25
|
+
module Trevl
|
|
26
|
+
GEM_ROOT = File.expand_path("..", __dir__)
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# --- Configuration ---
|
|
30
|
+
|
|
31
|
+
def configure
|
|
32
|
+
yield(configuration)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configuration
|
|
36
|
+
@configuration ||= Configuration.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def logger = configuration.logger
|
|
40
|
+
def template_store = configuration.template_store
|
|
41
|
+
|
|
42
|
+
# --- Core API ---
|
|
43
|
+
|
|
44
|
+
def parse(yaml_string)
|
|
45
|
+
Processor.new(yaml_string)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate(yaml_string)
|
|
49
|
+
Validator.validate(yaml_string)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# data: inline rows for this render call ({endpoint => rows}), wrapped in
|
|
53
|
+
# a Static source internally; answers any api name and components without
|
|
54
|
+
# one. data_sources: per-render sources ({name => instance}). Both take
|
|
55
|
+
# precedence over the global DataSource registry.
|
|
56
|
+
def render(yaml_string, params: {}, data: nil, data_sources: {})
|
|
57
|
+
components = parse(yaml_string).components || []
|
|
58
|
+
components.filter_map { |c| Renderer.new(c, params, data: data, data_sources: data_sources).render }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_to_html(yaml_string, params: {}, data: nil, data_sources: {}, width: 800, height: 400)
|
|
62
|
+
HtmlRenderer.new(width: width, height: height)
|
|
63
|
+
.render(yaml_string, params: params, data: data, data_sources: data_sources)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# --- AI tooling ---
|
|
67
|
+
|
|
68
|
+
def schema_reference
|
|
69
|
+
@schema_reference ||= File.read(File.join(GEM_ROOT, "llms.txt"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def examples
|
|
73
|
+
@examples ||= load_examples
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# --- Lifecycle ---
|
|
77
|
+
|
|
78
|
+
def reset!
|
|
79
|
+
@configuration = Configuration.new
|
|
80
|
+
@schema_reference = nil
|
|
81
|
+
@examples = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def load_examples
|
|
87
|
+
dir = File.join(GEM_ROOT, "examples")
|
|
88
|
+
return [] unless File.directory?(dir)
|
|
89
|
+
|
|
90
|
+
Dir.glob(File.join(dir, "*.yml")).sort.map do |path|
|
|
91
|
+
name = File.basename(path, ".yml")
|
|
92
|
+
content = File.read(path)
|
|
93
|
+
comment_lines = content.lines.take_while { |l| l.start_with?("#") }
|
|
94
|
+
description = comment_lines.map { |l| l.sub(/^#\s?/, "").chomp }.join(" ").strip
|
|
95
|
+
{name: name, description: description, yaml: content.sub(/\A(#[^\n]*\n)+/, "")}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|