Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add mock I/O functions to Blobl playground and support input metadata #241

Merged
merged 3 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 168 additions & 11 deletions blobl-editor/wasm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,209 @@ 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
select {}
}

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
}
62 changes: 53 additions & 9 deletions src/css/bloblang-playground.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Loading