diff --git a/site/decodal-site/src/pages/playground.astro b/site/decodal-site/src/pages/playground.astro index c5b8b57..47f733c 100644 --- a/site/decodal-site/src/pages/playground.astro +++ b/site/decodal-site/src/pages/playground.astro @@ -9,6 +9,11 @@ import ManualLayout from '../layouts/ManualLayout.astro';

Virtual files are evaluated in the browser through WebAssembly. Use import paths such as ./schemas/service.dcdl.

+ +

Loading WASM...

diff --git a/site/decodal-site/src/scripts/playground-examples.js b/site/decodal-site/src/scripts/playground-examples.js new file mode 100644 index 0000000..9e3859d --- /dev/null +++ b/site/decodal-site/src/scripts/playground-examples.js @@ -0,0 +1,174 @@ +export const playgroundExamples = [ + { + id: 'service-deployment', + title: 'Service deployment', + activePath: 'main.dcdl', + files: { + 'main.dcdl': `let + schema = import "./schemas/service.dcdl"; + prod = import "./env/prod.dcdl"; + shared = import "./shared/tags.dcdl"; +in + schema.Service & prod.ServicePatch & { + name = "api"; + image = "registry.example.com/api:2026-06-24"; + port = 9000 + 443; + public = prod.is_public && 9000 + 443 > 9000; + tags = shared.common ++ ["api", "edge"]; + } +`, + 'schemas/service.dcdl': `Service = { + name = String; + image = String; + port = Int & > 443 default 8443; + replicas = Int & > 0 default 2; + public = Bool default false; + feature.metrics = Bool default true; + feature.tracing = Bool default false; +}; +`, + 'env/prod.dcdl': `is_public = true; + +ServicePatch = { + replicas = 3; + feature.tracing = true; +}; +`, + 'shared/tags.dcdl': `common = ["decodal", "prod"]; +`, + }, + }, + { + id: 'access-policy', + title: 'Access policy', + activePath: 'main.dcdl', + files: { + 'main.dcdl': `let + policy = import "./policy/base.dcdl"; + teams = import "./policy/teams.dcdl"; + env = "prod"; +in +{ + service = "payments"; + readers = policy.defaultReaders ++ teams.observability; + writers = policy.defaultWriters ++ teams.payments; + admins = match env { + "prod": policy.breakglassAdmins; + _: teams.platform; + }; + audit.required = env == "prod"; + audit.retention_days = match env { + "prod": 365; + _: 30; + }; +} +`, + 'policy/base.dcdl': `defaultReaders = ["group:engineering", "group:support"]; +defaultWriters = ["group:platform"]; +breakglassAdmins = ["user:oncall-primary", "user:oncall-secondary"]; +`, + 'policy/teams.dcdl': `platform = ["group:platform-admins"]; +payments = ["group:payments-api", "group:payments-sre"]; +observability = ["group:observability"]; +`, + }, + }, + { + id: 'edge-worker', + title: 'Edge worker config', + activePath: 'main.dcdl', + files: { + 'main.dcdl': `let + schema = import "./schemas/worker.dcdl"; + routes = import "./routes.dcdl"; + production = true; +in + schema.Worker & { + name = "decodal-docs"; + compatibility_date = "2026-06-15"; + workers_dev = !production; + route.host = routes.primary_host; + route.paths = routes.docs_paths ++ routes.playground_paths; + cache.ttl_seconds = 60 * 60; + cache.bypass = !production; + } +`, + 'schemas/worker.dcdl': `Worker = { + name = String; + compatibility_date = String; + workers_dev = Bool default false; + route.host = String; + cache.ttl_seconds = Int & >= 0 default 300; + cache.bypass = Bool default false; +}; +`, + 'routes.dcdl': `primary_host = "decodal.example.com"; +docs_paths = ["/docs/*", "/assets/*"]; +playground_paths = ["/playground/*"]; +`, + }, + }, + { + id: 'data-pipeline', + title: 'Data pipeline', + activePath: 'main.dcdl', + files: { + 'main.dcdl': `let + schema = import "./schemas/pipeline.dcdl"; + presets = import "./presets/batch.dcdl"; + env = "staging"; +in + schema.Pipeline & presets.Batch & { + name = "events-rollup"; + source.topic = "events.raw"; + sink.table = "analytics.events_daily"; + workers = match env { + "prod": 8; + "staging": 3; + _: 1; + }; + alerts.enabled = env != "dev"; + transforms = presets.standardTransforms ++ ["dedupe", "aggregate_daily"]; + } +`, + 'schemas/pipeline.dcdl': `Pipeline = { + name = String; + source.topic = String; + sink.table = String; + workers = Int & > 0 default 1; + batch.size = Int & >= 100 default 1000; + batch.timeout_seconds = Int & > 0 default 60; + alerts.enabled = Bool default false; +}; +`, + 'presets/batch.dcdl': `standardTransforms = ["parse_json", "validate_schema", "enrich_metadata"]; + +Batch = { + batch.size = 5000; + batch.timeout_seconds = 30 + 30; +}; +`, + }, + }, + { + id: 'diagnostics', + title: 'Diagnostics: invalid port', + activePath: 'main.dcdl', + files: { + 'main.dcdl': `let + schema = import "./schemas/service.dcdl"; +in + schema.Service & { + name = "broken-api"; + port = 442; + } +`, + 'schemas/service.dcdl': `Service = { + name = String; + port = Int & > 443 default 8443; + replicas = Int & > 0 default 2; +}; +`, + }, + }, +]; diff --git a/site/decodal-site/src/scripts/playground.js b/site/decodal-site/src/scripts/playground.js index f52e597..b0b79b1 100644 --- a/site/decodal-site/src/scripts/playground.js +++ b/site/decodal-site/src/scripts/playground.js @@ -1,26 +1,9 @@ import init, { evaluateProject } from '../wasm/decodal_wasm.js'; import { highlightDecodal } from '../lib/highlight.js'; +import { playgroundExamples } from './playground-examples.js'; const STORAGE_KEY = 'decodal-playground-project-v1'; - -const starterFiles = { - 'main.dcdl': `let - schema = import "./schemas/service.dcdl"; -in - schema.Service & { - name = "api"; - port = 9000 + 443; - tags = ["web"] ++ ["prod"]; - feature.enable = 9000 + 443 > 9000 && true; - } -`, - 'schemas/service.dcdl': `Service = { - name = String; - port = Int & > 443 default 8443; - feature.enable = Bool default true; -}; -`, -}; +const starterProject = playgroundExamples[0]; const source = document.getElementById('source'); const sourceHighlight = document.getElementById('source-highlight'); @@ -31,9 +14,19 @@ const fileTree = document.getElementById('file-tree'); const activeFile = document.getElementById('active-file'); const newFile = document.getElementById('new-file'); const deleteFile = document.getElementById('delete-file'); +const exampleSelect = document.getElementById('example-select'); +const loadExample = document.getElementById('load-example'); const project = loadProject(); +for (const example of playgroundExamples) { + const option = document.createElement('option'); + option.value = example.id; + option.textContent = example.title; + exampleSelect.append(option); +} +exampleSelect.value = starterProject.id; + setActiveFile(project.activePath); renderFileTree(); updateHighlight(); @@ -49,7 +42,11 @@ function loadProject() { } catch (_error) { // Fall back to the starter project. } - return { files: { ...starterFiles }, activePath: 'main.dcdl' }; + return { files: cloneFiles(starterProject.files), activePath: starterProject.activePath }; +} + +function cloneFiles(files) { + return Object.fromEntries(Object.entries(files).map(([path, source]) => [path, source])); } function normalizeFiles(files) { @@ -65,6 +62,17 @@ function saveProject() { localStorage.setItem(STORAGE_KEY, JSON.stringify(project)); } +function loadExampleProject(exampleId) { + const example = playgroundExamples.find((item) => item.id === exampleId) ?? starterProject; + project.files = cloneFiles(example.files); + project.activePath = example.activePath; + exampleSelect.value = example.id; + setActiveFile(project.activePath); + output.textContent = ''; + output.classList.remove('error'); + execute(); +} + function normalizePath(path) { const parts = []; for (const part of String(path).replaceAll('\\', '/').split('/')) { @@ -165,6 +173,12 @@ try { } run.addEventListener('click', execute); +loadExample.addEventListener('click', () => { + const example = playgroundExamples.find((item) => item.id === exampleSelect.value); + if (!example) return; + if (!confirm(`Load example "${example.title}"? This replaces the current virtual files.`)) return; + loadExampleProject(example.id); +}); newFile.addEventListener('click', () => { const path = normalizePath(prompt('New virtual file path', 'schemas/types.dcdl') ?? ''); if (!path) return; @@ -172,7 +186,7 @@ newFile.addEventListener('click', () => { setActiveFile(path); return; } - project.files[path] = path.endsWith('.dcdl') ? '' : '// Decodal source\n'; + project.files[path] = 'value = "new";\n'; setActiveFile(path); }); deleteFile.addEventListener('click', () => { diff --git a/site/decodal-site/src/style.css b/site/decodal-site/src/style.css index d8b8ed8..4d6ee42 100644 --- a/site/decodal-site/src/style.css +++ b/site/decodal-site/src/style.css @@ -281,7 +281,30 @@ main.playground { .playground-actions { align-items: center; display: flex; - gap: 12px; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-end; +} + +.example-picker { + align-items: center; + color: var(--muted); + display: flex; + font-size: 12px; + font-weight: 700; + gap: 6px; + text-transform: uppercase; +} + +.example-picker select { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font: inherit; + min-width: 180px; + padding: 7px 10px; + text-transform: none; } button {