@eventcatalog/core 4.10.13 → 4.10.14
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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-SSSN5FXC.js → chunk-HERAHLM2.js} +1 -1
- package/dist/{chunk-IIECZFXN.js → chunk-LITOQH56.js} +1 -1
- package/dist/{chunk-34RMFKFB.js → chunk-LUW6RDA7.js} +1 -1
- package/dist/{chunk-JHUICVBT.js → chunk-ONDFEA5R.js} +1 -1
- package/dist/{chunk-PADMH2RJ.js → chunk-V2SKOQYF.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/api/02-config.md +55 -1
- package/dist/docs/development/authentication/providers/03-setting-up-github.md +29 -0
- package/dist/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md +2 -2
- package/dist/docs/development/bring-your-own-documentation/custom-pages/03-components.md +17 -0
- package/dist/docs/development/components/07-resource-references.md +7 -1
- package/dist/docs/development/components/components/05-architecture-graph.md +65 -0
- package/dist/docs/development/components/components/{05-attachments.md → 06-attachments.md} +1 -1
- package/dist/docs/development/components/components/{06-channel-information.md → 07-channel-information.md} +1 -1
- package/dist/docs/development/components/components/{07-custom-properties.md → 08-custom-properties.md} +1 -1
- package/dist/docs/development/components/components/{08-design.md → 09-design.md} +1 -1
- package/dist/docs/development/components/components/{09-entitymap.md → 10-entitymap.md} +1 -1
- package/dist/docs/development/components/components/{10-flow.md → 11-flow.md} +1 -1
- package/dist/docs/development/components/components/{11-link.md → 12-link.md} +1 -1
- package/dist/docs/development/components/components/{12-mermaid-file-loader.md → 13-mermaid-file-loader.md} +1 -1
- package/dist/docs/development/components/components/{13-message-table.md → 14-message-table.md} +1 -1
- package/dist/docs/development/components/components/{14-nodegraph.md → 15-nodegraph.md} +1 -1
- package/dist/docs/development/components/components/{15-openapi.md → 16-openapi.md} +1 -1
- package/dist/docs/development/components/components/{16-prompt.md → 17-prompt.md} +1 -1
- package/dist/docs/development/components/components/{17-remote-schema.md → 18-remote-schema.md} +1 -1
- package/dist/docs/development/components/components/{18-resource-group-table.md → 19-resource-group-table.md} +1 -1
- package/dist/docs/development/components/components/{19-resource-link.md → 20-resource-link.md} +1 -1
- package/dist/docs/development/components/components/{20-schema.md → 21-schema.md} +1 -1
- package/dist/docs/development/components/components/{21-schema-viewer.md → 22-schema-viewer.md} +1 -1
- package/dist/docs/development/components/components/{22-steps.md → 23-steps.md} +1 -1
- package/dist/docs/development/components/components/{23-tabs.md → 24-tabs.md} +1 -1
- package/dist/docs/development/components/components/{24-tiles.md → 25-tiles.md} +1 -1
- package/dist/docs/development/components/components/{25-visibility.md → 26-visibility.md} +1 -1
- package/dist/docs/development/customization/04-documentation-sidebar.md +3 -3
- package/dist/docs/development/customization/05-resource-sidebar.md +235 -0
- package/dist/docs/development/customization/09-embed-eventcatalog.md +179 -0
- package/dist/docs/development/customization/_category_.json +1 -1
- package/dist/docs/development/deployment/build-and-deploy.md +4 -0
- package/dist/docs/development/deployment/hosting-options.md +1 -1
- package/dist/docs/development/deployment/link-validation.md +105 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/00-overview.md +76 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/01-setup.md +144 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/_category_.json +10 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/01-configure-rules.md +130 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/02-run-in-ci.md +93 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/03-use-custom-frontmatter.md +81 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/04-reference-external-catalogs.md +51 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/05-fix-common-problems.md +120 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/_category_.json +12 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/01-cli.md +109 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/02-configuration.md +120 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/03-rules.md +287 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/04-versions.md +77 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/05-supported-resources.md +75 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/_category_.json +12 -0
- package/dist/docs/development/guides/97-architecture-graph.md +78 -0
- package/dist/docs/development/guides/domains/02-create-domain.md +1 -1
- package/dist/docs/development/guides/domains/03-ownership-and-language/02-adding-ubiquitous-language.md +29 -0
- package/dist/docs/development/guides/domains/09-reference.md +10 -0
- package/dist/docs/development/guides/resources/messages/04-message-channels/02-adding-channels.md +2 -2
- package/dist/docs/development/guides/resources/messages/04-message-channels/04-adding-messages-to-services.md +8 -0
- package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/01-add-openapi-specifications.md +2 -2
- package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/02-add-asyncapi-specifications.md +3 -3
- package/dist/docs/development/guides/systems/05-system-context-maps.md +2 -0
- package/dist/docs/development/license-keys/license-validation.md +7 -3
- package/dist/docs/development/license-keys/overview.md +7 -3
- package/dist/docs/development/upgrading/upgrading.md +3 -3
- package/dist/docs/development/upgrading/v2.md +3 -3
- package/dist/docs/federation/00-overview.md +158 -0
- package/dist/docs/federation/01-first-federation.md +199 -0
- package/dist/docs/federation/_category_.json +12 -0
- package/dist/docs/federation/explanation/_category_.json +12 -0
- package/dist/docs/federation/explanation/how-it-works.md +129 -0
- package/dist/docs/federation/explanation/lockfile-and-cache.md +134 -0
- package/dist/docs/federation/explanation/mvp-status-and-feedback.md +86 -0
- package/dist/docs/federation/explanation/ownership-and-references.md +134 -0
- package/dist/docs/federation/how-to/_category_.json +12 -0
- package/dist/docs/federation/how-to/configure-github-sources.md +156 -0
- package/dist/docs/federation/how-to/configure-validation-rules.md +127 -0
- package/dist/docs/federation/how-to/run-in-ci.md +138 -0
- package/dist/docs/federation/how-to/share-assets-and-components.md +125 -0
- package/dist/docs/federation/how-to/use-local-sources.md +138 -0
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/00-introduction.md +11 -2
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/01-installation.md +11 -3
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/02-plugin-configuration.md +11 -2
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/03-examples.md +11 -1
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/04-configuration.md +11 -1
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/05-setup-team-catalog.md +11 -2
- package/dist/docs/federation/legacy-federation/_category_.json +12 -0
- package/dist/docs/federation/reference/_category_.json +12 -0
- package/dist/docs/federation/reference/cli.md +122 -0
- package/dist/docs/federation/reference/configuration.md +192 -0
- package/dist/docs/federation/reference/diagnostic-rules.md +173 -0
- package/dist/docs/federation/reference/generated-output.md +118 -0
- package/dist/docs/federation/reference/supported-content.md +86 -0
- package/dist/docs/federation/reference/troubleshooting.md +208 -0
- package/dist/docs/plugins/01-intro.md +3 -3
- package/dist/docs/plugins/03-all-plugins.md +2 -2
- package/dist/docs/plugins/asyncapi/00-intro.md +4 -4
- package/dist/docs/plugins/asyncapi/02-plugin-configuration.md +4 -4
- package/dist/docs/plugins/backstage/00-overview.md +53 -0
- package/dist/docs/plugins/backstage/01-get-started.md +95 -0
- package/dist/docs/plugins/backstage/02-embed-entity-tabs.md +109 -0
- package/dist/docs/plugins/backstage/03-add-overview-cards.md +79 -0
- package/dist/docs/plugins/backstage/04-control-theme-and-size.md +54 -0
- package/dist/docs/plugins/backstage/05-migrate-to-annotations.md +49 -0
- package/dist/docs/plugins/backstage/06-configuration.md +85 -0
- package/dist/docs/plugins/backstage/07-components.md +129 -0
- package/dist/docs/plugins/backstage/08-how-embeds-work.md +52 -0
- package/dist/docs/plugins/backstage/_category_.json +4 -3
- package/dist/docs/plugins/eventbridge/00-intro.md +5 -5
- package/dist/docs/plugins/eventbridge/03-api.md +3 -3
- package/dist/docs/plugins/graphql/00-intro.md +2 -2
- package/dist/docs/plugins/graphql/02-plugin-configuration.md +2 -2
- package/dist/docs/plugins/hookdeck/01-intro.md +1 -1
- package/dist/docs/plugins/openapi/00-intro.md +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +21 -19
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +79 -77
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +91 -0
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +115 -33
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +33 -0
- package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +84 -0
- package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.ts +149 -0
- package/eventcatalog/src/utils/protobuf-schema.ts +211 -16
- package/package.json +3 -3
- package/dist/docs/cli/channels.md +0 -180
- package/dist/docs/cli/commands.md +0 -183
- package/dist/docs/cli/custom-docs.md +0 -78
- package/dist/docs/cli/data-products.md +0 -177
- package/dist/docs/cli/data-stores.md +0 -166
- package/dist/docs/cli/diagrams.md +0 -147
- package/dist/docs/cli/domains.md +0 -280
- package/dist/docs/cli/entities.md +0 -138
- package/dist/docs/cli/events.md +0 -186
- package/dist/docs/cli/export.md +0 -27
- package/dist/docs/cli/governance.md +0 -24
- package/dist/docs/cli/messages.md +0 -69
- package/dist/docs/cli/queries.md +0 -183
- package/dist/docs/cli/services.md +0 -266
- package/dist/docs/cli/snapshots.md +0 -44
- package/dist/docs/cli/teams.md +0 -75
- package/dist/docs/cli/users.md +0 -75
- package/dist/docs/cli/utilities.md +0 -43
- package/dist/docs/development/developer-tools/eventcatalog-linter.md +0 -732
- package/dist/docs/plugins/backstage/00-intro.md +0 -67
- package/dist/docs/plugins/backstage/01-installation.md +0 -242
- package/dist/docs/plugins/backstage/02-api.md +0 -51
- package/dist/docs/plugins/backstage/03-examples.md +0 -12
- package/dist/docs/plugins/eventcatalog-federation/_category_.json +0 -11
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import type { ProtobufSchema, ProtobufMessage, ProtobufEnum, ProtobufField } from '@utils/protobuf-schema';
|
|
3
|
+
import {
|
|
4
|
+
formatProtobufOptionValue,
|
|
5
|
+
formatProtovalidateRule,
|
|
6
|
+
getCustomFieldAnnotations,
|
|
7
|
+
getProtovalidateRules,
|
|
8
|
+
isProtovalidateRequired,
|
|
9
|
+
} from './protobuf-validation';
|
|
3
10
|
|
|
4
11
|
interface ProtobufSchemaViewerProps {
|
|
5
12
|
schema: ProtobufSchema;
|
|
@@ -63,9 +70,21 @@ function lookupType<T>(map: Map<string, T>, typeName: string, packageName?: stri
|
|
|
63
70
|
return undefined;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
return
|
|
73
|
+
function compactTypeName(typeName: string): string {
|
|
74
|
+
const normalized = typeName.startsWith('.') ? typeName.slice(1) : typeName;
|
|
75
|
+
return normalized.split('.').pop() ?? normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatProtobufType(field: ProtobufField, compact = false): string {
|
|
79
|
+
const type = field.map
|
|
80
|
+
? compact
|
|
81
|
+
? `map<${field.map.keyType}, ${compactTypeName(field.map.valueType)}>`
|
|
82
|
+
: field.type
|
|
83
|
+
: compact
|
|
84
|
+
? compactTypeName(field.type)
|
|
85
|
+
: field.type;
|
|
86
|
+
|
|
87
|
+
return field.label ? `${field.label} ${type}` : type;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
function countFields(messages: ProtobufMessage[]): number {
|
|
@@ -92,7 +111,27 @@ const ProtobufFieldRow = ({ field, level, expand, showRequired, registry, packag
|
|
|
92
111
|
const nestedEnum = nestedMessage ? undefined : lookupType(registry.enums, targetTypeName, packageName);
|
|
93
112
|
const isCyclic = nestedMessage ? ancestors.includes(nestedMessage.name) : false;
|
|
94
113
|
const hasNested = !!nestedMessage && nestedMessage.fields.length > 0 && !isCyclic;
|
|
95
|
-
const
|
|
114
|
+
const hasRequiredAnnotation = isProtovalidateRequired(field);
|
|
115
|
+
const isRequired = hasRequiredAnnotation || field.label === 'required';
|
|
116
|
+
const validationRules = getProtovalidateRules(field).filter((rule) => rule.path !== 'required');
|
|
117
|
+
const customAnnotations = getCustomFieldAnnotations(field);
|
|
118
|
+
const fullType = formatProtobufType(field);
|
|
119
|
+
const displayType = formatProtobufType(field, true);
|
|
120
|
+
const enumRuleScope = field.map ? 'map.values.enum' : field.label === 'repeated' ? 'repeated.items.enum' : 'enum';
|
|
121
|
+
const enumConstant = validationRules.find((rule) => rule.path === `${enumRuleScope}.const`);
|
|
122
|
+
const enumIncludes = validationRules.find((rule) => rule.path === `${enumRuleScope}.in`);
|
|
123
|
+
const enumExcludes = validationRules.find((rule) => rule.path === `${enumRuleScope}.not_in`);
|
|
124
|
+
const normalizeRuleValues = (value: unknown) => (Array.isArray(value) ? value : value === undefined ? [] : [value]);
|
|
125
|
+
const includedEnumValues = normalizeRuleValues(enumIncludes?.value);
|
|
126
|
+
const excludedEnumValues = normalizeRuleValues(enumExcludes?.value);
|
|
127
|
+
const displayedEnumValues = nestedEnum?.values.filter((value) => {
|
|
128
|
+
const matches = (candidate: unknown) => String(candidate) === String(value.value) || candidate === value.name;
|
|
129
|
+
return (
|
|
130
|
+
(!enumConstant || matches(enumConstant.value)) &&
|
|
131
|
+
(includedEnumValues.length === 0 || includedEnumValues.some(matches)) &&
|
|
132
|
+
!excludedEnumValues.some(matches)
|
|
133
|
+
);
|
|
134
|
+
});
|
|
96
135
|
|
|
97
136
|
useEffect(() => {
|
|
98
137
|
setIsExpanded(expand);
|
|
@@ -116,40 +155,83 @@ const ProtobufFieldRow = ({ field, level, expand, showRequired, registry, packag
|
|
|
116
155
|
)}
|
|
117
156
|
|
|
118
157
|
<div className="flex-grow">
|
|
119
|
-
<div className="
|
|
120
|
-
<div>
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
oneof {field.oneof}
|
|
158
|
+
<div className="-mx-1 rounded px-1 transition-colors hover:bg-[rgb(var(--ec-content-hover))]">
|
|
159
|
+
<div className="flex justify-between items-baseline">
|
|
160
|
+
<div>
|
|
161
|
+
<span className="proto-field-name font-semibold text-[rgb(var(--ec-page-text))] text-sm">{field.name}</span>
|
|
162
|
+
<span
|
|
163
|
+
className="ml-1.5 text-[rgb(var(--ec-accent))] font-mono text-xs"
|
|
164
|
+
title={displayType === fullType ? undefined : fullType}
|
|
165
|
+
>
|
|
166
|
+
{displayType}
|
|
129
167
|
</span>
|
|
168
|
+
{field.number !== undefined && (
|
|
169
|
+
<span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] font-mono text-xs">= {field.number}</span>
|
|
170
|
+
)}
|
|
171
|
+
{field.oneof && (
|
|
172
|
+
<span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] text-xs bg-[rgb(var(--ec-content-hover))] px-1 rounded">
|
|
173
|
+
oneof {field.oneof}
|
|
174
|
+
</span>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
{(hasRequiredAnnotation || (showRequired && isRequired)) && (
|
|
178
|
+
<span className="text-red-600 dark:text-red-400 text-xs ml-3 flex-shrink-0">required</span>
|
|
130
179
|
)}
|
|
131
180
|
</div>
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
|
|
182
|
+
{field.doc && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5">{field.doc}</p>}
|
|
183
|
+
|
|
184
|
+
{validationRules.length > 0 && (
|
|
185
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5 space-y-0">
|
|
186
|
+
{validationRules.map((rule, index) => {
|
|
187
|
+
const display = formatProtovalidateRule(rule);
|
|
188
|
+
return (
|
|
189
|
+
<div key={`${rule.path}-${index}`}>
|
|
190
|
+
{display.label}
|
|
191
|
+
{display.value !== undefined && (
|
|
192
|
+
<>
|
|
193
|
+
:{' '}
|
|
194
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
195
|
+
{display.value}
|
|
196
|
+
</code>
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
134
203
|
)}
|
|
135
|
-
</div>
|
|
136
204
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
{customAnnotations.length > 0 && (
|
|
206
|
+
<div className="mt-0.5 space-y-0 text-xs text-gray-400 dark:text-gray-500">
|
|
207
|
+
{customAnnotations.map((annotation, index) => (
|
|
208
|
+
<div key={`${annotation.name}-${index}`} className="flex max-w-full flex-wrap items-baseline gap-x-1">
|
|
209
|
+
<span>Annotation:</span>
|
|
210
|
+
<code className="break-all font-mono text-gray-500 dark:text-gray-400">{annotation.name}</code>
|
|
211
|
+
<span>=</span>
|
|
212
|
+
<code className="font-medium text-gray-500 dark:text-gray-400">
|
|
213
|
+
{formatProtobufOptionValue(annotation.value)}
|
|
214
|
+
</code>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Show enum values if the field type resolves to an enum */}
|
|
221
|
+
{nestedEnum && displayedEnumValues && displayedEnumValues.length > 0 && (
|
|
222
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5">
|
|
223
|
+
<span className="text-xs inline-block">Allowed values:</span>
|
|
224
|
+
{displayedEnumValues.map((value) => (
|
|
225
|
+
<span key={value.name} className="text-xs">
|
|
226
|
+
{' '}
|
|
227
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
228
|
+
{value.name}
|
|
229
|
+
</code>
|
|
230
|
+
</span>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
153
235
|
|
|
154
236
|
{/* Nested fields for message types */}
|
|
155
237
|
{hasNested && nestedMessage && (
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { parseProtobufSchema } from '@utils/protobuf-schema';
|
|
4
|
+
import AvroSchemaViewer from './AvroSchemaViewer';
|
|
5
|
+
import JSONSchemaViewer from './JSONSchemaViewer';
|
|
6
|
+
import ProtobufSchemaViewer from './ProtobufSchemaViewer';
|
|
7
|
+
|
|
8
|
+
const PROPERTY_HOVER_CLASS = 'hover:bg-[rgb(var(--ec-content-hover))]';
|
|
9
|
+
|
|
10
|
+
describe('schema viewer property hover states', () => {
|
|
11
|
+
it('adds a hover background to JSON Schema properties', () => {
|
|
12
|
+
const html = renderToStaticMarkup(
|
|
13
|
+
<JSONSchemaViewer schema={{ type: 'object', properties: { id: { type: 'string' } } }} search={false} />
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(html).toContain(PROPERTY_HOVER_CLASS);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('adds a hover background to Avro fields', () => {
|
|
20
|
+
const html = renderToStaticMarkup(
|
|
21
|
+
<AvroSchemaViewer schema={{ type: 'record', name: 'Example', fields: [{ name: 'id', type: 'string' }] }} search={false} />
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(html).toContain(PROPERTY_HOVER_CLASS);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('adds a hover background to Protobuf fields', () => {
|
|
28
|
+
const schema = parseProtobufSchema('syntax = "proto3"; message Example { string id = 1; }');
|
|
29
|
+
const html = renderToStaticMarkup(<ProtobufSchemaViewer schema={schema} search={false} />);
|
|
30
|
+
|
|
31
|
+
expect(html).toContain(PROPERTY_HOVER_CLASS);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ProtobufField } from '@utils/protobuf-schema';
|
|
3
|
+
import {
|
|
4
|
+
formatProtobufOptionValue,
|
|
5
|
+
formatProtovalidateRule,
|
|
6
|
+
getCustomFieldAnnotations,
|
|
7
|
+
getProtovalidateRules,
|
|
8
|
+
isProtovalidateRequired,
|
|
9
|
+
} from './protobuf-validation';
|
|
10
|
+
|
|
11
|
+
describe('protobuf validation helpers', () => {
|
|
12
|
+
it('flattens direct and aggregate Protovalidate options', () => {
|
|
13
|
+
const field: ProtobufField = {
|
|
14
|
+
name: 'status',
|
|
15
|
+
type: 'Status',
|
|
16
|
+
options: [
|
|
17
|
+
{ name: '(buf.validate.field).required', value: true },
|
|
18
|
+
{
|
|
19
|
+
name: '(buf.validate.field).enum',
|
|
20
|
+
value: { defined_only: true, not_in: [0] },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
expect(getProtovalidateRules(field)).toEqual([
|
|
26
|
+
{ path: 'required', value: true },
|
|
27
|
+
{ path: 'enum.defined_only', value: true },
|
|
28
|
+
{ path: 'enum.not_in', value: [0] },
|
|
29
|
+
]);
|
|
30
|
+
expect(isProtovalidateRequired(field)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('ignores non-Protovalidate custom options', () => {
|
|
34
|
+
const field: ProtobufField = {
|
|
35
|
+
name: 'description',
|
|
36
|
+
type: 'string',
|
|
37
|
+
options: [{ name: '(datahub.v1.gdpr_rule).string', value: true }],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
expect(getProtovalidateRules(field)).toEqual([]);
|
|
41
|
+
expect(isProtovalidateRequired(field)).toBe(false);
|
|
42
|
+
expect(getCustomFieldAnnotations(field)).toEqual([{ name: '(datahub.v1.gdpr_rule).string', value: true }]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('only treats extension options as custom annotations and formats aggregate values', () => {
|
|
46
|
+
const field: ProtobufField = {
|
|
47
|
+
name: 'description',
|
|
48
|
+
type: 'string',
|
|
49
|
+
options: [
|
|
50
|
+
{ name: 'deprecated', value: true },
|
|
51
|
+
{ name: '(buf.validate.field).string.min_len', value: 1 },
|
|
52
|
+
{ name: '(datahub.v1.classification)', value: { level: 'personal', encrypted: true } },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(getCustomFieldAnnotations(field)).toEqual([
|
|
57
|
+
{
|
|
58
|
+
name: '(datahub.v1.classification)',
|
|
59
|
+
value: { level: 'personal', encrypted: true },
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
expect(formatProtobufOptionValue(getCustomFieldAnnotations(field)[0].value)).toBe('{"level":"personal","encrypted":true}');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('formats common rules for display', () => {
|
|
66
|
+
expect(formatProtovalidateRule({ path: 'string.email', value: true })).toEqual({ label: 'Format', value: 'Email' });
|
|
67
|
+
expect(formatProtovalidateRule({ path: 'uint32.gte', value: 1 })).toEqual({ label: 'Minimum', value: '1' });
|
|
68
|
+
expect(formatProtovalidateRule({ path: 'enum.defined_only', value: true })).toEqual({
|
|
69
|
+
label: 'Defined enum values only',
|
|
70
|
+
});
|
|
71
|
+
expect(formatProtovalidateRule({ path: 'enum.not_in', value: [0] })).toEqual({
|
|
72
|
+
label: 'Excluded values',
|
|
73
|
+
value: '0',
|
|
74
|
+
});
|
|
75
|
+
expect(formatProtovalidateRule({ path: 'repeated.items.string.min_len', value: 3 })).toEqual({
|
|
76
|
+
label: 'Item minimum length',
|
|
77
|
+
value: '3',
|
|
78
|
+
});
|
|
79
|
+
expect(formatProtovalidateRule({ path: 'map.values.string.max_len', value: 100 })).toEqual({
|
|
80
|
+
label: 'Value maximum length',
|
|
81
|
+
value: '100',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ProtobufField, ProtobufOptionValue } from '@utils/protobuf-schema';
|
|
2
|
+
|
|
3
|
+
const PROTOVALIDATE_FIELD_OPTION = '(buf.validate.field)';
|
|
4
|
+
|
|
5
|
+
export interface ProtovalidateRule {
|
|
6
|
+
path: string;
|
|
7
|
+
value: ProtobufOptionValue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProtovalidateRuleDisplay {
|
|
11
|
+
label: string;
|
|
12
|
+
value?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProtobufCustomAnnotation {
|
|
16
|
+
name: string;
|
|
17
|
+
value: ProtobufOptionValue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isOptionObject = (value: ProtobufOptionValue): value is { [key: string]: ProtobufOptionValue } =>
|
|
21
|
+
typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
|
|
23
|
+
const flattenOptionValue = (path: string, value: ProtobufOptionValue): ProtovalidateRule[] => {
|
|
24
|
+
if (!isOptionObject(value)) return [{ path, value }];
|
|
25
|
+
|
|
26
|
+
return Object.entries(value).flatMap(([name, nestedValue]) => flattenOptionValue(path ? `${path}.${name}` : name, nestedValue));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getProtovalidateRules = (field: ProtobufField): ProtovalidateRule[] =>
|
|
30
|
+
(field.options ?? []).flatMap((option) => {
|
|
31
|
+
if (option.name !== PROTOVALIDATE_FIELD_OPTION && !option.name.startsWith(`${PROTOVALIDATE_FIELD_OPTION}.`)) return [];
|
|
32
|
+
|
|
33
|
+
const path = option.name.slice(PROTOVALIDATE_FIELD_OPTION.length).replace(/^\./, '');
|
|
34
|
+
return flattenOptionValue(path, option.value);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const isProtovalidateRequired = (field: ProtobufField) =>
|
|
38
|
+
getProtovalidateRules(field).some((rule) => rule.path === 'required' && rule.value === true);
|
|
39
|
+
|
|
40
|
+
export const getCustomFieldAnnotations = (field: ProtobufField): ProtobufCustomAnnotation[] =>
|
|
41
|
+
(field.options ?? [])
|
|
42
|
+
.filter(
|
|
43
|
+
(option) =>
|
|
44
|
+
option.name.startsWith('(') &&
|
|
45
|
+
option.name !== PROTOVALIDATE_FIELD_OPTION &&
|
|
46
|
+
!option.name.startsWith(`${PROTOVALIDATE_FIELD_OPTION}.`)
|
|
47
|
+
)
|
|
48
|
+
.map(({ name, value }) => ({ name, value }));
|
|
49
|
+
|
|
50
|
+
export const formatProtobufOptionValue = (value: ProtobufOptionValue): string => {
|
|
51
|
+
if (Array.isArray(value)) return value.map(formatProtobufOptionValue).join(', ');
|
|
52
|
+
if (isOptionObject(value)) return JSON.stringify(value);
|
|
53
|
+
return String(value);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const humanize = (value: string) => value.replace(/_/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase());
|
|
57
|
+
|
|
58
|
+
const FORMAT_NAMES: Record<string, string> = {
|
|
59
|
+
address: 'Address',
|
|
60
|
+
email: 'Email',
|
|
61
|
+
hostname: 'Hostname',
|
|
62
|
+
ip: 'IP',
|
|
63
|
+
ipv4: 'IPv4',
|
|
64
|
+
ipv6: 'IPv6',
|
|
65
|
+
protobuf_fqn: 'Protobuf FQN',
|
|
66
|
+
tuuid: 'TUUID',
|
|
67
|
+
uri: 'URI',
|
|
68
|
+
uri_ref: 'URI reference',
|
|
69
|
+
uuid: 'UUID',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getRuleScope = (path: string) => {
|
|
73
|
+
if (path.includes('repeated.items')) return 'Item';
|
|
74
|
+
if (path.includes('map.keys')) return 'Key';
|
|
75
|
+
if (path.includes('map.values')) return 'Value';
|
|
76
|
+
return '';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const withScope = (scope: string, label: string) =>
|
|
80
|
+
scope ? `${scope} ${label.charAt(0).toLowerCase()}${label.slice(1)}` : label;
|
|
81
|
+
|
|
82
|
+
export const formatProtovalidateRule = ({ path, value }: ProtovalidateRule): ProtovalidateRuleDisplay => {
|
|
83
|
+
const name = path.split('.').pop() ?? path;
|
|
84
|
+
const scope = getRuleScope(path);
|
|
85
|
+
const formattedValue = formatProtobufOptionValue(value);
|
|
86
|
+
|
|
87
|
+
switch (name) {
|
|
88
|
+
case 'required':
|
|
89
|
+
return value === true ? { label: 'Required' } : { label: 'Required', value: formattedValue };
|
|
90
|
+
case 'const':
|
|
91
|
+
return { label: withScope(scope, 'Constant'), value: formattedValue };
|
|
92
|
+
case 'len':
|
|
93
|
+
return { label: withScope(scope, 'Length'), value: formattedValue };
|
|
94
|
+
case 'min_len':
|
|
95
|
+
return { label: withScope(scope, 'Minimum length'), value: formattedValue };
|
|
96
|
+
case 'max_len':
|
|
97
|
+
return { label: withScope(scope, 'Maximum length'), value: formattedValue };
|
|
98
|
+
case 'len_bytes':
|
|
99
|
+
return { label: withScope(scope, 'Byte length'), value: formattedValue };
|
|
100
|
+
case 'min_bytes':
|
|
101
|
+
return { label: withScope(scope, 'Minimum byte length'), value: formattedValue };
|
|
102
|
+
case 'max_bytes':
|
|
103
|
+
return { label: withScope(scope, 'Maximum byte length'), value: formattedValue };
|
|
104
|
+
case 'pattern':
|
|
105
|
+
return { label: withScope(scope, 'Match pattern'), value: formattedValue };
|
|
106
|
+
case 'gt':
|
|
107
|
+
return { label: withScope(scope, 'Greater than'), value: formattedValue };
|
|
108
|
+
case 'gte':
|
|
109
|
+
return { label: withScope(scope, 'Minimum'), value: formattedValue };
|
|
110
|
+
case 'lt':
|
|
111
|
+
return { label: withScope(scope, 'Less than'), value: formattedValue };
|
|
112
|
+
case 'lte':
|
|
113
|
+
return { label: withScope(scope, 'Maximum'), value: formattedValue };
|
|
114
|
+
case 'in':
|
|
115
|
+
return { label: withScope(scope, 'Allowed values'), value: formattedValue };
|
|
116
|
+
case 'not_in':
|
|
117
|
+
return { label: withScope(scope, 'Excluded values'), value: formattedValue };
|
|
118
|
+
case 'defined_only':
|
|
119
|
+
return value === true
|
|
120
|
+
? { label: 'Defined enum values only' }
|
|
121
|
+
: { label: 'Defined enum values only', value: formattedValue };
|
|
122
|
+
case 'min_items':
|
|
123
|
+
return { label: 'Minimum items', value: formattedValue };
|
|
124
|
+
case 'max_items':
|
|
125
|
+
return { label: 'Maximum items', value: formattedValue };
|
|
126
|
+
case 'unique':
|
|
127
|
+
return value === true ? { label: 'Unique items' } : { label: 'Unique items', value: formattedValue };
|
|
128
|
+
case 'min_pairs':
|
|
129
|
+
return { label: 'Minimum map entries', value: formattedValue };
|
|
130
|
+
case 'max_pairs':
|
|
131
|
+
return { label: 'Maximum map entries', value: formattedValue };
|
|
132
|
+
case 'email':
|
|
133
|
+
case 'hostname':
|
|
134
|
+
case 'ip':
|
|
135
|
+
case 'ipv4':
|
|
136
|
+
case 'ipv6':
|
|
137
|
+
case 'uri':
|
|
138
|
+
case 'uri_ref':
|
|
139
|
+
case 'address':
|
|
140
|
+
case 'uuid':
|
|
141
|
+
case 'tuuid':
|
|
142
|
+
case 'protobuf_fqn':
|
|
143
|
+
return value === true
|
|
144
|
+
? { label: withScope(scope, 'Format'), value: FORMAT_NAMES[name] }
|
|
145
|
+
: { label: withScope(scope, humanize(name)), value: formattedValue };
|
|
146
|
+
default:
|
|
147
|
+
return { label: withScope(scope, humanize(name)), value: formattedValue };
|
|
148
|
+
}
|
|
149
|
+
};
|