This commit is contained in:
sujit
2025-10-07 23:06:32 +05:45
parent 9a2af7761f
commit 35cdedafc7
17 changed files with 805 additions and 161 deletions

View File

@@ -37,7 +37,7 @@ type Condition interface {
type ConditionProcessor interface {
Processor
SetConditions(map[string]Condition)
SetConditions(map[string]Condition, ...map[string]string)
}
type Provider struct {

View File

@@ -8,7 +8,7 @@ import (
"sort"
"strings"
"sync"
"github.com/oarkflow/jsonschema"
)
@@ -43,7 +43,7 @@ var fieldTemplates = map[string]string{
"output": `<output {{.AllAttributes}}>{{.Content}}</output>`,
"progress": `<progress {{.AllAttributes}}>{{.Content}}</progress>`,
"meter": `<meter {{.AllAttributes}}>{{.Content}}</meter>`,
// Text content elements
"h1": `<h1 {{.AllAttributes}}>{{.Content}}</h1>`,
"h2": `<h2 {{.AllAttributes}}>{{.Content}}</h2>`,
@@ -69,7 +69,7 @@ var fieldTemplates = map[string]string{
"abbr": `<abbr {{.AllAttributes}}>{{.Content}}</abbr>`,
"address": `<address {{.AllAttributes}}>{{.ContentHTML}}</address>`,
"time": `<time {{.AllAttributes}}>{{.Content}}</time>`,
// List elements
"ul": `<ul {{.AllAttributes}}>{{.ContentHTML}}</ul>`,
"ol": `<ol {{.AllAttributes}}>{{.ContentHTML}}</ol>`,
@@ -77,7 +77,7 @@ var fieldTemplates = map[string]string{
"dl": `<dl {{.AllAttributes}}>{{.ContentHTML}}</dl>`,
"dt": `<dt {{.AllAttributes}}>{{.Content}}</dt>`,
"dd": `<dd {{.AllAttributes}}>{{.Content}}</dd>`,
// Links and media
"a": `<a {{.AllAttributes}}>{{.Content}}</a>`,
"img": `<img {{.AllAttributes}} />`,
@@ -87,7 +87,7 @@ var fieldTemplates = map[string]string{
"video": `<video {{.AllAttributes}}>{{.ContentHTML}}</video>`,
"source": `<source {{.AllAttributes}} />`,
"track": `<track {{.AllAttributes}} />`,
// Table elements
"table": `<table {{.AllAttributes}}>{{.ContentHTML}}</table>`,
"caption": `<caption {{.AllAttributes}}>{{.Content}}</caption>`,
@@ -99,7 +99,7 @@ var fieldTemplates = map[string]string{
"td": `<td {{.AllAttributes}}>{{.Content}}</td>`,
"colgroup": `<colgroup {{.AllAttributes}}>{{.ContentHTML}}</colgroup>`,
"col": `<col {{.AllAttributes}} />`,
// Sectioning elements
"article": `<article {{.AllAttributes}}>{{.ContentHTML}}</article>`,
"section": `<section {{.AllAttributes}}>{{.ContentHTML}}</section>`,
@@ -108,12 +108,12 @@ var fieldTemplates = map[string]string{
"header": `<header {{.AllAttributes}}>{{.ContentHTML}}</header>`,
"footer": `<footer {{.AllAttributes}}>{{.ContentHTML}}</footer>`,
"main": `<main {{.AllAttributes}}>{{.ContentHTML}}</main>`,
// Interactive elements
"details": `<details {{.AllAttributes}}>{{.ContentHTML}}</details>`,
"summary": `<summary {{.AllAttributes}}>{{.Content}}</summary>`,
"dialog": `<dialog {{.AllAttributes}}>{{.ContentHTML}}</dialog>`,
// Embedded content
"iframe": `<iframe {{.AllAttributes}}>{{.Content}}</iframe>`,
"embed": `<embed {{.AllAttributes}} />`,
@@ -122,12 +122,12 @@ var fieldTemplates = map[string]string{
"picture": `<picture {{.AllAttributes}}>{{.ContentHTML}}</picture>`,
"canvas": `<canvas {{.AllAttributes}}>{{.Content}}</canvas>`,
"svg": `<svg {{.AllAttributes}}>{{.ContentHTML}}</svg>`,
// Meta elements
"br": `<br {{.AllAttributes}} />`,
"hr": `<hr {{.AllAttributes}} />`,
"wbr": `<wbr {{.AllAttributes}} />`,
// Generic template for any unlisted element
"generic": `<{{.Element}} {{.AllAttributes}}>{{.ContentHTML}}</{{.Element}}>`,
"void": `<{{.Element}} {{.AllAttributes}} />`,
@@ -154,10 +154,10 @@ func init() {
if err != nil {
panic(fmt.Sprintf("Failed to compile group template: %v", err))
}
templateCacheMutex.Lock()
defer templateCacheMutex.Unlock()
for element, tmplStr := range fieldTemplates {
compiled, err := template.New(element).Parse(tmplStr)
if err == nil {
@@ -228,13 +228,13 @@ func NewJSONSchemaRenderer(schema *jsonschema.Schema, htmlLayout string) *JSONSc
Schema: schema,
HTMLLayout: htmlLayout,
}
// Pre-compile layout template
renderer.compileLayoutTemplate()
// Pre-parse and cache groups and form config
renderer.precomputeStaticData()
return renderer
}
@@ -252,7 +252,7 @@ func (r *JSONSchemaRenderer) compileLayoutTemplate() {
r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype))
},
}).Parse(r.HTMLLayout)
if err == nil {
r.compiledLayout = tmpl
}
@@ -262,7 +262,7 @@ func (r *JSONSchemaRenderer) compileLayoutTemplate() {
func (r *JSONSchemaRenderer) precomputeStaticData() {
r.cachedGroups = r.parseGroupsFromSchema()
r.cachedButtons = r.renderButtons()
// Cache form configuration
if r.Schema.Form != nil {
if class, ok := r.Schema.Form["class"].(string); ok {
@@ -285,7 +285,7 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
if len(data) == 0 {
return templateStr
}
tmpl, err := template.New("interpolate").Parse(templateStr)
if err != nil {
// Fallback to simple string replacement if template parsing fails
@@ -298,13 +298,13 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
}
return result
}
var templateResult bytes.Buffer
err = tmpl.Execute(&templateResult, data)
if err != nil {
return templateStr // Return original string if execution fails
}
return templateResult.String()
}
@@ -312,22 +312,22 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
r.cacheMutex.RLock()
defer r.cacheMutex.RUnlock()
// Use a string builder for efficient string concatenation
var groupHTML strings.Builder
groupHTML.Grow(1024) // Pre-allocate reasonable capacity
for _, group := range r.cachedGroups {
groupHTML.WriteString(renderGroupWithData(group, data))
}
// Interpolate dynamic form action
formAction := r.interpolateTemplate(r.formConfig.Action, data)
// Use pre-compiled template if available
if r.compiledLayout != nil {
var renderedHTML bytes.Buffer
templateData := struct {
GroupsHTML string
FormAction string
@@ -335,9 +335,15 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
GroupsHTML: groupHTML.String(),
FormAction: formAction,
}
// Update the template functions with current data
updatedTemplate := r.compiledLayout.Funcs(template.FuncMap{
"error_message": func() string {
if msg, ok := data["error_message"].(string); ok {
return msg
}
return ""
},
"form_groups": func() template.HTML {
return template.HTML(templateData.GroupsHTML)
},
@@ -349,14 +355,14 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
r.formConfig.Class, templateData.FormAction, r.formConfig.Method, r.formConfig.Enctype))
},
})
if err := updatedTemplate.Execute(&renderedHTML, nil); err != nil {
return "", fmt.Errorf("failed to execute compiled template: %w", err)
}
return renderedHTML.String(), nil
}
// Fallback to original method if compilation failed
return r.renderFieldsFallback(data)
}
@@ -367,11 +373,17 @@ func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string,
for _, group := range r.cachedGroups {
groupHTML.WriteString(renderGroupWithData(group, data))
}
formAction := r.interpolateTemplate(r.formConfig.Action, data)
// Create a new template with the layout and functions
tmpl, err := template.New("layout").Funcs(template.FuncMap{
"error_message": func() string {
if msg, ok := data["error_message"].(string); ok {
return msg
}
return ""
},
"form_groups": func() template.HTML {
return template.HTML(groupHTML.String())
},
@@ -386,12 +398,12 @@ func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string,
if err != nil {
return "", fmt.Errorf("failed to parse HTML layout: %w", err)
}
var renderedHTML bytes.Buffer
if err := tmpl.Execute(&renderedHTML, nil); err != nil {
return "", fmt.Errorf("failed to execute HTML template: %w", err)
}
return renderedHTML.String(), nil
}
@@ -400,26 +412,26 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
if r.Schema.Form == nil {
return nil
}
groupsData, ok := r.Schema.Form["groups"]
if !ok {
return nil
}
groups, ok := groupsData.([]any)
if !ok {
return nil
}
var result []GroupInfo
var groupedFields = make(map[string]bool) // Track fields that are already in groups
for _, group := range groups {
groupMap, ok := group.(map[string]any)
if !ok {
continue
}
var groupTitle GroupTitle
if titleMap, ok := groupMap["title"].(map[string]any); ok {
if text, ok := titleMap["text"].(string); ok {
@@ -429,12 +441,12 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
groupTitle.Class = class
}
}
groupClass, _ := groupMap["class"].(string)
if groupClass == "" {
groupClass = "form-group-fields"
}
var fields []FieldInfo
if fieldsData, ok := groupMap["fields"].([]any); ok {
for _, fieldName := range fieldsData {
@@ -449,7 +461,7 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
}
}
}
// Sort fields by order
sort.Slice(fields, func(i, j int) bool {
orderI := 0
@@ -462,14 +474,14 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
}
return orderI < orderJ
})
result = append(result, GroupInfo{
Title: groupTitle,
Fields: fields,
GroupClass: groupClass,
})
}
// Add ungrouped hidden fields to the first group or create a hidden group
if r.Schema.Properties != nil {
var hiddenFields []FieldInfo
@@ -494,7 +506,7 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
}
}
}
// If we have hidden fields, add them to the first group or create a new hidden group
if len(hiddenFields) > 0 {
if len(result) > 0 {
@@ -510,31 +522,31 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
}
}
}
return result
}
// extractFieldsFromPath recursively extracts fields from a path, handling nested properties
func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) []FieldInfo {
var fields []FieldInfo
// Build the full path
fullPath := fieldPath
if parentPath != "" {
fullPath = parentPath + "." + fieldPath
}
// Navigate to the schema at this path
schema := r.getSchemaAtPath(fieldPath)
if schema == nil {
return fields
}
// For nested paths like "company.address.street", we need to check if the final part
// is required in its immediate parent's schema
var fieldName string
var parentSchemaPath string
pathParts := strings.Split(fieldPath, ".")
if len(pathParts) > 1 {
// Extract the field name (last part) and parent path (all but last)
@@ -545,10 +557,10 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
fieldName = fieldPath
parentSchemaPath = parentPath
}
// Check if this field is required at the parent level
isRequired := r.isFieldRequiredAtPath(fieldName, parentSchemaPath)
// If this schema has properties, it's a nested object
if schema.Properties != nil && len(*schema.Properties) > 0 {
// Recursively process nested properties
@@ -562,7 +574,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
if schema.Order != nil {
order = *schema.Order
}
fields = append(fields, FieldInfo{
Name: fieldName,
FieldPath: fullPath,
@@ -572,23 +584,23 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
Validation: extractValidationInfo(schema, isRequired),
})
}
return fields
}
// extractFieldsFromNestedSchema processes nested schema properties
func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, _ bool) []FieldInfo {
var fields []FieldInfo
fullPath := propName
if parentPath != "" {
fullPath = parentPath + "." + propName
}
// Check if this field is required at its immediate parent level
// The parent schema path is the current parentPath, not one level up
isRequired := r.isFieldRequiredAtPath(propName, parentPath)
// If this property has nested properties, recurse
if propSchema.Properties != nil && len(*propSchema.Properties) > 0 {
for nestedPropName, nestedPropSchema := range *propSchema.Properties {
@@ -601,7 +613,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
if propSchema.Order != nil {
order = *propSchema.Order
}
fields = append(fields, FieldInfo{
Name: propName,
FieldPath: fullPath,
@@ -611,7 +623,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
Validation: extractValidationInfo(propSchema, isRequired),
})
}
return fields
}
@@ -620,29 +632,29 @@ func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema {
if r.Schema.Properties == nil {
return nil
}
parts := strings.Split(path, ".")
currentSchema := r.Schema
for _, part := range parts {
if currentSchema.Properties == nil {
return nil
}
if propSchema, exists := (*currentSchema.Properties)[part]; exists {
currentSchema = propSchema
} else {
return nil
}
}
return currentSchema
}
// isFieldRequiredAtPath checks if a field is required at a specific schema path
func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool {
var schema *jsonschema.Schema
if schemaPath == "" {
// Check at root level
schema = r.Schema
@@ -650,11 +662,11 @@ func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string)
// Navigate to the schema at the given path
schema = r.getSchemaAtPath(schemaPath)
}
if schema == nil {
return false
}
return contains(schema.Required, fieldName)
}
@@ -666,18 +678,18 @@ func renderGroup(group GroupInfo) string {
for _, field := range group.Fields {
fieldsHTML.WriteString(renderField(field))
}
tmpl := template.Must(template.New("group").Parse(groupTemplateStr))
data := map[string]any{
"Title": group.Title,
"GroupClass": group.GroupClass,
"FieldsHTML": template.HTML(fieldsHTML.String()),
}
if err := tmpl.Execute(&groupHTML, data); err != nil {
return "" // Return empty string on error
}
return groupHTML.String()
}
@@ -688,20 +700,20 @@ func renderGroupWithData(group GroupInfo, data map[string]any) string {
for _, field := range group.Fields {
allValidations[field.FieldPath] = field.Validation
}
// Use string builder for better performance
var fieldsHTML strings.Builder
fieldsHTML.Grow(512) // Pre-allocate reasonable capacity
for _, field := range group.Fields {
fieldsHTML.WriteString(renderFieldWithContextAndData(field, allValidations, data))
}
// Use pre-compiled group template
templateCacheMutex.RLock()
groupTemplate := compiledGroupTemplate
templateCacheMutex.RUnlock()
if groupTemplate != nil {
var groupHTML bytes.Buffer
templateData := map[string]any{
@@ -709,12 +721,12 @@ func renderGroupWithData(group GroupInfo, data map[string]any) string {
"GroupClass": group.GroupClass,
"FieldsHTML": template.HTML(fieldsHTML.String()),
}
if err := groupTemplate.Execute(&groupHTML, templateData); err == nil {
return groupHTML.String()
}
}
// Fallback to original method
return renderGroup(group)
}
@@ -735,18 +747,18 @@ func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]Va
return ""
}
allAttributes := buildAttributesWithValidation(field, data)
content := getFieldContent(field)
contentHTML := getFieldContentHTML(field)
var labelHTML string
if element != "input" || getInputTypeFromSchema(field.Schema) != "hidden" {
labelHTML = generateLabel(field)
}
optionsHTML := generateOptionsFromSchema(field.Schema)
validationJS := generateClientSideValidation(field.FieldPath, field.Validation, allValidations)
templateData := map[string]any{
"Element": element,
"AllAttributes": template.HTMLAttr(allAttributes),
@@ -756,7 +768,7 @@ func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]Va
"Class": getFieldWrapperClass(field.Schema),
"OptionsHTML": template.HTML(optionsHTML),
}
var tmplStr string
if element == "input" && getInputTypeFromSchema(field.Schema) == "hidden" {
if template, exists := fieldTemplates["input_hidden"]; exists {
@@ -771,7 +783,7 @@ func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]Va
} else {
tmplStr = fieldTemplates["generic"]
}
tmpl := template.Must(template.New(element).Parse(tmplStr))
var buf bytes.Buffer
if err := tmpl.Execute(&buf, templateData); err != nil {
@@ -787,15 +799,15 @@ func determineFieldElement(schema *jsonschema.Schema) string {
return element
}
}
if shouldUseSelect(schema) {
return "select"
}
if shouldUseTextarea(schema) {
return "textarea"
}
var typeStr string
if len(schema.Type) > 0 {
typeStr = schema.Type[0]
@@ -816,7 +828,7 @@ func determineFieldElement(schema *jsonschema.Schema) string {
func buildAttributesWithValidation(field FieldInfo, data map[string]any) string {
var builder strings.Builder
builder.Grow(512) // Pre-allocate capacity
// Check if UI specifies a custom name, otherwise use field path for nested fields
var fieldName string
if field.Schema.UI != nil {
@@ -824,7 +836,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
fieldName = customName
}
}
// Fallback to field path or field name
if fieldName == "" {
fieldName = field.FieldPath
@@ -832,18 +844,18 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
fieldName = field.Name
}
}
// Add name attribute
builder.WriteString(`name="`)
builder.WriteString(fieldName)
builder.WriteString(`"`)
// Add id attribute for accessibility
fieldId := strings.ReplaceAll(fieldName, ".", "_")
builder.WriteString(` id="`)
builder.WriteString(fieldId)
builder.WriteString(`"`)
// Determine and add type attribute for input elements
element := determineFieldElement(field.Schema)
if element == "input" {
@@ -852,14 +864,14 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
builder.WriteString(inputType)
builder.WriteString(`"`)
}
// Add validation attributes
validationAttrs := generateValidationAttributes(field.Validation)
for _, attr := range validationAttrs {
builder.WriteString(` `)
builder.WriteString(attr)
}
// Add default value with interpolation
defaultValue := getDefaultValue(field.Schema)
if field.Schema.UI != nil {
@@ -877,7 +889,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
builder.WriteString(defaultValue)
builder.WriteString(`"`)
}
// Add placeholder with interpolation
placeholder := getPlaceholder(field.Schema)
if field.Schema.UI != nil {
@@ -894,7 +906,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
builder.WriteString(placeholder)
builder.WriteString(`"`)
}
// Add standard attributes from UI with interpolation
if field.Schema.UI != nil {
for _, attr := range standardAttrs {
@@ -907,13 +919,13 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
if attr == "class" && value == "" {
continue // Skip empty class
}
// Apply interpolation to string values if data is available
valueStr := fmt.Sprintf("%v", value)
if data != nil && strings.Contains(valueStr, "{{") {
valueStr = interpolateString(valueStr, data)
}
builder.WriteString(` `)
builder.WriteString(attr)
builder.WriteString(`="`)
@@ -921,7 +933,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
builder.WriteString(`"`)
}
}
// Add data-* and aria-* attributes with interpolation
for key, value := range field.Schema.UI {
if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
@@ -929,7 +941,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
if data != nil && strings.Contains(valueStr, "{{") {
valueStr = interpolateString(valueStr, data)
}
builder.WriteString(` `)
builder.WriteString(key)
builder.WriteString(`="`)
@@ -938,14 +950,14 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
}
}
}
return builder.String()
}
// generateOptionsFromSchema generates option HTML from schema enum or UI options
func generateOptionsFromSchema(schema *jsonschema.Schema) string {
var optionsHTML strings.Builder
// Check UI options first
if schema.UI != nil {
if options, ok := schema.UI["options"].([]any); ok {
@@ -970,14 +982,14 @@ func generateOptionsFromSchema(schema *jsonschema.Schema) string {
return optionsHTML.String()
}
}
// Generate options from enum
if len(schema.Enum) > 0 {
for _, enumValue := range schema.Enum {
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, enumValue, enumValue))
}
}
return optionsHTML.String()
}
@@ -1006,16 +1018,16 @@ func getPlaceholder(schema *jsonschema.Schema) string {
return placeholder
}
}
// Auto-generate placeholder from title or description
if schema.Title != nil {
return fmt.Sprintf("Enter %s", strings.ToLower(*schema.Title))
}
if schema.Description != nil {
return *schema.Description
}
return ""
}
@@ -1032,7 +1044,7 @@ func getFieldContent(field FieldInfo) string {
return content
}
}
return ""
}
@@ -1042,13 +1054,13 @@ func getFieldContentHTML(field FieldInfo) string {
if contentHTML, ok := field.Schema.UI["contentHTML"].(string); ok {
return contentHTML
}
// Check for children elements
if children, ok := field.Schema.UI["children"].([]any); ok {
return renderChildren(children)
}
}
return ""
}
@@ -1063,7 +1075,7 @@ func renderChildren(children []any) string {
if title, ok := childMap["title"].(string); ok {
childSchema.Title = &title
}
childField := FieldInfo{
Name: getMapValue(childMap, "name", ""),
Schema: childSchema,
@@ -1081,7 +1093,7 @@ func generateLabel(field FieldInfo) string {
return ""
}
}
var title string
if field.Schema.Title != nil {
title = *field.Schema.Title
@@ -1089,18 +1101,18 @@ func generateLabel(field FieldInfo) string {
if title == "" {
return ""
}
fieldName := field.FieldPath
if fieldName == "" {
fieldName = field.Name
}
// Check if field is required
requiredSpan := ""
if field.IsRequired {
requiredSpan = ` <span class="required">*</span>`
}
return fmt.Sprintf(`<label for="%s">%s%s</label>`, fieldName, title, requiredSpan)
}
@@ -1125,19 +1137,19 @@ func (r *JSONSchemaRenderer) renderButtons() string {
if r.Schema.Form == nil {
return ""
}
var buttonsHTML bytes.Buffer
if submitConfig, ok := r.Schema.Form["submit"].(map[string]any); ok {
buttonHTML := renderButtonFromConfig(submitConfig, "submit")
buttonsHTML.WriteString(buttonHTML)
}
if resetConfig, ok := r.Schema.Form["reset"].(map[string]any); ok {
buttonHTML := renderButtonFromConfig(resetConfig, "reset")
buttonsHTML.WriteString(buttonHTML)
}
// Support for additional custom buttons
if buttons, ok := r.Schema.Form["buttons"].([]any); ok {
for _, button := range buttons {
@@ -1148,20 +1160,20 @@ func (r *JSONSchemaRenderer) renderButtons() string {
}
}
}
return buttonsHTML.String()
}
func renderButtonFromConfig(config map[string]any, defaultType string) string {
var attributes []string
buttonType := getMapValue(config, "type", defaultType)
attributes = append(attributes, fmt.Sprintf(`type="%s"`, buttonType))
if class := getMapValue(config, "class", ""); class != "" {
attributes = append(attributes, fmt.Sprintf(`class="%s"`, class))
}
// Add other button attributes
for key, value := range config {
switch key {
@@ -1171,9 +1183,9 @@ func renderButtonFromConfig(config map[string]any, defaultType string) string {
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
}
}
content := getMapValue(config, "label", getMapValue(config, "content", "Button"))
return fmt.Sprintf(`<button %s>%s</button>`,
strings.Join(attributes, " "), content)
}
@@ -1237,7 +1249,7 @@ func GetFromBytes(schemaContent []byte, template string, templateFiles ...string
</form>
`)
}
renderer := NewJSONSchemaRenderer(schema, string(htmlLayout))
cachedTemplate := &RequestSchemaTemplate{
Schema: schema,
@@ -1276,7 +1288,7 @@ func GetFromSchema(schema *jsonschema.Schema, template string, templateFiles ...
</form>
`)
}
renderer := NewJSONSchemaRenderer(schema, string(htmlLayout))
cachedTemplate := &RequestSchemaTemplate{
Schema: schema,
@@ -1319,7 +1331,7 @@ func interpolateString(templateStr string, data map[string]any) string {
if len(data) == 0 {
return templateStr
}
// First try Go template interpolation
tmpl, err := template.New("interpolate").Parse(templateStr)
if err == nil {
@@ -1329,7 +1341,7 @@ func interpolateString(templateStr string, data map[string]any) string {
return templateResult.String()
}
}
// Fallback to simple string replacement if template parsing/execution fails
result := templateStr
for key, value := range data {

View File

@@ -2,6 +2,7 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"github.com/oarkflow/mq"
@@ -27,11 +28,15 @@ var defaultKey = "default"
type Condition struct {
dag.Operation
conditions map[string]dag.Condition
conditions map[string]dag.Condition
conditionalResetTo map[string]string
}
func (e *Condition) SetConditions(conditions map[string]dag.Condition) {
func (e *Condition) SetConditions(conditions map[string]dag.Condition, conditionalResetTo ...map[string]string) {
e.conditions = conditions
if len(conditionalResetTo) > 0 {
e.conditionalResetTo = conditionalResetTo[0]
}
}
func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
@@ -51,6 +56,19 @@ func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
if conditionStatus == "" && ok {
conditionStatus = defaultKey
}
if conditionStatus != "" {
if resetTo, ok := e.conditionalResetTo[conditionStatus]; ok && resetTo != "" {
errorMessage, _ := e.Payload.Data["error_message"].(string)
if errorMessage != "" {
data["error_message"] = errorMessage
} else {
data["error_message"] = "Please try again."
}
bt, _ := json.Marshal(data)
return mq.Result{Payload: bt, ConditionStatus: conditionStatus, Ctx: ctx, ResetTo: resetTo}
}
}
return mq.Result{Payload: task.Payload, ConditionStatus: conditionStatus, Ctx: ctx}
}

View File

@@ -0,0 +1,29 @@
{
"routes": [
{
"route_uri": "/test-route",
"route_method": "POST",
"schema_file": "test-route.json",
"description": "Handle test route",
"model": "test_route",
"operation": "custom",
"handler_key": "print:check"
},
{
"route_uri": "/print",
"route_method": "GET",
"description": "Handles print",
"model": "print",
"operation": "custom",
"handler_key": "print:check"
},
{
"route_uri": "/send-email",
"route_method": "GET",
"description": "Handles send email",
"model": "print",
"operation": "custom",
"handler_key": "email:notification"
}
]
}

View File

@@ -0,0 +1,86 @@
{
"name": "Login Flow",
"key": "login:flow",
"nodes": [
{
"id": "LoginForm",
"first_node": true,
"node": "render-html",
"data": {
"additional_data": {
"schema_file": "login.json",
"template_file": "templates/basic.html"
}
}
},
{
"id": "ValidateLogin",
"node": "condition",
"data": {
"mapping": {
"username": "username",
"password": "password"
},
"additional_data": {
"error_message": "Invalid login credentials."
},
"conditions": {
"default": {
"id": "condition:default",
"node": "output"
},
"invalid": {
"id": "condition:invalid_login",
"node": "error-page",
"reset_to": "back",
"group": {
"reverse": true,
"filters": [
{
"field": "username",
"operator": "eq",
"value": "admin"
},
{
"field": "password",
"operator": "eq",
"value": "password"
}
]
}
}
}
}
},
{
"id": "error-page",
"node": "render-html",
"data": {
"mapping": {
"error_message": "eval.{{'Invalid login credentials.'}}",
"error_field": "eval.{{'username'}}",
"retry_suggested": "eval.{{true}}"
},
"additional_data": {
"template_file": "templates/error.html"
}
}
},
{
"id": "output",
"node": "output",
"data": {
"mapping": {
"login_message": "eval.{{'Login successful!'}}"
}
}
}
],
"edges": [
{
"source": "LoginForm",
"target": [ "ValidateLogin" ]
}
]
}

View File

@@ -0,0 +1,11 @@
{
"name": "Sample Print",
"key": "print:check",
"nodes": [
{
"id": "print1",
"node": "print",
"first_node": true
}
]
}

View File

@@ -0,0 +1,46 @@
{
"name": "Email Notification System",
"key": "email:notification",
"nodes": [
{
"id": "Login",
"name": "Check Login",
"node_key": "login:flow",
"first_node": true
},
{
"id": "ContactForm",
"node": "render-html",
"data": {
"additional_data": {
"schema_file": "schema.json",
"template_file": "templates/basic.html"
}
}
},
{
"id": "output",
"node": "output",
"data": {
"mapping": {
"login_message": "eval.{{'Email sent successfully!'}}"
},
"additional_data": {
"except_fields": [ "html_content" ]
}
}
}
],
"edges": [
{
"source": "Login.output",
"label": "on_success",
"target": [ "ContactForm" ]
},
{
"source": "ContactForm",
"label": "on_email_sent",
"target": [ "output" ]
}
]
}

View File

@@ -0,0 +1,63 @@
{
"type": "object",
"properties": {
"username": {
"type": "string",
"title": "Username or Email",
"order": 1,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "username",
"placeholder": "Enter your username or email"
}
},
"password": {
"type": "string",
"title": "Password",
"order": 2,
"ui": {
"element": "input",
"type": "password",
"class": "form-group",
"name": "password",
"placeholder": "Enter your password"
}
},
"remember_me": {
"type": "boolean",
"title": "Remember Me",
"order": 3,
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-check",
"name": "remember_me"
}
}
},
"required": [ "username", "password" ],
"form": {
"class": "form-horizontal",
"action": "{{current_uri}}?task_id={{task_id}}&next=true",
"method": "POST",
"enctype": "application/x-www-form-urlencoded",
"groups": [
{
"title": "Login Credentials",
"fields": [ "username", "password", "remember_me" ]
}
],
"submit": {
"type": "submit",
"label": "Log In",
"class": "btn btn-primary"
},
"reset": {
"type": "reset",
"label": "Clear",
"class": "btn btn-secondary"
}
}
}

View File

@@ -0,0 +1,105 @@
{
"type": "object",
"properties": {
"first_name": {
"type": "string",
"title": "First Name",
"order": 1,
"ui": {
"element": "input",
"class": "form-group",
"name": "first_name"
}
},
"last_name": {
"type": "string",
"title": "Last Name",
"order": 2,
"ui": {
"element": "input",
"class": "form-group",
"name": "last_name"
}
},
"email": {
"type": "email",
"title": "Email Address",
"order": 3,
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
"name": "email"
}
},
"user_type": {
"type": "string",
"title": "User Type",
"order": 4,
"ui": {
"element": "select",
"class": "form-group",
"name": "user_type",
"options": [ "new", "premium", "standard" ]
}
},
"priority": {
"type": "string",
"title": "Priority Level",
"order": 5,
"ui": {
"element": "select",
"class": "form-group",
"name": "priority",
"options": [ "low", "medium", "high", "urgent" ]
}
},
"subject": {
"type": "string",
"title": "Subject",
"order": 6,
"ui": {
"element": "input",
"class": "form-group",
"name": "subject"
}
},
"message": {
"type": "textarea",
"title": "Message",
"order": 7,
"ui": {
"element": "textarea",
"class": "form-group",
"name": "message"
}
}
},
"required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ],
"form": {
"class": "form-horizontal",
"action": "{{current_uri}}?task_id={{task_id}}&next=true",
"method": "POST",
"enctype": "application/x-www-form-urlencoded",
"groups": [
{
"title": "User Information",
"fields": [ "first_name", "last_name", "email" ]
},
{
"title": "Ticket Details",
"fields": [ "user_type", "priority", "subject", "message" ]
}
],
"submit": {
"type": "submit",
"label": "Submit",
"class": "btn btn-primary"
},
"reset": {
"type": "reset",
"label": "Reset",
"class": "btn btn-secondary"
}
}
}

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"description": "users",
"required": [ "user_id" ],
"properties": {
"last_name": {
"type": "string",
"default": "now()"
},
"user_id": {
"type": [
"integer",
"string"
],
"maxLength": 64
}
}
}

View File

@@ -0,0 +1,16 @@
{
"prefix": "/",
"middlewares": [
{"name": "cors"}
],
"static": {
"dir": "./public",
"prefix": "/",
"options": {
"byte_range": true,
"browse": true,
"compress": true,
"index_file": "index.html"
}
}
}

36
services/examples/main.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/oarkflow/cli"
"github.com/oarkflow/cli/console"
"github.com/oarkflow/cli/contracts"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/handlers"
"github.com/oarkflow/mq/services"
dagConsole "github.com/oarkflow/mq/services/console"
)
func main() {
handlers.Init()
brokerAddr := ":5051"
loader := services.NewLoader("config")
loader.Load()
serverApp := fiber.New()
services.Setup(loader, serverApp, brokerAddr)
cli.Run("mq", "0.0.1", func(client contracts.Cli) []contracts.Command {
return []contracts.Command{
console.NewListCommand(client),
dagConsole.NewRunHandler(loader.UserConfig, loader.ParsedPath, brokerAddr),
dagConsole.NewRunServer(serverApp),
}
})
}
func init() {
dag.AddHandler("render-html", func(id string) mq.Processor { return handlers.NewRenderHTMLNode(id) })
dag.AddHandler("condition", func(id string) mq.Processor { return handlers.NewCondition(id) })
dag.AddHandler("output", func(id string) mq.Processor { return handlers.NewOutputHandler(id) })
}

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>Basic Template</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="form.css">
<style>
.required {
color: #dc3545;
}
.group-header {
font-weight: bold;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.section-title {
color: #0d6efd;
border-bottom: 2px solid #0d6efd;
padding-bottom: 0.5rem;
}
.form-group-fields>div {
margin-bottom: 1rem;
}
</style>
</head>
<body class="bg-gray-100">
<form {{form_attributes}}>
<div class="form-container p-4 bg-white shadow-md rounded">
{{if error_message}}
<div class="status-message status-error">
<strong>❌ Request Failed:</strong> {{error_message}}
</div>
{{end}}
{{form_groups}}
<div class="mt-4 flex gap-2">
{{form_buttons}}
</div>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html>
<head>
<title>Email Error</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 700px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #FF6B6B 0%, #FF5722 100%);
color: white;
}
.error-container {
background: rgba(255, 255, 255, 0.1);
padding: 40px;
border-radius: 20px;
backdrop-filter: blur(15px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
text-align: center;
}
.error-icon {
font-size: 80px;
margin-bottom: 20px;
animation: shake 0.5s ease-in-out infinite alternate;
}
@keyframes shake {
0% {
transform: translateX(0);
}
100% {
transform: translateX(5px);
}
}
h1 {
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
font-size: 2.5em;
}
.error-message {
background: rgba(255, 255, 255, 0.2);
padding: 25px;
border-radius: 12px;
margin: 25px 0;
font-size: 18px;
border-left: 6px solid #FFB6B6;
line-height: 1.6;
}
.error-details {
background: rgba(255, 255, 255, 0.15);
padding: 20px;
border-radius: 12px;
margin: 25px 0;
text-align: left;
}
.actions {
margin-top: 40px;
}
.btn {
background: linear-gradient(45deg, #4ECDC4, #44A08D);
color: white;
padding: 15px 30px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin: 0 15px;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.retry-btn {
background: linear-gradient(45deg, #FFA726, #FF9800);
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon"></div>
<h1>Email Processing Error</h1>
<div class="error-message">
{{error_message}}
</div>
{{if error_field}}
<div class="error-details">
<strong>🎯 Error Field:</strong> {{error_field}}<br>
<strong>⚡ Action Required:</strong> Please correct the highlighted field and try again.<br>
<strong>💡 Tip:</strong> Make sure all required fields are properly filled out.
</div>
{{end}}
{{if retry_suggested}}
<div class="error-details">
<strong>⚠️ Temporary Issue:</strong> This appears to be a temporary system issue.
Please try sending your message again in a few moments.<br>
<strong>🔄 Auto-Retry:</strong> Our system will automatically retry failed deliveries.
</div>
{{end}}
<div class="actions">
<a href="/" class="btn retry-btn">🔄 Try Again</a>
<a href="/api/status" class="btn">📊 Check Status</a>
</div>
<div style="margin-top: 30px; font-size: 14px; opacity: 0.8;">
🔄 DAG Error Handler | Email Notification Workflow Failed<br>
Our advanced routing system ensures reliable message delivery.
</div>
</div>
</body>
</html>

View File

@@ -28,6 +28,7 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
@@ -36,6 +37,7 @@ require (
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hetiansu5/urlquery v1.2.7 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -63,11 +65,15 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.66.0 // indirect
github.com/xhit/go-simple-mail/v2 v2.16.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/sync v0.17.0 // indirect

View File

@@ -18,6 +18,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -46,6 +48,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hetiansu5/urlquery v1.2.7 h1:jn0h+9pIRqUziSPnRdK/gJK8S5TCnk+HZZx5fRHf8K0=
@@ -135,6 +139,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -145,12 +151,18 @@ github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNu
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=

View File

@@ -10,7 +10,7 @@ import (
"sort"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/cors"
@@ -24,8 +24,8 @@ import (
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/services/http/responses"
"github.com/oarkflow/mq/services/middlewares"
"github.com/oarkflow/mq/services/web"
"github.com/oarkflow/mq/services/utils"
"github.com/oarkflow/mq/services/web"
"github.com/oarkflow/protocol/utils/str"
)
@@ -48,14 +48,14 @@ func SetupEnhanced(loader *Loader, serverApp *fiber.App, brokerAddr string, conf
if loader.UserConfig == nil || serverApp == nil {
return nil
}
// Initialize enhanced services
if config != nil {
if err := InitializeEnhancedServices(config); err != nil {
return fmt.Errorf("failed to initialize enhanced services: %w", err)
}
}
// Setup both traditional and enhanced services
return SetupEnhancedServices(loader.Prefix(), serverApp, brokerAddr)
}
@@ -67,10 +67,10 @@ func InitializeEnhancedServices(config *EnhancedServiceConfig) error {
if err := EnhancedServiceManagerInstance.Initialize(config); err != nil {
return fmt.Errorf("failed to initialize enhanced service manager: %w", err)
}
// Initialize enhanced DAG service
EnhancedDAGServiceInstance = NewEnhancedDAGService(config)
// Initialize enhanced validation if config is provided
if config.ValidationConfig != nil {
validation, err := NewEnhancedValidation(config.ValidationConfig)
@@ -79,7 +79,7 @@ func InitializeEnhancedServices(config *EnhancedServiceConfig) error {
}
EnhancedValidationInstance = validation
}
return nil
}
@@ -88,13 +88,13 @@ func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string
if router == nil {
return nil
}
// Setup traditional handlers
err := SetupHandlers(userConfig.Policy.Handlers, brokerAddr)
if err != nil {
return err
}
// Setup enhanced handlers if available
if len(userConfig.Policy.EnhancedHandlers) > 0 {
err = SetupEnhancedHandlers(userConfig.Policy.EnhancedHandlers, brokerAddr)
@@ -102,11 +102,11 @@ func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string
return fmt.Errorf("failed to setup enhanced handlers: %w", err)
}
}
// Setup background handlers (both traditional and enhanced)
setupBackgroundHandlers(brokerAddr)
setupEnhancedBackgroundHandlers(brokerAddr)
// Setup static files and rendering
static := userConfig.Policy.Web.Static
if static != nil && static.Dir != "" {
@@ -121,12 +121,12 @@ func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string
},
)
}
err = setupRender(prefix, router)
if err != nil {
return fmt.Errorf("failed to setup render: %w", err)
}
// Setup API routes (both traditional and enhanced)
return SetupEnhancedAPI(prefix, router, brokerAddr)
}
@@ -140,7 +140,7 @@ func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...b
DisableLog: handler.DisableLog,
Debug: handler.Debug,
}
// Convert enhanced nodes to traditional nodes
for _, enhancedNode := range handler.Nodes {
traditionalNode := Node{
@@ -153,10 +153,10 @@ func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...b
}
traditionalHandler.Nodes = append(traditionalHandler.Nodes, traditionalNode)
}
// Copy edges and convert loops to proper type
traditionalHandler.Edges = handler.Edges
// Convert enhanced loops (Edge type) to traditional loops (Loop type)
for _, enhancedLoop := range handler.Loops {
traditionalLoop := Loop{
@@ -166,13 +166,13 @@ func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...b
}
traditionalHandler.Loops = append(traditionalHandler.Loops, traditionalLoop)
}
// Use existing SetupHandler function
dagInstance := SetupHandler(traditionalHandler, brokerAddr, async...)
if dagInstance.Error != nil {
return nil, dagInstance.Error
}
return dagInstance, nil
}
@@ -252,6 +252,7 @@ type FilterGroup struct {
type Filter struct {
Filter *filters.Filter `json:"condition"`
FilterGroup *FilterGroup `json:"group"`
ResetTo string `json:"reset_to" yaml:"reset_to"`
Node string `json:"node"`
ID string `json:"id"`
}
@@ -280,8 +281,12 @@ func prepareNode(flow *dag.DAG, node Node) error {
Providers: providers,
})
condition := make(map[string]string)
conditionalResetTo := make(map[string]string)
conditions := make(map[string]dag.Condition)
for key, cond := range node.Data.Conditions {
if cond.ResetTo != "" {
conditionalResetTo[key] = cond.ResetTo
}
condition[key] = cond.Node
if cond.Filter != nil {
conditions[key] = cond.Filter
@@ -306,7 +311,7 @@ func prepareNode(flow *dag.DAG, node Node) error {
}
}
flow.AddCondition(node.ID, condition)
nodeHandler.SetConditions(conditions)
nodeHandler.SetConditions(conditions, conditionalResetTo)
case dag.Processor:
nodeHandler.SetConfig(dag.Payload{
Mapping: node.Data.Mapping,
@@ -429,7 +434,7 @@ func SetupAPI(prefix string, router fiber.Router, brokerAddr string) error {
if flow.HasPageNode() {
mw = append(mw, flow.RenderFiber)
routeGroup.All(route.Uri, mw...)
} else {
mw = append(mw, customHandler(flow))
routeGroup.Add(strings.ToUpper(route.Method), route.Uri, mw...)
@@ -563,7 +568,7 @@ func customHandler(flow *dag.DAG) fiber.Handler {
return func(ctx *fiber.Ctx) error {
// Step 1: always parse query params
utils.ParseQueryParams(ctx)
// Step 2: build user context
userCtx := ctx.UserContext()
contentType := ctx.Get("Content-Type")
@@ -571,13 +576,13 @@ func customHandler(flow *dag.DAG) fiber.Handler {
// attach Fiber ctx so downstream can access files later
userCtx = context.WithValue(userCtx, "fiberCtx", ctx)
}
// Step 3: run DAG flow with the enriched context
result := flow.Process(userCtx, ctx.BodyRaw())
if result.Error != nil {
return result.Error
}
// Step 4: handle response content type
contentType = ""
if ct := result.Ctx.Value(consts.ContentType); ct != nil {
@@ -585,18 +590,18 @@ func customHandler(flow *dag.DAG) fiber.Handler {
contentType = s
}
}
if contentType == "" ||
contentType == fiber.MIMEApplicationJSON ||
contentType == fiber.MIMEApplicationJSONCharsetUTF8 {
return responses.Success(ctx, 200, result.Payload)
}
var resultData map[string]any
if err := json.Unmarshal(result.Payload, &resultData); err != nil {
return ctx.JSON(fiber.Map{"success": false, "error": "Invalid response payload"})
}
ctx.Set(consts.ContentType, contentType)
html, _ := resultData["html_content"].(string)
return ctx.SendString(html)
@@ -722,7 +727,7 @@ func setupMiddlewares(middlewares ...Middleware) (mid []any) {
if err != nil {
panic(err)
}
expiration, err := utils.ParseDuration(options.Expiration)
if err != nil {
panic(err)
@@ -878,7 +883,7 @@ func setupEnhancedBackgroundHandlers(brokerAddress string) {
log.Error().Err(err).Msgf("Failed to setup enhanced background handler: %s", handler.Key)
continue
}
// Start background processing using traditional DAG
go func(dag *dag.DAG, key string) {
ctx := context.Background()
@@ -896,7 +901,7 @@ func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) err
prefix = "/" + prefix
}
api := router.Group(prefix)
// Setup traditional API routes
for _, configRoute := range userConfig.Policy.Web.Apis {
routeGroup := api.Group(configRoute.Prefix)
@@ -930,7 +935,7 @@ func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) err
}
}
}
// Setup enhanced API routes for enhanced handlers
for _, handler := range userConfig.Policy.EnhancedHandlers {
if handler.WorkflowEnabled {
@@ -938,18 +943,18 @@ func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) err
if err != nil {
return fmt.Errorf("failed to setup enhanced handler for API: %w", err)
}
// Create API endpoint for enhanced handler (using traditional DAG handler)
path := fmt.Sprintf("/enhanced/%s", handler.Key)
api.Post(path, customHandler(dagInstance))
// Create DAG visualization endpoint (using traditional DAG visualization)
api.Get(path+"/dag", func(ctx *fiber.Ctx) error {
return getDAGPage(ctx, dagInstance)
})
}
}
return nil
}