diff --git a/blobl-editor/wasm/main.go b/blobl-editor/wasm/main.go index 6477a60c..5a5fd016 100644 --- a/blobl-editor/wasm/main.go +++ b/blobl-editor/wasm/main.go @@ -10,7 +10,12 @@ import ( _ "github.com/redpanda-data/connect/v4/public/components/pure/extended" ) +var globalEnv *bloblang.Environment + func main() { + // Initialize the global Bloblang environment + globalEnv = createMockEnvironment() + js.Global().Set("blobl", js.FuncOf(blobl)) // Wait for a signal to shut down @@ -18,44 +23,196 @@ func main() { } func blobl(_ js.Value, args []js.Value) any { - if len(args) != 2 { - return fmt.Sprintf("Expected two arguments, received %d instead", len(args)) + if len(args) < 2 || len(args) > 3 { + return fmt.Sprintf("Expected 2 or 3 arguments, received %d instead", len(args)) } - mapping, err := bloblang.NewEnvironment().Parse(args[0].String()) + // Parse the mapping + mapping, err := globalEnv.Parse(args[0].String()) if err != nil { return fmt.Sprintf("Failed to parse mapping: %s", err) } - msg, err := service.NewMessage([]byte(args[1].String())).BloblangQuery(mapping) + // Parse the payload JSON + var payload map[string]any + if err := json.Unmarshal([]byte(args[1].String()), &payload); err != nil { + return fmt.Sprintf("Failed to parse payload: %s", err) + } + + // Parse the optional metadata + metadata := map[string]any{} + if len(args) == 3 { + if err := json.Unmarshal([]byte(args[2].String()), &metadata); err != nil { + return fmt.Sprintf("Failed to parse metadata: %s", err) + } + } + + + // Serialize the payload for the message + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Sprintf("Failed to serialize payload: %s", err) + } + + // Create a new message with the payload + msg := service.NewMessage(payloadBytes) + + // Apply metadata to the message + for key, value := range metadata { + strValue, ok := value.(string) + if !ok { + return fmt.Errorf("metadata value for key '%s' must be a string, got %T", key, value) + } + msg.MetaSet(key, strValue) + } + + // Execute the mapping + result, err := msg.BloblangQuery(mapping) if err != nil { return fmt.Sprintf("Failed to execute mapping: %s", err) } - message, err := msg.AsStructured() + // Extract the structured message + message, err := result.AsStructured() if err != nil { return fmt.Sprintf("Failed to marshal message: %s", err) } - var metadata map[string]any - msg.MetaWalkMut(func(key string, value any) error { - if metadata == nil { - metadata = make(map[string]any) + // Extract metadata + var extractedMetadata map[string]any + result.MetaWalkMut(func(key string, value any) error { + if extractedMetadata == nil { + extractedMetadata = make(map[string]any) } - metadata[key] = value + extractedMetadata[key] = value return nil }) + // Marshal the final output var output []byte if output, err = json.MarshalIndent(struct { Msg any `json:"msg"` Meta map[string]any `json:"meta,omitempty"` }{ Msg: message, - Meta: metadata, + Meta: extractedMetadata, }, "", " "); err != nil { return fmt.Sprintf("Failed to marshal output: %s", err) } return string(output) } + +// createMockEnvironment creates a shared Bloblang environment with mocked I/O functions. +func createMockEnvironment() *bloblang.Environment { + env := bloblang.NewEnvironment() + + // Mock `env` function + env.RegisterFunction("env", func(args ...any) (bloblang.Function, error) { + return func() (any, error) { + var name string + var noCache bool + + if len(args) == 1 { + name, _ = args[0].(string) + } else if len(args) == 2 { + switch v := args[0].(type) { + case string: + name = v + noCache, _ = args[1].(bool) + case map[string]any: + name, _ = v["name"].(string) + noCache, _ = v["no_cache"].(bool) + default: + return nil, fmt.Errorf("invalid argument format for `env`") + } + } else { + return nil, fmt.Errorf("invalid number of arguments for `env`") + } + + mockValues := map[string]string{ + "key": "mocked_value", + } + if val, exists := mockValues[name]; exists { + if noCache { + return val + " (no cache)", nil + } + return val, nil + } + return nil, nil + }, nil + }) + + // Mock `file` function + env.RegisterFunction("file", func(args ...any) (bloblang.Function, error) { + return func() (any, error) { + var path string + var noCache bool + + if len(args) == 1 { + path, _ = args[0].(string) + } else if len(args) == 2 { + params, ok := args[0].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid argument format for `file`") + } + path, _ = params["path"].(string) + noCache, _ = params["no_cache"].(bool) + } else { + return nil, fmt.Errorf("invalid number of arguments for `file`") + } + + mockFiles := map[string]string{ + "/mock/path/file.json": `{"hello": "world"}`, + } + if content, exists := mockFiles[path]; exists { + if noCache { + return content + " (no_cache)", nil + } + return content, nil + } + return nil, fmt.Errorf("file not found: %s", path) + }, nil + }) + + // Mock `file_rel` function + env.RegisterFunction("file_rel", func(args ...any) (bloblang.Function, error) { + return func() (any, error) { + var path string + var noCache bool + + if len(args) == 1 { + path, _ = args[0].(string) + } else if len(args) == 2 { + params, ok := args[0].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid argument format for `file_rel`") + } + path, _ = params["path"].(string) + noCache, _ = params["no_cache"].(bool) + } else { + return nil, fmt.Errorf("invalid number of arguments for `file_rel`") + } + + mockFiles := map[string]string{ + "relative/path/file.json": `{"hello": "world"}`, + } + if content, exists := mockFiles[path]; exists { + if noCache { + return content + " (no_cache)", nil + } + return content, nil + } + return nil, fmt.Errorf("file not found: %s", path) + }, nil + }) + + // Mock `hostname` function + env.RegisterFunction("hostname", func(args ...any) (bloblang.Function, error) { + return func() (any, error) { + return "mocked-hostname", nil + }, nil + }) + + return env +} diff --git a/src/css/bloblang-playground.css b/src/css/bloblang-playground.css index 5870c88d..ce374327 100644 --- a/src/css/bloblang-playground.css +++ b/src/css/bloblang-playground.css @@ -127,10 +127,6 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut margin: 0; } -.bloblang-playground .output-section .editor-container details { - height: 100%; -} - .bloblang-playground .editor-container summary { padding: 10px; cursor: pointer; @@ -145,27 +141,31 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut /* Editor areas */ .bloblang-playground .editor { - font-size: 12pt; + font-size: var(--body-font-size); background-color: var(--pre-background) !important; color: var(--code-font-color) !important; - height: 250px; + min-height: 130px; padding: 10px; overflow-y: auto; + border-radius: 5px; position: relative; border: 1px solid rgb(204, 204, 204); - resize: both; + resize: horizontal; min-width: 200px; - min-height: 100px; } /* Output section (right side) */ .bloblang-playground .output-section { - flex: 1 1 35%; + flex: 1 1 45%; gap: 20px; display: flex; flex-direction: column; } +.bloblang-playground #ace-input-metadata { + min-height: unset; +} + .bloblang-playground .choices[data-type*=select-one] .choices__inner { padding: 0; } @@ -213,6 +213,46 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut gap: 5px; } +.bloblang-snippet { + display: flex; + flex-direction: column; + gap: 1em; + padding: 1.5em; + border: 1px solid rgb(204, 204, 204); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 2em 0; +} + +.bloblang-snippet .bloblang-editor { + font-family: var(--monospace-font-family); + font-size: var(--body-font-size); + resize: horizontal; +} + +.bloblang-snippet .row { + display: flex; + gap: 1em; + flex-wrap: wrap; +} + +.bloblang-snippet .box { + flex: 1; +} + +.bloblang-snippet .box.full-width { + flex: 0 0 100%; +} + +.bloblang-snippet .ace-editor { + width: 100%; + border: 1px solid rgb(204, 204, 204); + border-radius: 5px; + min-height: 75px; + color: var(--code-font-color); + background-color: var(--pre-background); +} + @media (max-width: 1024px) { .bloblang-playground .banner { margin-left: -1rem; @@ -244,4 +284,8 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut .bloblang-playground .doc { margin-right: unset; } + + .bloblang-snippet .row { + flex-direction: column; + } } diff --git a/src/partials/bloblang-playground.hbs b/src/partials/bloblang-playground.hbs index c51dc4f5..cef6bcb7 100644 --- a/src/partials/bloblang-playground.hbs +++ b/src/partials/bloblang-playground.hbs @@ -17,9 +17,13 @@
- Input (JSON) + Input
+
+ Input metadata +
+
@@ -34,11 +38,11 @@
Output -
"Output will appear here..."
+
"Output will appear here..."
- Metadata output -
"Metadata will appear here..."
+ Output metadata +
"Metadata will appear here..."
@@ -58,6 +62,7 @@ const TAB_SIZE = 2; // Keys for sessionStorage const sessionStorageKeys = { input: "blobl-editor-input", + metadata: "blobl-editor-metadata", mapping: "blobl-editor-mapping", }; @@ -65,10 +70,35 @@ const sessionStorageKeys = { const defaultInput = `{ "numbers": [1, 2, 3, 4, 5] }`; +const defaultMetaInput = "{}"; const defaultMapping = `root.even_numbers = this.numbers.filter(n -> n % 2 == 0) root.sum = this.numbers.sum()`; const defaultOutput = "Output will appear here..."; -const defaultMeta = "Output metadata will appear here..."; +const defaultMetaOutput = "Output metadata will appear here..."; + +// Initialize ACE Editors +function initializeAceEditor(editorId, mode, readOnly = false, initialValue = '') { + const editor = ace.edit(editorId); + editor.setTheme('ace/theme/github'); + editor.session.setMode(mode); + editor.setReadOnly(readOnly); + editor.setValue(prettifyJSON(initialValue), 1); + editor.session.setTabSize(TAB_SIZE); + editor.session.setUseSoftTabs(true); + editor.setOptions({ + minLines: 1, // Minimum height + maxLines: 50, // Allow growth + }); + return editor; +} + +function prettifyJSON(json) { + try { + return JSON.stringify(JSON.parse(json), null, 2); + } catch (error) { + return json; // Return original value if it's not valid JSON + } +} document.addEventListener("DOMContentLoaded", () => { metadataDetails = document.getElementById("metadata-details"); @@ -80,35 +110,19 @@ document.addEventListener("DOMContentLoaded", () => { } // Initialize input editor - aceInputEditor = ace.edit("ace-input"); - aceInputEditor.setTheme("ace/theme/github"); - aceInputEditor.session.setMode("ace/mode/json"); - aceInputEditor.session.setTabSize(TAB_SIZE); - aceInputEditor.session.setUseSoftTabs(true); + aceInputEditor = initializeAceEditor("ace-input", 'ace/mode/json', false, defaultInput); + + // Initialize input metadata editor + aceInputMetadataEditor = initializeAceEditor("ace-input-metadata", 'ace/mode/json', false, defaultMetaInput); // Initialize mapping editor - aceMappingEditor = ace.edit("ace-mapping"); - aceMappingEditor.setTheme("ace/theme/github"); - aceMappingEditor.session.setMode("ace/mode/coffee"); - aceMappingEditor.session.setOption("useWorker", false); - aceMappingEditor.session.setTabSize(TAB_SIZE); - aceMappingEditor.session.setUseSoftTabs(true); + aceMappingEditor = initializeAceEditor("ace-mapping", 'ace/mode/coffee', false, defaultMapping); // Initialize output editor - aceOutputEditor = ace.edit("ace-output"); - aceOutputEditor.setTheme("ace/theme/github"); - aceOutputEditor.session.setMode("ace/mode/text"); // JSON syntax highlighting - aceOutputEditor.setReadOnly(true); // Make output read-only - aceOutputEditor.session.setTabSize(TAB_SIZE); - aceOutputEditor.session.setUseSoftTabs(true); + aceOutputEditor = initializeAceEditor("ace-output", 'ace/mode/text', true, defaultMapping); // Initialize metadata editor - aceMetadataEditor = ace.edit("ace-metadata"); - aceMetadataEditor.setTheme("ace/theme/github"); - aceMetadataEditor.session.setMode("ace/mode/json"); // Use JSON mode for metadata - aceMetadataEditor.setReadOnly(true); // Make metadata read-only - aceMetadataEditor.session.setTabSize(TAB_SIZE); - aceMetadataEditor.session.setUseSoftTabs(true); + aceOutputMetadataEditor = initializeAceEditor("ace-metadata", 'ace/mode/text', true, defaultMetaOutput); choices = new Choices(document.getElementById("sample-dropdown"), { searchEnabled: true, @@ -130,16 +144,20 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("clear").addEventListener("click", () => { aceInputEditor.setValue(""); aceMappingEditor.setValue(""); + aceInputMetadataEditor.setValue("") aceOutputEditor.setValue(defaultOutput, 1); - aceMetadataEditor.setValue(defaultMeta, 1); + aceOutputMetadataEditor.setValue(defaultMetaOutput, 1); saveTosessionStorage(); }); // Handle prettify button document.getElementById("prettify").addEventListener("click", () => { try { - const formatted = JSON.stringify(JSON.parse(aceInputEditor.getValue()), null, 2); - aceInputEditor.setValue(formatted, 1); + const formattedInput = JSON.stringify(JSON.parse(aceInputEditor.getValue()), null, 2); + const formattedInputMetadata = JSON.stringify(JSON.parse(aceInputMetadataEditor.getValue()), null, 2); + aceInputEditor.setValue(formattedInput, 1); + saveTosessionStorage(); + aceInputMetadataEditor.setValue(formattedInputMetadata, 1); saveTosessionStorage(); } catch (error) { aceOutputEditor.setValue("Error: Invalid JSON input", 1); @@ -156,13 +174,14 @@ document.addEventListener("DOMContentLoaded", () => { if (sample) { aceInputEditor.setValue(sample.input, 1); aceMappingEditor.setValue(sample.mapping, 1); + aceInputMetadataEditor.setValue(sample.metadata || defaultMetaInput, 1); execute(); saveTosessionStorage(); } }); // Save content to sessionStorage and execute on changes - [aceInputEditor, aceMappingEditor].forEach((editor) => { + [aceInputEditor, aceInputMetadataEditor, aceMappingEditor].forEach((editor) => { editor.on("change", saveTosessionStorage); editor.on("change", execute); }); @@ -214,6 +233,10 @@ function getMapping() { return aceMappingEditor ? aceMappingEditor.getValue() : ""; } +function getInputMetadata() { + return aceInputMetadataEditor ? aceInputMetadataEditor.getValue() : ""; +} + function isValidJSON(str) { try { JSON.parse(str); @@ -228,10 +251,10 @@ function execute() { if (!metadataDetails) metadataDetails = document.getElementById("metadata-details"); aceOutputEditor.setValue(""); - aceMetadataEditor.setValue(""); + aceOutputMetadataEditor.setValue(""); try { - const result = blobl(getMapping(), getInput()); + const result = blobl(getMapping(), getInput(), getInputMetadata()); if (isValidJSON(result)) { const parsedResult = JSON.parse(result); @@ -246,27 +269,27 @@ function execute() { // Display metadata if (Object.keys(metadata).length > 0) { - aceMetadataEditor.session.setMode("ace/mode/json"); - aceMetadataEditor.setValue(JSON.stringify(metadata, null, 2), 1); + aceOutputMetadataEditor.session.setMode("ace/mode/json"); + aceOutputMetadataEditor.setValue(JSON.stringify(metadata, null, 2), 1); } else { - aceMetadataEditor.session.setMode("ace/mode/text"); - aceMetadataEditor.setValue("{}", 1); + aceOutputMetadataEditor.session.setMode("ace/mode/text"); + aceOutputMetadataEditor.setValue("{}", 1); } } else { // If the result is not JSON, handle it as raw text aceOutputEditor.session.setMode("ace/mode/text"); aceOutputEditor.setValue(result, 1); - aceMetadataEditor.session.setMode("ace/mode/text"); - aceMetadataEditor.setValue("No metadata available", 1); + aceOutputMetadataEditor.session.setMode("ace/mode/text"); + aceOutputMetadataEditor.setValue("No metadata available", 1); } } catch (error) { // Handle general errors aceOutputEditor.session.setMode("ace/mode/text"); aceOutputEditor.setValue("Error: " + error.message, 1); - aceMetadataEditor.session.setMode("ace/mode/text"); - aceMetadataEditor.setValue("Error: " + error.message, 1); + aceOutputMetadataEditor.session.setMode("ace/mode/text"); + aceOutputMetadataEditor.setValue("Error: " + error.message, 1); } } @@ -274,12 +297,14 @@ function execute() { function saveTosessionStorage() { sessionStorage.setItem(sessionStorageKeys.input, aceInputEditor.getValue()); sessionStorage.setItem(sessionStorageKeys.mapping, aceMappingEditor.getValue()); + sessionStorage.setItem(sessionStorageKeys.metadata, aceInputMetadataEditor.getValue()); } // Restore editor content from sessionStorage function restoreFromStorage() { const savedInput = sessionStorage.getItem(sessionStorageKeys.input); const savedMapping = sessionStorage.getItem(sessionStorageKeys.mapping); + const savedMeta = sessionStorage.getItem(sessionStorageKeys.metadata); const dropdownElement = document.getElementById("sample-dropdown"); const samples = {{{page.attributes.bloblang-samples}}}; @@ -291,16 +316,19 @@ function restoreFromStorage() { if (defaultSample) { aceInputEditor.setValue(defaultSample.input, 1); aceMappingEditor.setValue(defaultSample.mapping, 1); + defaultSample.metadata ? aceInputMetadataEditor.setValue(defaultSample.metadata, 1) : aceInputMetadataEditor.setValue(defaultMetaInput, 1) choices.setChoiceByValue(defaultSample.title) } else { aceInputEditor.setValue(defaultInput, 1); aceMappingEditor.setValue(defaultMapping, 1); + aceInputMetadataEditor.setValue(defaultMetaInput, 1); choices.setChoiceByValue(""); } } else { // Restore values from storage aceInputEditor.setValue(savedInput || defaultInput, 1); aceMappingEditor.setValue(savedMapping || defaultMapping, 1); + aceInputMetadataEditor.setValue(savedMeta || defaultMetaInput, 1); // Find and select the corresponding dropdown option const matchingSample = Object.values(samples).find( @@ -315,7 +343,7 @@ function restoreFromStorage() { } aceOutputEditor.setValue(defaultOutput, 1); - aceMetadataEditor.setValue(defaultMeta, 1); + aceOutputMetadataEditor.setValue(defaultMetaOutput, 1); } diff --git a/src/partials/head-scripts.hbs b/src/partials/head-scripts.hbs index 42a2efd2..799369a1 100644 --- a/src/partials/head-scripts.hbs +++ b/src/partials/head-scripts.hbs @@ -51,7 +51,7 @@ data-modal-disclaimer="This is a custom LLM for Redpanda with access to all deve {{/if}} -{{#if (eq page.attributes.role 'bloblang-playground')}} +{{#if (or (eq page.attributes.role 'bloblang-playground')(eq page.attributes.role 'bloblang-snippets'))}}