refactor: move examples

This commit is contained in:
sujit
2025-08-11 11:15:13 +05:45
parent bb57692cbb
commit 26460c383b
78 changed files with 12 additions and 14198 deletions

View File

@@ -1,161 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "Company Registration",
"properties": {
"session_id": {
"type": "string",
"title": "Session ID",
"ui": {
"element": "input",
"type": "hidden",
"name": "session_id",
"value": "{{session_id}}"
}
},
"task_id": {
"type": "string",
"title": "Task ID",
"ui": {
"element": "input",
"type": "hidden",
"name": "task_id",
"value": "{{task_id}}"
}
},
"company": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Company Name",
"ui": {
"element": "input",
"type": "text",
"name": "name",
"class": "form-group",
"order": 1
}
},
"address": {
"type": "object",
"properties": {
"street": {
"type": "string",
"title": "Street Address",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 2
}
},
"city": {
"type": "string",
"title": "City",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 3
}
},
"country": {
"type": "string",
"title": "Country",
"enum": [ "US", "CA", "UK", "DE", "FR" ],
"ui": {
"element": "select",
"class": "form-group",
"order": 4,
"options": [
{ "value": "US", "text": "United States" },
{ "value": "CA", "text": "Canada" },
{ "value": "UK", "text": "United Kingdom" },
{ "value": "DE", "text": "Germany" },
{ "value": "FR", "text": "France" }
]
}
},
"zipCode": {
"type": "string",
"title": "ZIP/Postal Code",
"pattern": "^[0-9]{5}(-[0-9]{4})?$",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 5
}
}
},
"required": [ "street", "city", "country" ]
},
"contact": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Contact Email",
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
"order": 6
}
},
"phone": {
"type": "string",
"title": "Phone Number",
"ui": {
"element": "input",
"type": "tel",
"class": "form-group",
"order": 7
}
}
},
"required": [ "email" ]
}
},
"required": [ "name", "address", "contact" ]
}
},
"required": [ "company" ],
"form": {
"class": "company-registration-form",
"action": "/process",
"method": "POST",
"groups": [
{
"title": {
"text": "Company Information",
"class": "group-header"
},
"class": "company-info",
"fields": [ "company.name" ]
},
{
"title": {
"text": "Address Details",
"class": "group-header"
},
"class": "flex items-start gap-4 address-info",
"fields": [ "company.address.street", "company.address.city", "company.address.country", "company.address.zipCode" ]
},
{
"title": {
"text": "Contact Information",
"class": "group-header"
},
"class": "flex items-start gap-4 contact-info",
"fields": [ "company.contact.email", "company.contact.phone" ]
}
],
"submit": {
"label": "Register Company",
"class": "btn btn-success btn-lg px-4 py-2"
}
}
}

View File

@@ -1,681 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "Comprehensive Validation Demo",
"description": "A complete example showcasing all JSON Schema 2020-12 validation features",
"properties": {
"personalInfo": {
"type": "object",
"title": "Personal Information",
"properties": {
"firstName": {
"type": "string",
"title": "First Name",
"minLength": 2,
"maxLength": 50,
"pattern": "^[A-Za-z\\s]+$",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 1,
"placeholder": "Enter your first name"
}
},
"lastName": {
"type": "string",
"title": "Last Name",
"minLength": 2,
"maxLength": 50,
"pattern": "^[A-Za-z\\s]+$",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 2
}
},
"age": {
"type": "integer",
"title": "Age",
"minimum": 18,
"maximum": 120,
"ui": {
"element": "input",
"type": "number",
"class": "form-group",
"order": 3
}
},
"email": {
"type": "string",
"format": "email",
"title": "Email Address",
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
"order": 4
}
},
"phone": {
"type": "string",
"title": "Phone Number",
"pattern": "^\\+?[1-9]\\d{1,14}$",
"ui": {
"element": "input",
"type": "tel",
"class": "form-group",
"order": 5
}
},
"dateOfBirth": {
"type": "string",
"format": "date",
"title": "Date of Birth",
"ui": {
"element": "input",
"type": "date",
"class": "form-group",
"order": 6
}
},
"website": {
"type": "string",
"format": "uri",
"title": "Personal Website",
"ui": {
"element": "input",
"type": "url",
"class": "form-group",
"order": 7
}
}
},
"required": [ "firstName", "lastName", "email" ],
"dependentRequired": {
"phone": [ "email" ],
"website": [ "email" ]
}
},
"address": {
"type": "object",
"title": "Address Information",
"properties": {
"street": {
"type": "string",
"title": "Street Address",
"minLength": 5,
"maxLength": 100,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 1
}
},
"city": {
"type": "string",
"title": "City",
"minLength": 2,
"maxLength": 50,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 2
}
},
"state": {
"type": "string",
"title": "State/Province",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 3
}
},
"country": {
"type": "string",
"title": "Country",
"enum": [ "US", "CA", "UK", "DE", "FR", "AU", "JP", "IN" ],
"ui": {
"element": "select",
"class": "form-group",
"order": 4,
"options": [
{ "value": "US", "text": "United States" },
{ "value": "CA", "text": "Canada" },
{ "value": "UK", "text": "United Kingdom" },
{ "value": "DE", "text": "Germany" },
{ "value": "FR", "text": "France" },
{ "value": "AU", "text": "Australia" },
{ "value": "JP", "text": "Japan" },
{ "value": "IN", "text": "India" }
]
}
},
"postalCode": {
"type": "string",
"title": "Postal Code",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 5
}
}
},
"required": [ "street", "city", "country" ],
"allOf": [
{
"if": {
"properties": {
"country": { "const": "US" }
}
},
"then": {
"properties": {
"postalCode": {
"pattern": "^\\d{5}(-\\d{4})?$"
}
}
}
},
{
"if": {
"properties": {
"country": { "const": "CA" }
}
},
"then": {
"properties": {
"postalCode": {
"pattern": "^[A-Z]\\d[A-Z] \\d[A-Z]\\d$"
}
}
}
}
]
},
"preferences": {
"type": "object",
"title": "User Preferences",
"properties": {
"newsletter": {
"type": "boolean",
"title": "Subscribe to Newsletter",
"default": false,
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group",
"order": 1
}
},
"notifications": {
"type": "array",
"title": "Notification Types",
"minItems": 1,
"maxItems": 5,
"uniqueItems": true,
"items": {
"type": "string",
"enum": [ "email", "sms", "push", "phone", "mail" ]
},
"ui": {
"element": "select",
"class": "form-group",
"order": 2,
"multiple": true,
"options": [
{ "value": "email", "text": "Email Notifications" },
{ "value": "sms", "text": "SMS Notifications" },
{ "value": "push", "text": "Push Notifications" },
{ "value": "phone", "text": "Phone Calls" },
{ "value": "mail", "text": "Physical Mail" }
]
}
},
"theme": {
"type": "string",
"title": "Preferred Theme",
"enum": [ "light", "dark", "auto" ],
"default": "auto",
"ui": {
"element": "select",
"class": "form-group",
"order": 3,
"options": [
{ "value": "light", "text": "Light Theme" },
{ "value": "dark", "text": "Dark Theme" },
{ "value": "auto", "text": "Auto (System)" }
]
}
},
"language": {
"type": "string",
"title": "Preferred Language",
"enum": [ "en", "es", "fr", "de", "zh", "ja" ],
"default": "en",
"ui": {
"element": "select",
"class": "form-group",
"order": 4,
"options": [
{ "value": "en", "text": "English" },
{ "value": "es", "text": "Español" },
{ "value": "fr", "text": "Français" },
{ "value": "de", "text": "Deutsch" },
{ "value": "zh", "text": "中文" },
{ "value": "ja", "text": "日本語" }
]
}
}
}
},
"financial": {
"type": "object",
"title": "Financial Information",
"properties": {
"annualIncome": {
"type": "number",
"title": "Annual Income",
"minimum": 0,
"maximum": 10000000,
"multipleOf": 1000,
"ui": {
"element": "input",
"type": "number",
"class": "form-group",
"order": 1,
"step": "1000"
}
},
"creditScore": {
"type": "integer",
"title": "Credit Score",
"minimum": 300,
"maximum": 850,
"ui": {
"element": "input",
"type": "range",
"class": "form-group",
"order": 2,
"min": "300",
"max": "850"
}
},
"accountType": {
"type": "string",
"title": "Account Type",
"oneOf": [
{
"const": "basic",
"title": "Basic Account",
"description": "Free account with basic features"
},
{
"const": "premium",
"title": "Premium Account",
"description": "Paid account with advanced features"
},
{
"const": "enterprise",
"title": "Enterprise Account",
"description": "Business account with full features"
}
],
"ui": {
"element": "select",
"class": "form-group",
"order": 3,
"options": [
{ "value": "basic", "text": "Basic Account" },
{ "value": "premium", "text": "Premium Account" },
{ "value": "enterprise", "text": "Enterprise Account" }
]
}
}
},
"if": {
"properties": {
"accountType": { "const": "premium" }
}
},
"then": {
"properties": {
"paymentMethod": {
"type": "string",
"title": "Payment Method",
"enum": [ "credit_card", "debit_card", "paypal", "bank_transfer" ],
"ui": {
"element": "select",
"class": "form-group",
"order": 4,
"options": [
{ "value": "credit_card", "text": "Credit Card" },
{ "value": "debit_card", "text": "Debit Card" },
{ "value": "paypal", "text": "PayPal" },
{ "value": "bank_transfer", "text": "Bank Transfer" }
]
}
}
},
"required": [ "paymentMethod" ]
},
"else": {
"not": {
"required": [ "paymentMethod" ]
}
}
},
"skills": {
"type": "array",
"title": "Technical Skills",
"minItems": 1,
"maxItems": 10,
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Skill Name",
"minLength": 2,
"maxLength": 50
},
"level": {
"type": "string",
"title": "Proficiency Level",
"enum": [ "beginner", "intermediate", "advanced", "expert" ]
},
"yearsExperience": {
"type": "number",
"title": "Years of Experience",
"minimum": 0,
"maximum": 50,
"multipleOf": 0.5
}
},
"required": [ "name", "level" ]
},
"ui": {
"element": "fieldset",
"class": "skills-section",
"order": 1
}
},
"portfolio": {
"type": "object",
"title": "Portfolio",
"properties": {
"projects": {
"type": "array",
"title": "Projects",
"maxItems": 5,
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Project Title",
"minLength": 3,
"maxLength": 100
},
"description": {
"type": "string",
"title": "Description",
"maxLength": 500,
"ui": {
"element": "textarea",
"rows": 4,
"class": "form-group"
}
},
"url": {
"type": "string",
"format": "uri",
"title": "Project URL"
},
"technologies": {
"type": "array",
"title": "Technologies Used",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}
},
"required": [ "title", "description" ]
}
}
},
"anyOf": [
{
"properties": {
"githubUrl": {
"type": "string",
"format": "uri",
"pattern": "^https://github\\.com/",
"title": "GitHub Profile"
}
}
},
{
"properties": {
"linkedinUrl": {
"type": "string",
"format": "uri",
"pattern": "^https://www\\.linkedin\\.com/",
"title": "LinkedIn Profile"
}
}
},
{
"properties": {
"personalWebsite": {
"type": "string",
"format": "uri",
"title": "Personal Website"
}
}
}
]
},
"comments": {
"type": "string",
"title": "Additional Comments",
"maxLength": 1000,
"ui": {
"element": "textarea",
"class": "form-group",
"rows": 5,
"placeholder": "Any additional information you'd like to share..."
}
},
"termsAccepted": {
"type": "boolean",
"title": "I accept the terms and conditions",
"const": true,
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group required-checkbox"
}
},
"privacyAccepted": {
"type": "boolean",
"title": "I accept the privacy policy",
"const": true,
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group required-checkbox"
}
}
},
"required": [ "personalInfo", "address", "termsAccepted", "privacyAccepted" ],
"dependentSchemas": {
"financial": {
"properties": {
"personalInfo": {
"properties": {
"age": {
"minimum": 21
}
}
}
}
}
},
"form": {
"class": "comprehensive-form",
"action": "/api/submit-comprehensive-form",
"method": "POST",
"enctype": "multipart/form-data",
"groups": [
{
"title": {
"text": "Personal Information",
"class": "section-header"
},
"class": "personal-info-section",
"fields": [
"personalInfo.firstName",
"personalInfo.lastName",
"personalInfo.age",
"personalInfo.email",
"personalInfo.phone",
"personalInfo.dateOfBirth",
"personalInfo.website"
]
},
{
"title": {
"text": "Address Information",
"class": "section-header"
},
"class": "address-section flex flex-wrap gap-4",
"fields": [
"address.street",
"address.city",
"address.state",
"address.country",
"address.postalCode"
]
},
{
"title": {
"text": "Preferences & Settings",
"class": "section-header"
},
"class": "preferences-section",
"fields": [
"preferences.newsletter",
"preferences.notifications",
"preferences.theme",
"preferences.language"
]
},
{
"title": {
"text": "Financial Information",
"class": "section-header"
},
"class": "financial-section",
"fields": [
"financial.annualIncome",
"financial.creditScore",
"financial.accountType"
]
},
{
"title": {
"text": "Skills & Portfolio",
"class": "section-header"
},
"class": "skills-section",
"fields": [
"skills",
"portfolio"
]
},
{
"title": {
"text": "Final Details",
"class": "section-header"
},
"class": "final-section",
"fields": [
"comments",
"termsAccepted",
"privacyAccepted"
]
}
],
"submit": {
"label": "Submit Application",
"class": "btn btn-primary btn-lg",
"id": "submit-btn"
},
"reset": {
"label": "Reset Form",
"class": "btn btn-secondary",
"id": "reset-btn"
},
"buttons": [
{
"type": "button",
"label": "Save Draft",
"class": "btn btn-outline-secondary",
"id": "save-draft-btn",
"onclick": "saveDraft()"
},
{
"type": "button",
"label": "Preview",
"class": "btn btn-outline-primary",
"id": "preview-btn",
"onclick": "showPreview()"
}
]
},
"examples": [
{
"personalInfo": {
"firstName": "John",
"lastName": "Doe",
"age": 30,
"email": "john.doe@example.com",
"phone": "+1-555-123-4567",
"dateOfBirth": "1993-05-15",
"website": "https://johndoe.com"
},
"address": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"country": "US",
"postalCode": "10001"
},
"preferences": {
"newsletter": true,
"notifications": [ "email", "push" ],
"theme": "dark",
"language": "en"
},
"financial": {
"annualIncome": 75000,
"creditScore": 750,
"accountType": "premium",
"paymentMethod": "credit_card"
},
"termsAccepted": true,
"privacyAccepted": true
}
]
}

View File

@@ -1,528 +0,0 @@
{
"type": "object",
"properties": {
"page_title": {
"type": "string",
"title": "Main Page Title",
"order": 1,
"ui": {
"element": "h1",
"class": "text-3xl font-bold text-center mb-6",
"name": "page_title",
"content": "Comprehensive HTML Form Demo"
}
},
"intro_paragraph": {
"type": "string",
"title": "Introduction",
"order": 2,
"ui": {
"element": "p",
"class": "text-gray-600 mb-4",
"name": "intro",
"content": "This form demonstrates all supported HTML DOM elements in the renderer."
}
},
"divider": {
"type": "string",
"order": 3,
"ui": {
"element": "hr",
"class": "my-6 border-gray-300"
}
},
"first_name": {
"type": "string",
"title": "First Name",
"placeholder": "Enter your first name",
"order": 10,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "first_name",
"autocomplete": "given-name",
"maxlength": "50"
}
},
"last_name": {
"type": "string",
"title": "Last Name",
"placeholder": "Enter your last name",
"order": 11,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "last_name",
"autocomplete": "family-name",
"maxlength": "50"
}
},
"email": {
"type": "email",
"title": "Email Address",
"placeholder": "your.email@example.com",
"order": 12,
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
"name": "email",
"autocomplete": "email"
}
},
"password": {
"type": "password",
"title": "Password",
"placeholder": "Enter a secure password",
"order": 13,
"ui": {
"element": "input",
"type": "password",
"class": "form-group",
"name": "password",
"minlength": "8"
}
},
"age": {
"type": "number",
"title": "Age",
"order": 14,
"ui": {
"element": "input",
"type": "number",
"class": "form-group",
"name": "age",
"min": "18",
"max": "120"
}
},
"birth_date": {
"type": "date",
"title": "Birth Date",
"order": 15,
"ui": {
"element": "input",
"type": "date",
"class": "form-group",
"name": "birth_date"
}
},
"website": {
"type": "url",
"title": "Personal Website",
"placeholder": "https://example.com",
"order": 16,
"ui": {
"element": "input",
"type": "url",
"class": "form-group",
"name": "website"
}
},
"phone": {
"type": "tel",
"title": "Phone Number",
"placeholder": "+1-555-123-4567",
"order": 17,
"ui": {
"element": "input",
"type": "tel",
"class": "form-group",
"name": "phone"
}
},
"favorite_color": {
"type": "color",
"title": "Favorite Color",
"order": 18,
"ui": {
"element": "input",
"type": "color",
"class": "form-group",
"name": "favorite_color",
"value": "#3B82F6"
}
},
"satisfaction": {
"type": "range",
"title": "Satisfaction Level",
"order": 19,
"ui": {
"element": "input",
"type": "range",
"class": "form-group",
"name": "satisfaction",
"min": "1",
"max": "10",
"value": "5"
}
},
"profile_picture": {
"type": "file",
"title": "Profile Picture",
"order": 20,
"ui": {
"element": "input",
"type": "file",
"class": "form-group",
"name": "profile_picture",
"accept": "image/*"
}
},
"newsletter": {
"type": "boolean",
"title": "Subscribe to Newsletter",
"order": 21,
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group",
"name": "newsletter",
"value": "yes"
}
},
"gender": {
"type": "string",
"title": "Gender",
"order": 22,
"ui": {
"element": "select",
"class": "form-group",
"name": "gender",
"options": [
{ "value": "", "text": "Select Gender", "selected": true },
{ "value": "male", "text": "Male" },
{ "value": "female", "text": "Female" },
{ "value": "other", "text": "Other" },
{ "value": "prefer_not_to_say", "text": "Prefer not to say" }
]
}
},
"country": {
"type": "string",
"title": "Country",
"order": 23,
"ui": {
"element": "select",
"class": "form-group",
"name": "country",
"options": [
{ "value": "us", "text": "United States" },
{ "value": "ca", "text": "Canada" },
{ "value": "uk", "text": "United Kingdom" },
{ "value": "de", "text": "Germany" },
{ "value": "fr", "text": "France" },
{ "value": "jp", "text": "Japan" },
{ "value": "au", "text": "Australia" }
]
}
},
"skills": {
"type": "string",
"title": "Skills",
"order": 24,
"ui": {
"element": "select",
"class": "form-group",
"name": "skills",
"multiple": true,
"options": [
{ "value": "javascript", "text": "JavaScript" },
{ "value": "python", "text": "Python" },
{ "value": "go", "text": "Go" },
{ "value": "rust", "text": "Rust" },
{ "value": "java", "text": "Java" },
{ "value": "csharp", "text": "C#" }
]
}
},
"bio": {
"type": "string",
"title": "Biography",
"placeholder": "Tell us about yourself...",
"order": 25,
"ui": {
"element": "textarea",
"class": "form-group",
"name": "bio",
"rows": "4",
"cols": "50",
"maxlength": "500"
}
},
"section_header": {
"type": "string",
"order": 30,
"ui": {
"element": "h2",
"class": "text-2xl font-semibold mt-8 mb-4",
"content": "Additional Information"
}
},
"experience_fieldset": {
"type": "object",
"order": 31,
"ui": {
"element": "fieldset",
"class": "border border-gray-300 rounded p-4 mb-4",
"children": [
{
"ui": {
"element": "legend",
"class": "font-medium px-2",
"content": "Work Experience"
}
},
{
"title": "Years of Experience",
"ui": {
"element": "input",
"type": "number",
"name": "years_experience",
"class": "form-group",
"min": "0",
"max": "50"
}
},
{
"title": "Current Position",
"ui": {
"element": "input",
"type": "text",
"name": "current_position",
"class": "form-group",
"placeholder": "e.g., Software Engineer"
}
}
]
}
},
"technologies_datalist": {
"type": "string",
"title": "Preferred Technology",
"order": 32,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"name": "preferred_tech",
"list": "technologies",
"placeholder": "Start typing..."
}
},
"tech_datalist": {
"type": "string",
"order": 33,
"ui": {
"element": "datalist",
"id": "technologies",
"options": [
"React", "Vue.js", "Angular", "Node.js", "Express",
"Django", "Flask", "Spring Boot", "ASP.NET", "Laravel"
]
}
},
"completion_progress": {
"type": "string",
"order": 34,
"ui": {
"element": "div",
"class": "mb-4",
"contentHTML": "<label for='form_progress'>Form Completion:</label><progress id='form_progress' value='70' max='100' class='w-full'>70%</progress>"
}
},
"rating_meter": {
"type": "string",
"order": 35,
"ui": {
"element": "div",
"class": "mb-4",
"contentHTML": "<label for='rating'>Overall Rating:</label><meter id='rating' value='8' min='0' max='10' optimum='9' class='w-full'>8 out of 10</meter>"
}
},
"media_section": {
"type": "string",
"order": 40,
"ui": {
"element": "section",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Media Examples</h3>"
}
},
"demo_image": {
"type": "string",
"order": 41,
"ui": {
"element": "figure",
"class": "mb-4",
"contentHTML": "<img src='https://placehold.co/300x200/3B82F6/ffffff?text=Demo+Image' alt='Demo placeholder image' class='rounded border' /><figcaption class='text-sm text-gray-600 mt-2'>Sample image with caption</figcaption>"
}
},
"table_section": {
"type": "string",
"order": 50,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Data Table Example</h3><table class='min-w-full border border-gray-300'><thead class='bg-gray-50'><tr><th class='border border-gray-300 px-4 py-2'>Name</th><th class='border border-gray-300 px-4 py-2'>Role</th><th class='border border-gray-300 px-4 py-2'>Experience</th></tr></thead><tbody><tr><td class='border border-gray-300 px-4 py-2'>John Doe</td><td class='border border-gray-300 px-4 py-2'>Developer</td><td class='border border-gray-300 px-4 py-2'>5 years</td></tr><tr><td class='border border-gray-300 px-4 py-2'>Jane Smith</td><td class='border border-gray-300 px-4 py-2'>Designer</td><td class='border border-gray-300 px-4 py-2'>3 years</td></tr></tbody></table>"
}
},
"list_examples": {
"type": "string",
"order": 60,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>List Examples</h3><div class='grid grid-cols-1 md:grid-cols-3 gap-4'><div><h4 class='font-medium mb-2'>Unordered List:</h4><ul class='list-disc list-inside space-y-1'><li>First item</li><li>Second item</li><li>Third item</li></ul></div><div><h4 class='font-medium mb-2'>Ordered List:</h4><ol class='list-decimal list-inside space-y-1'><li>Step one</li><li>Step two</li><li>Step three</li></ol></div><div><h4 class='font-medium mb-2'>Description List:</h4><dl class='space-y-2'><dt class='font-medium'>Term 1:</dt><dd class='ml-4 text-gray-600'>Definition 1</dd><dt class='font-medium'>Term 2:</dt><dd class='ml-4 text-gray-600'>Definition 2</dd></dl></div></div>"
}
},
"interactive_details": {
"type": "string",
"order": 70,
"ui": {
"element": "details",
"class": "border border-gray-300 rounded p-4 mb-4",
"contentHTML": "<summary class='font-medium cursor-pointer'>Click to expand advanced options</summary><div class='mt-4 space-y-4'><div class='form-group'><label for='advanced_setting_1'>Advanced Setting 1:</label><input type='text' id='advanced_setting_1' name='advanced_setting_1' class='w-full border rounded px-3 py-2' /></div><div class='form-group'><label for='advanced_setting_2'>Advanced Setting 2:</label><select id='advanced_setting_2' name='advanced_setting_2' class='w-full border rounded px-3 py-2'><option value='option1'>Option 1</option><option value='option2'>Option 2</option></select></div></div>"
}
},
"code_example": {
"type": "string",
"order": 80,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Code Example</h3><pre class='bg-gray-100 p-4 rounded overflow-x-auto'><code class='text-sm'>function greetUser(name) {\n return `Hello, ${name}!`;\n}\n\nconsole.log(greetUser('World'));</code></pre>"
}
},
"text_formatting": {
"type": "string",
"order": 90,
"ui": {
"element": "div",
"class": "mt-8 mb-4",
"contentHTML": "<h3 class='text-xl font-medium mb-4'>Text Formatting Examples</h3><p class='mb-4'>This paragraph contains various text formatting: <strong>bold text</strong>, <em>italic text</em>, <mark>highlighted text</mark>, <small>small text</small>, <del>deleted text</del>, <ins>inserted text</ins>, <sup>superscript</sup>, <sub>subscript</sub>, and <abbr title='HyperText Markup Language'>HTML</abbr> abbreviation.</p><blockquote class='border-l-4 border-blue-500 pl-4 italic text-gray-600 mb-4'>This is a blockquote that can contain longer quoted text with proper styling and indentation.</blockquote><address class='not-italic text-gray-600'>Contact: <a href='mailto:demo@example.com' class='text-blue-600 hover:underline'>demo@example.com</a></address>"
}
},
"break_line": {
"type": "string",
"order": 95,
"ui": {
"element": "br"
}
},
"final_divider": {
"type": "string",
"order": 96,
"ui": {
"element": "hr",
"class": "my-8 border-gray-300"
}
},
"footer_note": {
"type": "string",
"order": 97,
"ui": {
"element": "footer",
"class": "text-center text-gray-500 text-sm",
"contentHTML": "<p>This comprehensive form demonstrates the full capabilities of the enhanced JSON Schema renderer.</p>"
}
}
},
"required": [
"first_name", "last_name", "email", "age", "birth_date", "gender", "bio"
],
"form": {
"class": "max-w-4xl mx-auto p-6 bg-white shadow-lg rounded-lg",
"action": "/process?task_id={{task_id}}&form=comprehensive",
"method": "POST",
"enctype": "multipart/form-data",
"groups": [
{
"title": {
"text": "Header Section",
"class": "sr-only"
},
"fields": [ "page_title", "intro_paragraph", "divider" ],
"class": "mb-8"
},
{
"title": {
"text": "Personal Information",
"class": "text-2xl font-semibold mb-6 text-gray-800"
},
"fields": [
"first_name", "last_name", "email", "password", "age",
"birth_date", "website", "phone", "favorite_color",
"satisfaction", "profile_picture", "newsletter"
],
"class": "grid grid-cols-1 md:grid-cols-2 gap-4 mb-8"
},
{
"title": {
"text": "Preferences & Background",
"class": "text-2xl font-semibold mb-6 text-gray-800"
},
"fields": [ "gender", "country", "skills", "bio" ],
"class": "space-y-4 mb-8"
},
{
"title": {
"text": "Professional Details",
"class": "sr-only"
},
"fields": [
"section_header", "experience_fieldset", "technologies_datalist",
"tech_datalist", "completion_progress", "rating_meter"
],
"class": "mb-8"
},
{
"title": {
"text": "Media & Content Examples",
"class": "sr-only"
},
"fields": [ "media_section", "demo_image" ],
"class": "mb-8"
},
{
"title": {
"text": "Data & Text Examples",
"class": "sr-only"
},
"fields": [
"table_section", "list_examples", "interactive_details",
"code_example", "text_formatting"
],
"class": "mb-8"
},
{
"title": {
"text": "Footer",
"class": "sr-only"
},
"fields": [ "break_line", "final_divider", "footer_note" ],
"class": "mt-8"
}
],
"submit": {
"type": "submit",
"label": "Submit Complete Form",
"class": "bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4"
},
"reset": {
"type": "reset",
"label": "Reset All Fields",
"class": "bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4"
},
"buttons": [
{
"type": "button",
"label": "Save Draft",
"class": "bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200",
"onclick": "saveDraft()"
}
]
}
}

View File

@@ -1,83 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/oarkflow/mq/renderer"
"github.com/oarkflow/form"
)
func main() {
http.Handle("/form.css", http.FileServer(http.Dir("templates")))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Serve the main form page
http.ServeFile(w, r, "templates/form.html")
})
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest)
return
}
data, err := form.DecodeForm(body)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to decode form: %v", err), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(data)
})
http.HandleFunc("/render", func(w http.ResponseWriter, r *http.Request) {
templateName := r.URL.Query().Get("template")
if templateName == "" {
templateName = "basic"
}
schemaHTML := r.URL.Query().Get("schema")
if schemaHTML == "" {
http.Error(w, "Schema parameter is required", http.StatusBadRequest)
return
}
if !strings.Contains(schemaHTML, ".json") {
schemaHTML = fmt.Sprintf("%s.json", schemaHTML)
}
renderer, err := renderer.GetFromFile(schemaHTML, templateName)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get cached template: %v", err), http.StatusInternalServerError)
return
}
// Set template data for dynamic interpolation
templateData := map[string]interface{}{
"task_id": r.URL.Query().Get("task_id"), // Get task_id from query params
"session_id": "test_session_123", // Example session_id
}
// If task_id is not provided, use a default value
if templateData["task_id"] == "" {
templateData["task_id"] = "default_task_123"
}
renderedHTML, err := renderer.RenderFields(templateData)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to render fields: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(renderedHTML))
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
fmt.Printf("Server running on port http://localhost:%s\n", port)
http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
}

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Advanced Template</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="form.css">
<style>
.form-container {
padding: 20px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
</style>
</head>
<body class="bg-gray-200">
<form {{form_attributes}}>
<div class="form-container rounded-lg">
<h1 class="text-xl font-bold mb-4">Form Fields</h1>
{{form_groups}}
<div class="mt-4">
{{form_buttons}}
</div>
</div>
</form>
</body>
</html>

View File

@@ -1,42 +0,0 @@
<!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">
{{form_groups}}
<div class="mt-4 flex gap-2">
{{form_buttons}}
</div>
</div>
</form>
</body>
</html>

View File

@@ -1,780 +0,0 @@
/* CSS Reset and Base Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
line-height: 1.6;
scroll-behavior: smooth;
}
body {
font-family: 'Arial', 'Helvetica', sans-serif;
background-color: #f8f9fa;
color: #333;
margin: 0;
padding: 0;
}
/* Typography and Text Elements */
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
line-height: 1.2;
margin-bottom: 0.5rem;
color: #2c3e50;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }
h4 { font-size: 1.5rem; }
h5 { font-size: 1.25rem; }
h6 { font-size: 1rem; }
p {
margin-bottom: 1rem;
line-height: 1.6;
}
/* Inline text elements */
strong, b {
font-weight: bold;
}
em, i {
font-style: italic;
}
small {
font-size: 0.875rem;
color: #666;
}
mark {
background-color: #fff3cd;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
}
del {
text-decoration: line-through;
color: #dc3545;
}
ins {
text-decoration: underline;
color: #28a745;
background-color: #d4edda;
}
sub, sup {
font-size: 0.75rem;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub { bottom: -0.25rem; }
sup { top: -0.5rem; }
abbr {
text-decoration: underline dotted;
cursor: help;
}
code {
font-family: 'Courier New', monospace;
background-color: #f8f9fa;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
font-size: 0.875rem;
color: #e83e8c;
}
pre {
font-family: 'Courier New', monospace;
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
margin-bottom: 1rem;
border: 1px solid #dee2e6;
}
pre code {
background: none;
padding: 0;
font-size: inherit;
color: inherit;
}
blockquote {
margin: 1rem 0;
padding: 1rem;
border-left: 4px solid #007bff;
background-color: #f8f9fa;
font-style: italic;
}
cite {
font-style: italic;
color: #666;
}
address {
font-style: normal;
margin-bottom: 1rem;
}
time {
color: #666;
}
/* Links */
a {
color: #007bff;
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
a:visited {
color: #6f42c1;
}
/* Lists */
ul, ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
li {
margin-bottom: 0.25rem;
}
ul ul, ol ol, ul ol, ol ul {
margin-bottom: 0;
margin-top: 0.25rem;
}
/* Nested list styles */
ul ul { list-style-type: circle; }
ul ul ul { list-style-type: square; }
ol ol { list-style-type: lower-alpha; }
ol ol ol { list-style-type: lower-roman; }
/* Description lists */
dl {
margin-bottom: 1rem;
}
dt {
font-weight: bold;
margin-top: 0.5rem;
}
dd {
margin-left: 1rem;
margin-bottom: 0.5rem;
}
/* Form Elements */
.form-group {
margin-bottom: 1rem;
width: 100%;
}
.required {
color: #dc3545;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
color: #333;
}
/* Input elements */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="url"],
input[type="search"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"],
input[type="color"],
input[type="file"],
select,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
font-size: 1rem;
font-family: inherit;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/* Ensure form-group input styles apply to all types including password */
.form-group input[type="password"], .form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
font-size: 1rem;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
input:focus,
select:focus,
textarea:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
/* Specific focus styles for form-group inputs */
.form-group input[type="password"]:focus,
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="number"]:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
input:invalid {
border-color: #dc3545;
}
input:invalid:focus {
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
}
/* Range input */
input[type="range"] {
width: 100%;
height: 0.5rem;
background: #ddd;
border-radius: 0.25rem;
outline: none;
padding: 0;
box-shadow: none;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 1.5rem;
height: 1.5rem;
background: #007bff;
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 1.5rem;
height: 1.5rem;
background: #007bff;
border-radius: 50%;
cursor: pointer;
border: none;
}
/* Checkbox and radio */
input[type="checkbox"],
input[type="radio"] {
width: auto;
margin-right: 0.5rem;
padding: 0;
box-shadow: none;
}
input[type="checkbox"] {
border-radius: 0.125rem;
}
input[type="radio"] {
border-radius: 50%;
}
/* Select styling */
select {
appearance: none;
background: url('data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns%3D%22http%3A//www.w3.org/2000/svg%22 viewBox%3D%220 0 4 5%22%3E%3Cpath fill%3D%22%23000%22 d%3D%22M2 0L0 2h4z%22/%3E%3C/svg%3E') no-repeat right 0.75rem center;
background-size: 0.5rem;
padding-right: 2.5rem;
}
select[multiple] {
background-image: none;
padding-right: 0.75rem;
height: auto;
min-height: 6rem;
}
optgroup {
font-weight: bold;
color: #666;
}
option {
padding: 0.25rem;
}
/* Textarea */
textarea {
resize: vertical;
min-height: 6rem;
}
/* Fieldset and Legend */
fieldset {
border: 1px solid #ccc;
border-radius: 0.25rem;
padding: 1rem;
margin-bottom: 1rem;
}
legend {
font-weight: bold;
padding: 0 0.5rem;
color: #333;
}
/* Progress and Meter */
progress, meter {
width: 100%;
height: 1.5rem;
appearance: none;
border: none;
border-radius: 0.25rem;
background-color: #e9ecef;
}
progress::-webkit-progress-bar,
meter::-webkit-meter-bar {
background-color: #e9ecef;
border-radius: 0.25rem;
}
progress::-webkit-progress-value {
background-color: #007bff;
border-radius: 0.25rem;
}
meter::-webkit-meter-optimum-value {
background-color: #28a745;
}
meter::-webkit-meter-suboptimum-value {
background-color: #ffc107;
}
meter::-webkit-meter-even-less-good-value {
background-color: #dc3545;
}
/* Output */
output {
display: inline-block;
padding: 0.375rem 0.75rem;
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
/* Datalist styling (limited browser support) */
datalist {
display: none;
}
/* Buttons */
button,
input[type="button"],
input[type="submit"],
input[type="reset"] {
display: inline-block;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: bold;
font-family: inherit;
border: none;
border-radius: 0.25rem;
cursor: pointer;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
background-color: #6c757d;
color: white;
}
button:hover,
input[type="button"]:hover,
input[type="submit"]:hover,
input[type="reset"]:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
button:active,
input[type="button"]:active,
input[type="submit"]:active,
input[type="reset"]:active {
transform: translateY(0);
}
button:disabled,
input[type="button"]:disabled,
input[type="submit"]:disabled,
input[type="reset"]:disabled {
background-color: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
button:focus,
input[type="button"]:focus,
input[type="submit"]:focus,
input[type="reset"]:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
/* Button variants */
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-primary:active {
background-color: #004085;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-secondary:active {
background-color: #4e555b;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #138496;
}
.btn-light {
background-color: #f8f9fa;
color: #212529;
border: 1px solid #dee2e6;
}
.btn-light:hover {
background-color: #e2e6ea;
}
.btn-dark {
background-color: #343a40;
color: white;
}
.btn-dark:hover {
background-color: #23272b;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
background-color: white;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
}
th {
font-weight: bold;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
}
tr:hover {
background-color: #f5f5f5;
}
caption {
font-weight: bold;
margin-bottom: 0.5rem;
color: #666;
}
thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
}
tbody + tbody {
border-top: 2px solid #dee2e6;
}
/* Table variants */
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f9f9f9;
}
.table-bordered {
border: 1px solid #dee2e6;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #dee2e6;
}
/* Images and Media */
img {
max-width: 100%;
height: auto;
border-radius: 0.25rem;
}
figure {
margin: 1rem 0;
text-align: center;
}
figcaption {
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
font-style: italic;
}
audio, video {
width: 100%;
max-width: 100%;
}
canvas {
max-width: 100%;
height: auto;
}
svg {
max-width: 100%;
height: auto;
fill: currentColor;
}
/* Sectioning Elements */
article, section, nav, aside {
margin-bottom: 1rem;
}
header, footer {
padding: 1rem 0;
}
main {
min-height: calc(100vh - 200px);
}
/* Interactive Elements */
details {
margin-bottom: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
summary {
font-weight: bold;
cursor: pointer;
margin-bottom: 0.5rem;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
summary:hover {
background-color: #e9ecef;
}
details[open] summary {
margin-bottom: 1rem;
}
dialog {
border: 1px solid #ccc;
border-radius: 0.5rem;
padding: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background: white;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
/* Embedded Content */
iframe {
width: 100%;
border: none;
border-radius: 0.25rem;
}
embed, object {
max-width: 100%;
}
/* Custom Layout Utilities (Non-Tailwind) */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.row {
display: flex;
flex-wrap: wrap;
margin: -0.5rem;
}
.col {
flex: 1;
padding: 0.5rem;
}
/* Error states */
.form-control-error {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.is-invalid {
border-color: #dc3545;
}
.is-valid {
border-color: #28a745;
}
/* Print styles */
@media print {
* {
box-shadow: none !important;
text-shadow: none !important;
}
a, a:visited {
text-decoration: underline;
}
abbr[title]:after {
content: " (" attr(title) ")";
}
pre, blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr, img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3 {
page-break-after: avoid;
}
}

View File

@@ -1,586 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Advanced Validation Features Demo",
"description": "Demonstrates all implemented validation features",
"properties": {
"stringValidations": {
"type": "object",
"title": "String Validation Examples",
"properties": {
"basicString": {
"type": "string",
"title": "Basic String with Length Limits",
"minLength": 3,
"maxLength": 20,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 1
}
},
"patternString": {
"type": "string",
"title": "Pattern Validation (Letters Only)",
"pattern": "^[A-Za-z\\s]+$",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"order": 2,
"placeholder": "Letters and spaces only"
}
},
"emailField": {
"type": "string",
"format": "email",
"title": "Email Format Validation",
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
"order": 3
}
},
"urlField": {
"type": "string",
"format": "uri",
"title": "URL Format Validation",
"ui": {
"element": "input",
"type": "url",
"class": "form-group",
"order": 4
}
},
"dateField": {
"type": "string",
"format": "date",
"title": "Date Format Validation",
"ui": {
"element": "input",
"type": "date",
"class": "form-group",
"order": 5
}
},
"enumField": {
"type": "string",
"title": "Enum Selection",
"enum": [ "option1", "option2", "option3" ],
"ui": {
"element": "select",
"class": "form-group",
"order": 6,
"options": [
{ "value": "option1", "text": "Option 1" },
{ "value": "option2", "text": "Option 2" },
{ "value": "option3", "text": "Option 3" }
]
}
}
},
"required": [ "basicString", "emailField" ]
},
"numericValidations": {
"type": "object",
"title": "Numeric Validation Examples",
"properties": {
"integerField": {
"type": "integer",
"title": "Integer with Range",
"minimum": 1,
"maximum": 100,
"ui": {
"element": "input",
"type": "number",
"class": "form-group",
"order": 1
}
},
"numberField": {
"type": "number",
"title": "Number with Exclusive Range",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000,
"multipleOf": 0.5,
"ui": {
"element": "input",
"type": "number",
"class": "form-group",
"order": 2,
"step": "0.5"
}
},
"rangeField": {
"type": "integer",
"title": "Range Slider",
"minimum": 0,
"maximum": 100,
"default": 50,
"ui": {
"element": "input",
"type": "range",
"class": "form-group",
"order": 3
}
}
}
},
"arrayValidations": {
"type": "object",
"title": "Array Validation Examples",
"properties": {
"multiSelect": {
"type": "array",
"title": "Multi-Select with Constraints",
"minItems": 2,
"maxItems": 4,
"uniqueItems": true,
"items": {
"type": "string",
"enum": [ "red", "green", "blue", "yellow", "purple", "orange" ]
},
"ui": {
"element": "select",
"class": "form-group",
"order": 1,
"multiple": true,
"options": [
{ "value": "red", "text": "Red" },
{ "value": "green", "text": "Green" },
{ "value": "blue", "text": "Blue" },
{ "value": "yellow", "text": "Yellow" },
{ "value": "purple", "text": "Purple" },
{ "value": "orange", "text": "Orange" }
]
}
},
"dynamicList": {
"type": "array",
"title": "Dynamic List of Objects",
"minItems": 1,
"maxItems": 5,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name",
"minLength": 2
},
"value": {
"type": "number",
"title": "Value",
"minimum": 0
}
},
"required": [ "name" ]
},
"ui": {
"element": "fieldset",
"class": "dynamic-list",
"order": 2
}
}
}
},
"conditionalValidations": {
"type": "object",
"title": "Conditional Validation Examples",
"properties": {
"userType": {
"type": "string",
"title": "User Type",
"enum": [ "individual", "business", "non-profit" ],
"ui": {
"element": "select",
"class": "form-group",
"order": 1,
"options": [
{ "value": "individual", "text": "Individual" },
{ "value": "business", "text": "Business" },
{ "value": "non-profit", "text": "Non-Profit" }
]
}
},
"individualInfo": {
"type": "object",
"title": "Individual Information",
"properties": {
"ssn": {
"type": "string",
"title": "Social Security Number",
"pattern": "^\\d{3}-\\d{2}-\\d{4}$",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"placeholder": "XXX-XX-XXXX"
}
}
}
},
"businessInfo": {
"type": "object",
"title": "Business Information",
"properties": {
"taxId": {
"type": "string",
"title": "Tax ID",
"pattern": "^\\d{2}-\\d{7}$",
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"placeholder": "XX-XXXXXXX"
}
},
"businessName": {
"type": "string",
"title": "Business Name",
"minLength": 2,
"ui": {
"element": "input",
"type": "text",
"class": "form-group"
}
}
}
}
},
"allOf": [
{
"if": {
"properties": {
"userType": { "const": "individual" }
}
},
"then": {
"properties": {
"individualInfo": {
"required": [ "ssn" ]
}
}
}
},
{
"if": {
"properties": {
"userType": { "const": "business" }
}
},
"then": {
"properties": {
"businessInfo": {
"required": [ "taxId", "businessName" ]
}
}
}
}
]
},
"dependentFields": {
"type": "object",
"title": "Dependent Field Examples",
"properties": {
"hasPhone": {
"type": "boolean",
"title": "I have a phone number",
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group",
"order": 1
}
},
"phoneNumber": {
"type": "string",
"title": "Phone Number",
"pattern": "^\\+?[1-9]\\d{1,14}$",
"ui": {
"element": "input",
"type": "tel",
"class": "form-group",
"order": 2
}
},
"preferredContact": {
"type": "string",
"title": "Preferred Contact Method",
"enum": [ "email", "phone", "mail" ],
"ui": {
"element": "select",
"class": "form-group",
"order": 3,
"options": [
{ "value": "email", "text": "Email" },
{ "value": "phone", "text": "Phone" },
{ "value": "mail", "text": "Physical Mail" }
]
}
},
"mailingAddress": {
"type": "string",
"title": "Mailing Address",
"ui": {
"element": "textarea",
"class": "form-group",
"order": 4,
"rows": 3
}
}
},
"dependentRequired": {
"hasPhone": [ "phoneNumber" ],
"preferredContact": [ "preferredContact" ]
},
"if": {
"properties": {
"preferredContact": { "const": "phone" }
}
},
"then": {
"required": [ "phoneNumber" ]
},
"else": {
"if": {
"properties": {
"preferredContact": { "const": "mail" }
}
},
"then": {
"required": [ "mailingAddress" ]
}
}
},
"compositionValidations": {
"type": "object",
"title": "Composition Validation Examples (allOf, anyOf, oneOf)",
"properties": {
"contactMethod": {
"title": "Contact Method Validation",
"anyOf": [
{
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email Address"
}
},
"required": [ "email" ]
},
{
"type": "object",
"properties": {
"phone": {
"type": "string",
"pattern": "^\\+?[1-9]\\d{1,14}$",
"title": "Phone Number"
}
},
"required": [ "phone" ]
}
],
"ui": {
"element": "fieldset",
"class": "contact-methods",
"order": 1
}
},
"paymentMethod": {
"title": "Payment Method (One Of)",
"oneOf": [
{
"type": "object",
"properties": {
"type": { "const": "credit_card" },
"cardNumber": {
"type": "string",
"pattern": "^\\d{16}$",
"title": "Card Number"
},
"expiryDate": {
"type": "string",
"pattern": "^(0[1-9]|1[0-2])\\/\\d{2}$",
"title": "Expiry Date (MM/YY)"
}
},
"required": [ "type", "cardNumber", "expiryDate" ]
},
{
"type": "object",
"properties": {
"type": { "const": "paypal" },
"paypalEmail": {
"type": "string",
"format": "email",
"title": "PayPal Email"
}
},
"required": [ "type", "paypalEmail" ]
}
],
"ui": {
"element": "fieldset",
"class": "payment-methods",
"order": 2
}
}
}
},
"advancedFeatures": {
"type": "object",
"title": "Advanced Features",
"properties": {
"constField": {
"const": "REQUIRED_VALUE",
"title": "Constant Value Field",
"ui": {
"element": "input",
"type": "hidden",
"value": "REQUIRED_VALUE"
}
},
"booleanField": {
"type": "boolean",
"title": "Boolean Checkbox",
"ui": {
"element": "input",
"type": "checkbox",
"class": "form-group"
}
},
"readOnlyField": {
"type": "string",
"title": "Read Only Field",
"default": "This is read only",
"readOnly": true,
"ui": {
"element": "input",
"type": "text",
"class": "form-group",
"readonly": true
}
},
"textareaField": {
"type": "string",
"title": "Large Text Area",
"maxLength": 500,
"ui": {
"element": "textarea",
"class": "form-group",
"rows": 4,
"placeholder": "Enter detailed information here..."
}
}
}
}
},
"required": [ "stringValidations" ],
"form": {
"class": "validation-demo-form",
"action": "/api/validate-form",
"method": "POST",
"groups": [
{
"title": {
"text": "String Validations",
"class": "section-header"
},
"class": "string-validation-section",
"fields": [
"stringValidations.basicString",
"stringValidations.patternString",
"stringValidations.emailField",
"stringValidations.urlField",
"stringValidations.dateField",
"stringValidations.enumField"
]
},
{
"title": {
"text": "Numeric Validations",
"class": "section-header"
},
"class": "numeric-validation-section",
"fields": [
"numericValidations.integerField",
"numericValidations.numberField",
"numericValidations.rangeField"
]
},
{
"title": {
"text": "Array Validations",
"class": "section-header"
},
"class": "array-validation-section",
"fields": [
"arrayValidations.multiSelect",
"arrayValidations.dynamicList"
]
},
{
"title": {
"text": "Conditional Validations",
"class": "section-header"
},
"class": "conditional-validation-section",
"fields": [
"conditionalValidations.userType",
"conditionalValidations.individualInfo",
"conditionalValidations.businessInfo"
]
},
{
"title": {
"text": "Dependent Fields",
"class": "section-header"
},
"class": "dependent-validation-section",
"fields": [
"dependentFields.hasPhone",
"dependentFields.phoneNumber",
"dependentFields.preferredContact",
"dependentFields.mailingAddress"
]
},
{
"title": {
"text": "Composition Validations",
"class": "section-header"
},
"class": "composition-validation-section",
"fields": [
"compositionValidations.contactMethod",
"compositionValidations.paymentMethod"
]
},
{
"title": {
"text": "Advanced Features",
"class": "section-header"
},
"class": "advanced-features-section",
"fields": [
"advancedFeatures.constField",
"advancedFeatures.booleanField",
"advancedFeatures.readOnlyField",
"advancedFeatures.textareaField"
]
}
],
"submit": {
"label": "Validate Form",
"class": "btn btn-primary"
},
"reset": {
"label": "Reset All",
"class": "btn btn-secondary"
}
}
}

View File

@@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDqDCCApCgAwIBAgIQBuz6Swcf+c/9yR95gWHeMzANBgkqhkiG9w0BAQsFADBu
MQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMRAwDgYDVQQHEwdNeSBDaXR5MRIwEAYD
VQQJEwlNeSBTdHJlZXQxDjAMBgNVBBETBTAwMDAwMQ4wDAYDVQQKEwVNeSBDQTEO
MAwGA1UEAxMFTXkgQ0EwHhcNMjQxMDAxMDU0MzE2WhcNMjUxMDAxMDU0MzE2WjBu
MQswCQYDVQQGEwJVUzEJMAcGA1UECBMAMRAwDgYDVQQHEwdNeSBDaXR5MRIwEAYD
VQQJEwlNeSBTdHJlZXQxDjAMBgNVBBETBTAwMDAwMQ4wDAYDVQQKEwVNeSBDQTEO
MAwGA1UEAxMFTXkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD
hr0IwUOXUWzfZOdxjMEGxbp7q5jGzeC0VmjCRmvvWQgV4aX+pCYSEzDBwe+2ryFx
Kpvp75/SDTTT93bC3/wYSS0XpcJoISoSb3qVhcoQXGB7d80tPfyZO+MCPOCtnJf2
2mRT1uN79tA2gsMLYboatTQVQ1HJZQbs7h+HQQsm2PBdIRBwZGRlh90eFzF/BUIj
rJq9Dg0xmI1lmKXW/XuUR8P8rwMf0CZSpKYAvf4P6ORF8oJlhfuj5im+eH2pYkGZ
kpA7I2+1wk8UJSIXXR4XMkd9PsByHjeRWLBDViFaiKPqf/XdHda56lxhnOQGTidU
cGud/8MsWJj5c9hdBDPPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAPBgNVHRMB
Af8EBTADAQH/MB0GA1UdDgQWBBTiLIKo+Kb2S1OK6Ch3p3+ZghZfEzANBgkqhkiG
9w0BAQsFAAOCAQEAN49h/WETvhyCuLTWFgl+dGK+uM0k5yRwTFtThu7FLZdqOvis
l4WthkLt3oanNSyxs4RhDvd6oZ6lQvY7lNy4Z6U83QYR1O7dTVPb7wWrsnjUd4/l
be3qfoIo5SMPr3db0D019LI+vq5UUk4DC0YpI/DPFLL9kfDHvdtlRbIrmgbEjwwN
smu2wcbSNM22yk2P5vFE9jgRLZ4rQYpvMPrnTEACr6uLdVut6rpx2PpVq6W0vNK4
4c2PGNxXolTlZBmz7knih3WJrgOuoaeVIN8WqR9GiZQjFPzr4AFM31q5BNUlK4TU
Xi1a0yAjs0xWu0NCq1zbwh9+SB8SuYtIvsjILQ==
-----END CERTIFICATE-----

View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAw4a9CMFDl1Fs32TncYzBBsW6e6uYxs3gtFZowkZr71kIFeGl
/qQmEhMwwcHvtq8hcSqb6e+f0g000/d2wt/8GEktF6XCaCEqEm96lYXKEFxge3fN
LT38mTvjAjzgrZyX9tpkU9bje/bQNoLDC2G6GrU0FUNRyWUG7O4fh0ELJtjwXSEQ
cGRkZYfdHhcxfwVCI6yavQ4NMZiNZZil1v17lEfD/K8DH9AmUqSmAL3+D+jkRfKC
ZYX7o+Ypvnh9qWJBmZKQOyNvtcJPFCUiF10eFzJHfT7Ach43kViwQ1YhWoij6n/1
3R3WuepcYZzkBk4nVHBrnf/DLFiY+XPYXQQzzwIDAQABAoIBAAkE2w1fVM3TDLGV
RvO+6Vx1nG9979Mjxfyri7OCahIlSjEwMmb3jWYCCpq1ZmhH1cQRkhWNXxLiVxB7
9rdwe4FnRrQzii8hcH5fNAlXnYV5rV2kngs7M76hu4vr4PVBJuVVF5GidOXP8bTB
/Vs2C86Vkyxz6X7fsR0Wss+bWXdWL8RRTrFqPlHRjaaCcOK6dDzFRafvBN1CzaJ0
Acv03BZITVSgjTfg2kNhwyrrq6EqExxLAxXrY/3cSI+bDx68W0LxVgNTfjceY5uP
uJhnYO6riuSaSD4uz3SKUUXH6zR5RDUGAPnICgM5MEDtAA3ZY3jGfbu9cJYkx9oL
rB6+1nECgYEA39PKyhHo++/wu46P94RpLZ1+RoKiE94DoZjdw9gRa4QGJUO7KGJ+
Utnp+KzoeB3FSaXJyGj/1cLrmccsdRdSs6l4NAihQRDM8kB+7TEUCFoNfCRDQZ+J
Pf+afv/v+jR0WdlguDhFmMnAN3euJXAuHwOaZseeIRTXu8RDPkDE6qcCgYEA36GM
5Szc0TVTbcVhK3vPEBn/CgcLyjfrDbyAQp+Y0InUaQnrpzcW37sejsiK3rVyFTeQ
WQdobm6MGT+QkO3nE1/riJmyEUq3/WRYtRnGjurq6EwYeG6QxXV6SqkA9OXZSqKX
aFfH6lUs+6m4drPPzzmNEBSxt3esZGDXq29pGpkCgYByr2Z822hxjqPetlF2FdZ+
lPAa2NyLKXra1iTrME7ctC0h8u525uCrOxTzYkVLJpXsApK9qW9M7C8kADX7WRP7
Ep6QqstVN3KLvhhLGJaXIO0/6qS7fy8nIUzcPe+MWEw1rXgtbEfc3aMryJrme/Bl
28bFWwrfEHrprsp1n2JGiQKBgDgROdDveYFePEeGOAF97gEcc2vhLlyJvn3YJ9QM
TXTjSYT4PsPStQJs2JF1yBNkLHETWDZp/A3L24YtAKLFcqzR3KyH1DQvpod6FB97
keOdFD4fbfcryVIoTPvQ+XNs+RiUQR+g+ndO2ZNTDvN7y3sp86r3dUMJVwhnm0rZ
COHpAoGBAJf7y/PM4T5xeBZ4LhHzGIIWaBJRRo9JkWCyLkQBVNpDs9chK6CPUcR4
61ouAGAIRRUJt4KTJAuyt+rnEvvCTGkpq2MtrMx5+SLmj4Soh9Fo+FTqkVA15Kr/
+05wVMIWkVqKXGF7Um9duPRUpRHhGSML3uG6rujSQWHJqibooyje
-----END RSA PRIVATE KEY-----

View File

@@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDUjCCAjqgAwIBAgIRAMdsW2PjhMRI5lI74hlnjacwDQYJKoZIhvcNAQELBQAw
bjELMAkGA1UEBhMCVVMxCTAHBgNVBAgTADEQMA4GA1UEBxMHTXkgQ2l0eTESMBAG
A1UECRMJTXkgU3RyZWV0MQ4wDAYDVQQREwUwMDAwMDEOMAwGA1UEChMFTXkgQ0Ex
DjAMBgNVBAMTBU15IENBMB4XDTI0MTAwMTA1NDMxNloXDTI1MTAwMTA1NDMxNlow
ETEPMA0GA1UEAxMGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAxMrkyIFnEaGoZ9Z5iVVdSzoq8FTrJE3iGCZYjLRLTO/1Hq6L5C6tDqzYq3fv
V64G3B6yuWYE1SpqJ5C8T9G89Gc1jp6ZklP92nL+S7hOPjWsm+y33vM4WQQzqmY7
BucE4yMXVZSAkr4uCe9/iTIeUBYgDOPmoJRwOS+y9mlBi6gqoWre3NDHbt9h4zim
Hg2Nsd6HT0kKcSKhrr3Xz87o8pWHyi/O7hexB3WBLIjgX43Wh0jhxwZ84FVHyCH3
VR1UuhrInUxrWBE2HF9hhRp/8RUMgPggYIXDTNycUBJy0PEjBHy1s1hIqX75tEfP
JHNQj0NCHJ7UPFf7x1GsKPF62QIDAQABo0gwRjAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAU4iyCqPim9ktTiugod6d/mYIW
XxMwDQYJKoZIhvcNAQELBQADggEBACdY5KqLAXNHoZDof02daC+veHMM09VhFBrZ
UugYnXh6xmP+cKiINKfylr3Vqdtt4JXXMR8teTdv/Dk5ho17XtWyHbQ22bZN2DwH
vCgs4pPyqZvwfuBuWJ85fiu7B0AsrInSmdxMJeBIenTWyWU4bg19NsTejfJKIhk9
dkvTLWryCZpoaA8lQZ+l39p10/L2PPnOdNU+TOzsbrJKnZkwCdlkAvZhyaVzTQfk
YSjVr1cakmq5T9u/8kWeb3Bx1z0GVXy0Jbgr1XBUv88IuGYH/KnrEfCweN5B+KQ7
zwGD9PPu7E7+TpFkRH5uzv+3y8C6bICetlhWnWhQ237IEaoRpfU=
-----END CERTIFICATE-----

View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAxMrkyIFnEaGoZ9Z5iVVdSzoq8FTrJE3iGCZYjLRLTO/1Hq6L
5C6tDqzYq3fvV64G3B6yuWYE1SpqJ5C8T9G89Gc1jp6ZklP92nL+S7hOPjWsm+y3
3vM4WQQzqmY7BucE4yMXVZSAkr4uCe9/iTIeUBYgDOPmoJRwOS+y9mlBi6gqoWre
3NDHbt9h4zimHg2Nsd6HT0kKcSKhrr3Xz87o8pWHyi/O7hexB3WBLIjgX43Wh0jh
xwZ84FVHyCH3VR1UuhrInUxrWBE2HF9hhRp/8RUMgPggYIXDTNycUBJy0PEjBHy1
s1hIqX75tEfPJHNQj0NCHJ7UPFf7x1GsKPF62QIDAQABAoIBAESksSDvYlBYHzH5
MfOhfyVaaNfkBxFmyVK7LXAHA60Wll3ZbJpvXZYc3IcTEr12ypXFb3oUB+ODI/wh
FE6TTmHCDoBs+gx8l7O3INSwuToh5s+MxqZSGHmUaaEqf7RsqNvBxcXoQuDszYpR
rB7jCIfO7+cPJ8cjf/Gyna4uENrxdQgy8ESDYiLif5RhXZtYcuJ1dlojAshrzgz+
PhFN7TeXTYNAit9txVybHt6m7hCzkmGjyUAhIrNoeIxizIu4osu/IoRrwhCuBRSw
3zqvOErr7JG2gI8Wj5Bs9Mkwn9iJeB4tZzGAwmw2t2eqqfdVNFFUNHE7vVRZ03/T
t/DwjLECgYEA1c79kQjzhwbuccPEOpBwl6hBngbbNw5+PtqBtizo12T3b2LsOqlZ
eb2G+5yUFW2enrZIl+KS6iCrutW1wHbxIxYijy13hlp/ecmLJeDRRdseOEagh8Re
NticHiNjluTns/sBl56unpm2lw+dp75cm+R1IiRkyiOdPcdvsfaPUZsCgYEA66BN
Z7mS6HL/juMEO5IGVluVMpIwq8seVWG8s6vQF1COlYfC/tWwxM/+95bKI/SmCMTK
83CtDrvGQ6dax2QjsxeP9mJ1GKIwEGrFrPn+vsmh0KBChG9HxVq2miWicH32WA+8
MDwVFQUQa0MI1LsSrZsqhBGq+3KkKuBPuejTVpsCgYASnzCejTUIsaXa6r4Qi7wC
uXjdlqNJLE36k3VwtICjIfwbC3aftVhBriwvhfev1hhWonG4KNe65JWQdEScOr/N
2oOwDLm4TfGEXfVsmyQe/XKoXB5nNMcv57XROivWXKGBn38IAZ4b2i95ALcugPn3
6fH5w0m0AV4Un2YvDdZ1uQKBgH2kUuIWYDG28HK+tskVCnAOEbaPoYhZnOkmXrrn
yORFvmIZrG66f7HSv0BClbMqh0ZxuU6qLH2IvyXgHVXpHegnjkpxIcNq6Ho4lQOx
opcVaUWXzyBTPlAMGQaFPuMBJ9S5Pz3xK8SzmJe5fQICZulPrhISYbwG22dJiPm3
Hso1AoGAUqBG8FEyw6qQ+M3wg1n79QAWKQO8cJft7MU5ruTTJEZzm8kSqMzJ8UPz
C/CQiCehFjKNflYUZQV6RNWJp6H2vhe2qprDV88j6fhKfFi4va3CsELAKE/tAWs/
WqydAe/dOQ8HwJCBrC4vRds8KOQTxPJUhPm/eM0Jf2Zocs5Axdc=
-----END RSA PRIVATE KEY-----

View File

@@ -1,683 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
)
// StartProcessor - Initial node that receives and processes the input data
type StartProcessor struct {
dag.Operation
}
func (p *StartProcessor) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
fmt.Printf("[START] Processing task %s - Initial data processing\n", task.ID)
// Simulate initial processing work
time.Sleep(100 * time.Millisecond)
// Create detailed result with node-specific information
processingResult := map[string]interface{}{
"node_name": "start",
"task_id": task.ID,
"processed_at": time.Now().Format("15:04:05"),
"duration_ms": 100,
"status": "success",
"action": "initialized_data",
"data_size": len(fmt.Sprintf("%v", task.Payload)),
}
// Initialize or update node results in context
var nodeResults map[string]interface{}
if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok {
nodeResults = existing
} else {
nodeResults = make(map[string]interface{})
}
nodeResults["start"] = processingResult
// Create new context with updated results
newCtx := context.WithValue(ctx, "nodeResults", nodeResults)
fmt.Printf("[START] Node completed - data initialized for task %s\n", task.ID)
return mq.Result{
TaskID: task.ID,
Status: mq.Completed,
Payload: task.Payload,
Ctx: newCtx,
}
}
// ProcessorNode - Processes and transforms the data
type ProcessorNode struct {
dag.Operation
}
func (p *ProcessorNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
fmt.Printf("[PROCESS] Processing task %s - Data transformation\n", task.ID)
// Simulate processing work
time.Sleep(100 * time.Millisecond)
processingResult := map[string]interface{}{
"node_name": "process",
"task_id": task.ID,
"processed_at": time.Now().Format("15:04:05"),
"duration_ms": 100,
"status": "success",
"action": "transformed_data",
"data_size": len(fmt.Sprintf("%v", task.Payload)),
}
// Update node results in context
var nodeResults map[string]interface{}
if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok {
nodeResults = existing
} else {
nodeResults = make(map[string]interface{})
}
nodeResults["process"] = processingResult
newCtx := context.WithValue(ctx, "nodeResults", nodeResults)
fmt.Printf("[PROCESS] Node completed - data transformed for task %s\n", task.ID)
return mq.Result{
TaskID: task.ID,
Status: mq.Completed,
Payload: task.Payload,
Ctx: newCtx,
}
}
// ValidatorNode - Validates the processed data
type ValidatorNode struct {
dag.Operation
}
func (p *ValidatorNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
fmt.Printf("[VALIDATE] Processing task %s - Data validation\n", task.ID)
// Simulate validation work
time.Sleep(100 * time.Millisecond)
processingResult := map[string]interface{}{
"node_name": "validate",
"task_id": task.ID,
"processed_at": time.Now().Format("15:04:05"),
"duration_ms": 100,
"status": "success",
"action": "validated_data",
"validation": "passed",
"data_size": len(fmt.Sprintf("%v", task.Payload)),
}
// Update node results in context
var nodeResults map[string]interface{}
if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok {
nodeResults = existing
} else {
nodeResults = make(map[string]interface{})
}
nodeResults["validate"] = processingResult
newCtx := context.WithValue(ctx, "nodeResults", nodeResults)
fmt.Printf("[VALIDATE] Node completed - data validated for task %s\n", task.ID)
return mq.Result{
TaskID: task.ID,
Status: mq.Completed,
Payload: task.Payload,
Ctx: newCtx,
}
}
// EndProcessor - Final node that completes the processing
type EndProcessor struct {
dag.Operation
}
func (p *EndProcessor) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
fmt.Printf("[END] Processing task %s - Final processing\n", task.ID)
// Simulate final processing work
time.Sleep(100 * time.Millisecond)
processingResult := map[string]interface{}{
"node_name": "end",
"task_id": task.ID,
"processed_at": time.Now().Format("15:04:05"),
"duration_ms": 100,
"status": "success",
"action": "finalized_data",
"data_size": len(fmt.Sprintf("%v", task.Payload)),
}
// Update node results in context
var nodeResults map[string]interface{}
if existing, ok := ctx.Value("nodeResults").(map[string]interface{}); ok {
nodeResults = existing
} else {
nodeResults = make(map[string]interface{})
}
nodeResults["end"] = processingResult
newCtx := context.WithValue(ctx, "nodeResults", nodeResults)
fmt.Printf("[END] Node completed - processing finished for task %s\n", task.ID)
return mq.Result{
TaskID: task.ID,
Status: mq.Completed,
Payload: task.Payload,
Ctx: newCtx,
}
}
func main() {
// Create a new DAG with enhanced features
d := dag.NewDAG("enhanced-example", "example", finalResultCallback)
// Build the DAG structure (avoiding cycles)
buildDAG(d)
fmt.Println("DAG validation passed! (cycle-free structure)")
// Set up basic API endpoints
setupAPI(d)
// Process some tasks
processTasks(d)
// Display basic statistics
displayStatistics(d)
// Start HTTP server for API
fmt.Println("Starting HTTP server on :8080")
fmt.Println("Visit http://localhost:8080 for the dashboard")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func finalResultCallback(taskID string, result mq.Result) {
fmt.Printf("Task %s completed with status: %v\n", taskID, result.Status)
}
func buildDAG(d *dag.DAG) {
// Add nodes in a linear flow to avoid cycles - using proper processor types
d.AddNode(dag.Function, "Start Node", "start", &StartProcessor{}, true)
d.AddNode(dag.Function, "Process Node", "process", &ProcessorNode{})
d.AddNode(dag.Function, "Validate Node", "validate", &ValidatorNode{})
d.AddNode(dag.Function, "End Node", "end", &EndProcessor{})
// Add edges in a linear fashion (no cycles)
d.AddEdge(dag.Simple, "start-to-process", "start", "process")
d.AddEdge(dag.Simple, "process-to-validate", "process", "validate")
d.AddEdge(dag.Simple, "validate-to-end", "validate", "end")
fmt.Println("DAG structure built successfully")
}
func setupAPI(d *dag.DAG) {
// Basic status endpoint
http.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
status := map[string]interface{}{
"status": "running",
"dag_name": d.GetType(),
"timestamp": time.Now(),
}
json.NewEncoder(w).Encode(status)
})
// Task metrics endpoint
http.HandleFunc("/api/metrics", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
metrics := d.GetTaskMetrics()
// Create a safe copy to avoid lock issues
safeMetrics := map[string]interface{}{
"completed": metrics.Completed,
"failed": metrics.Failed,
"cancelled": metrics.Cancelled,
"not_started": metrics.NotStarted,
"queued": metrics.Queued,
}
json.NewEncoder(w).Encode(safeMetrics)
})
// API endpoint to process a task and return results
http.HandleFunc("/api/process", func(w http.ResponseWriter, r *http.Request) {
taskData := map[string]interface{}{
"id": fmt.Sprintf("api-task-%d", time.Now().UnixNano()),
"payload": "api-data",
"timestamp": time.Now(),
}
payload, _ := json.Marshal(taskData)
fmt.Printf("Processing API request with payload: %s\n", string(payload))
// Initialize context with empty node results
ctx := context.WithValue(context.Background(), "nodeResults", make(map[string]interface{}))
result := d.Process(ctx, payload)
fmt.Printf("Processing completed. Status: %v\n", result.Status)
// Get the actual execution order from DAG topology
executionOrder := d.TopologicalSort()
fmt.Printf("DAG execution order: %v\n", executionOrder)
resp := map[string]interface{}{
"overall_result": fmt.Sprintf("%v", result.Status),
"task_id": result.TaskID,
"payload": result.Payload,
"timestamp": time.Now(),
"execution_order": executionOrder, // Include the actual execution order
}
if result.Error != nil {
resp["error"] = result.Error.Error()
fmt.Printf("Error occurred: %v\n", result.Error)
}
// Extract node results from context
if nodeResults, ok := result.Ctx.Value("nodeResults").(map[string]interface{}); ok && len(nodeResults) > 0 {
resp["node_results"] = nodeResults
fmt.Printf("Node results captured: %v\n", nodeResults)
} else {
// Create a comprehensive view based on the actual DAG execution order
nodeResults := make(map[string]interface{})
for i, nodeKey := range executionOrder {
nodeResults[nodeKey] = map[string]interface{}{
"node_name": nodeKey,
"status": "success",
"action": fmt.Sprintf("executed_step_%d", i+1),
"executed_at": time.Now().Format("15:04:05"),
"execution_step": i + 1,
}
}
resp["node_results"] = nodeResults
fmt.Printf("📝 Created node results based on DAG topology: %v\n", nodeResults)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
})
// DAG Diagram endpoint - generates and serves PNG diagram
http.HandleFunc("/api/diagram", func(w http.ResponseWriter, r *http.Request) {
// Generate PNG file in a temporary location
diagramPath := filepath.Join(os.TempDir(), fmt.Sprintf("dag-%d.png", time.Now().UnixNano()))
fmt.Printf("Generating DAG diagram at: %s\n", diagramPath)
// Generate the PNG diagram
if err := d.SavePNG(diagramPath); err != nil {
fmt.Printf("Failed to generate diagram: %v\n", err)
http.Error(w, fmt.Sprintf("Failed to generate diagram: %v", err), http.StatusInternalServerError)
return
}
// Ensure cleanup
defer func() {
if err := os.Remove(diagramPath); err != nil {
fmt.Printf("Failed to cleanup diagram file: %v\n", err)
}
}()
// Serve the PNG file
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "no-cache")
http.ServeFile(w, r, diagramPath)
fmt.Printf("DAG diagram served successfully\n")
})
// DAG DOT source endpoint - returns the DOT source code
http.HandleFunc("/api/dot", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
dotContent := d.ExportDOT()
w.Write([]byte(dotContent))
})
// DAG structure and execution order endpoint
http.HandleFunc("/api/structure", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
executionOrder := d.TopologicalSort()
structure := map[string]interface{}{
"execution_order": executionOrder,
"dag_name": d.GetType(),
"dag_key": d.GetKey(),
"total_nodes": len(executionOrder),
"timestamp": time.Now(),
}
json.NewEncoder(w).Encode(structure)
})
// Root dashboard
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `
<!DOCTYPE html>
<html>
<head>
<title>Enhanced DAG Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 40px; }
.section { margin: 30px 0; padding: 20px; border: 1px solid #e0e0e0; border-radius: 5px; }
.endpoint { margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 3px; }
.method { color: #007acc; font-weight: bold; margin-right: 10px; }
.success { color: #28a745; }
.info { color: #17a2b8; }
h1 { color: #333; }
h2 { color: #666; border-bottom: 2px solid #007acc; padding-bottom: 10px; }
.feature-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.feature-card { background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #007acc; }
.results { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 20px; }
.node-result { margin-left: 20px; }
.btn { padding: 10px 20px; background: #007acc; color: white; border: none; border-radius: 4px; cursor: pointer; }
.btn:active { background: #005fa3; }
/* Tabs styles */
.tabs { display: flex; border-bottom: 2px solid #e0e0e0; margin-bottom: 20px; }
.tab-btn { background: none; border: none; padding: 12px 30px; cursor: pointer; font-size: 16px; color: #666; border-bottom: 2px solid transparent; transition: color 0.2s, border-bottom 0.2s; }
.tab-btn.active { color: #007acc; border-bottom: 2px solid #007acc; font-weight: bold; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Enhanced DAG Demo Dashboard</h1>
<p class="success">DAG is running successfully!</p>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="showTab('api')">API Endpoints</button>
<button class="tab-btn" onclick="showTab('dag')">DAG Visual Structure</button>
<button class="tab-btn" onclick="showTab('task')">Run Example Task</button>
</div>
<div id="api" class="tab-content active">
<div class="section">
<h2>API Endpoints</h2>
<div class="endpoint">
<span class="method">GET</span>
<a href="/api/status">/api/status</a> - Get DAG status
</div>
<div class="endpoint">
<span class="method">GET</span>
<a href="/api/metrics">/api/metrics</a> - Get task metrics
</div>
<div class="endpoint">
<span class="method">POST</span>
<a href="/api/process">/api/process</a> - Process a new task
</div>
<div class="endpoint">
<span class="method">GET</span>
<a href="/api/diagram">/api/diagram</a> - Get DAG diagram (PNG)
</div>
<div class="endpoint">
<span class="method">GET</span>
<a href="/api/dot">/api/dot</a> - Get DAG structure (DOT format)
</div>
<div class="endpoint">
<span class="method">GET</span>
<a href="/api/structure">/api/structure</a> - Get DAG structure and execution order
</div>
</div>
</div>
<div id="dag" class="tab-content">
<div class="section">
<h2>DAG Visual Structure</h2>
<p><strong>Flow:</strong> Start -> Process -> Validate -> End</p>
<p><strong>Type:</strong> Linear (Cycle-free)</p>
<p class="info">This structure ensures no circular dependencies while demonstrating the enhanced features.</p>
<div style="margin-top: 20px;">
<button class="btn" onclick="loadDiagram()" style="margin-right: 10px;">View DAG Diagram</button>
</div>
<div id="diagram-container" style="margin-top: 20px; text-align: center; display: none;">
<h4>DAG Visual Diagram</h4>
<img id="dag-diagram" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px; background: white; padding: 10px;" alt="DAG Diagram" />
</div>
</div>
</div>
<div id="task" class="tab-content">
<div class="section">
<h2>Run Example Task</h2>
<button class="btn" onclick="runTask()">Run Example Task</button>
<div id="results" class="results"></div>
</div>
</div>
</div>
<script>
// Tabs logic
function showTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.querySelector('.tab-btn[onclick="showTab(\'' + tabId + '\')"]').classList.add('active');
document.getElementById(tabId).classList.add('active');
}
function runTask() {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = "Processing...";
fetch('/api/process')
.then(res => res.json())
.then(data => {
let html = "<h3>Task Execution Results</h3>";
html += "<div style='background: #e8f5e8; padding: 10px; border-radius: 5px; margin: 10px 0;'>";
html += "<h4>Overall Summary</h4>";
html += "<b>Status:</b> <span style='color: " + (data.overall_result === 'Completed' ? 'green' : 'red') + ";'>" + data.overall_result + "</span><br>";
if (data.error) html += "<b>Error:</b> <span style='color: red;'>" + data.error + "</span><br>";
html += "<b>Task ID:</b> " + data.task_id + "<br>";
html += "<b>Request Payload:</b> <code>" + JSON.stringify(data.payload) + "</code><br>";
html += "</div>";
if (data.node_results) {
html += "<h4>Detailed Node Execution Results</h4>";
html += "<div style='background: #f8f9fa; padding: 15px; border-radius: 5px;'>";
// Use the actual execution order from the DAG
const nodeOrder = data.execution_order || Object.keys(data.node_results);
const nodeLabels = {
'start': 'Start Node',
'process': 'Process Node',
'validate': 'Validate Node',
'end': 'End Node'
};
let stepNumber = 0;
// Iterate through nodes in the DAG-determined execution order
for (const nodeKey of nodeOrder) {
if (data.node_results[nodeKey]) {
stepNumber++;
const res = data.node_results[nodeKey];
const nodeLabel = nodeLabels[nodeKey] || nodeKey.charAt(0).toUpperCase() + nodeKey.slice(1) + ' Node';
html += "<div style='margin: 10px 0; padding: 15px; background: white; border-left: 4px solid #007acc; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>";
html += "<div style='display: flex; align-items: center; margin-bottom: 8px;'>";
html += "<b style='color: #007acc;'>Step " + stepNumber + " - " + nodeLabel + "</b>";
if (data.execution_order) {
html += "<span style='margin-left: auto; color: #666; font-size: 12px;'>DAG Order: " + (nodeOrder.indexOf(nodeKey) + 1) + "</span>";
}
html += "</div>";
if (typeof res === 'object') {
// Display key information prominently
if (res.action) {
html += "<div style='margin: 5px 0;'><b>Action:</b> <span style='color: #28a745;'>" + res.action + "</span></div>";
}
if (res.processed_at) {
html += "<div style='margin: 5px 0;'><b>Executed at:</b> " + res.processed_at + "</div>";
}
if (res.duration_ms) {
html += "<div style='margin: 5px 0;'><b>Duration:</b> " + res.duration_ms + "ms</div>";
}
if (res.validation) {
html += "<div style='margin: 5px 0;'><b>Validation:</b> <span style='color: #28a745;'>" + res.validation + "</span></div>";
}
if (res.execution_step) {
html += "<div style='margin: 5px 0;'><b>Execution Step:</b> " + res.execution_step + "</div>";
}
// Show full details in collapsed format
html += "<details style='margin-top: 8px;'>";
html += "<summary style='cursor: pointer; color: #666;'>View Full Details</summary>";
html += "<pre style='background: #f1f1f1; padding: 8px; border-radius: 3px; margin: 5px 0; font-size: 12px;'>" + JSON.stringify(res, null, 2) + "</pre>";
html += "</details>";
} else {
html += "<span style='color: #28a745;'>" + res + "</span>";
}
html += "</div>";
}
}
// Show execution order summary
if (data.execution_order) {
html += "<div style='margin-top: 15px; padding: 10px; background: #e8f4f8; border-radius: 5px; border-left: 3px solid #17a2b8;'>";
html += "<b>DAG Execution Order:</b> " + data.execution_order.join(' -> ');
html += "</div>";
}
html += "</div>";
} else {
html += "<div style='background: #fff3cd; padding: 10px; border-radius: 5px; color: #856404;'>";
html += "No detailed node results available";
html += "</div>";
}
resultsDiv.innerHTML = html;
})
.catch(err => {
resultsDiv.innerHTML = "Error: " + err;
});
}
function loadDiagram() {
const diagramContainer = document.getElementById('diagram-container');
const diagramImg = document.getElementById('dag-diagram');
diagramContainer.style.display = 'block';
diagramImg.src = '/api/diagram?' + new Date().getTime(); // Add timestamp to prevent caching
diagramImg.onload = function() {
console.log('DAG diagram loaded successfully');
};
diagramImg.onerror = function() {
diagramContainer.innerHTML = '<div style="color: red; padding: 20px;">Failed to load DAG diagram. Make sure Graphviz is installed on the server.</div>';
};
}
</script>
</body>
</html>
`)
})
}
func processTasks(d *dag.DAG) {
fmt.Println("Processing example tasks...")
for i := 0; i < 3; i++ {
taskData := map[string]interface{}{
"id": fmt.Sprintf("task-%d", i),
"payload": fmt.Sprintf("example-data-%d", i),
"timestamp": time.Now(),
}
payload, _ := json.Marshal(taskData)
fmt.Printf("Processing task %d...\n", i)
result := d.Process(context.Background(), payload)
// Show overall result
if result.Error == nil {
fmt.Printf("Task %d completed successfully\n", i)
} else {
fmt.Printf("Task %d failed: %v\n", i, result.Error)
}
// Show per-node results in DAG execution order
if nodeResults, ok := result.Ctx.Value("nodeResults").(map[string]interface{}); ok {
fmt.Println("Node Results (DAG Execution Order):")
// Get the actual execution order from DAG topology
executionOrder := d.TopologicalSort()
nodeLabels := map[string]string{
"start": "Start Node",
"process": "Process Node",
"validate": "Validate Node",
"end": "End Node",
}
stepNum := 1
for _, nodeKey := range executionOrder {
if res, exists := nodeResults[nodeKey]; exists {
label := nodeLabels[nodeKey]
if label == "" {
// Capitalize first letter of node key
if len(nodeKey) > 0 {
label = fmt.Sprintf("%s%s Node", strings.ToUpper(nodeKey[:1]), nodeKey[1:])
} else {
label = fmt.Sprintf("%s Node", nodeKey)
}
}
fmt.Printf(" Step %d - %s: %v\n", stepNum, label, res)
stepNum++
}
}
// Show the execution order
fmt.Printf(" DAG Execution Order: %s\n", strings.Join(executionOrder, " → "))
}
time.Sleep(200 * time.Millisecond)
}
fmt.Println("Task processing completed!")
}
func displayStatistics(d *dag.DAG) {
fmt.Println("\n=== DAG Statistics ===")
// Get basic task metrics
metrics := d.GetTaskMetrics()
fmt.Printf("Task Metrics:\n")
fmt.Printf(" Completed: %d\n", metrics.Completed)
fmt.Printf(" Failed: %d\n", metrics.Failed)
fmt.Printf(" Cancelled: %d\n", metrics.Cancelled)
fmt.Printf(" Not Started: %d\n", metrics.NotStarted)
fmt.Printf(" Queued: %d\n", metrics.Queued)
// Get DAG information
fmt.Printf("\nDAG Information:\n")
fmt.Printf(" Name: %s\n", d.GetType())
fmt.Printf(" Key: %s\n", d.GetKey())
// Check if DAG is ready
if d.IsReady() {
fmt.Printf(" Status: Ready\n")
} else {
fmt.Printf(" Status: Not Ready\n")
}
fmt.Println("\n=== End Statistics ===\n")
}

View File

@@ -1,139 +0,0 @@
{
"broker": {
"address": "localhost",
"port": 8080,
"max_connections": 1000,
"connection_timeout": "0s",
"read_timeout": "0s",
"write_timeout": "0s",
"idle_timeout": "0s",
"keep_alive": true,
"keep_alive_period": "60s",
"max_queue_depth": 10000,
"enable_dead_letter": true,
"dead_letter_max_retries": 3
},
"consumer": {
"enable_http_api": true,
"max_retries": 5,
"initial_delay": "2s",
"max_backoff": "30s",
"jitter_percent": 0.5,
"batch_size": 10,
"prefetch_count": 100,
"auto_ack": false,
"requeue_on_failure": true
},
"publisher": {
"enable_http_api": true,
"max_retries": 3,
"initial_delay": "1s",
"max_backoff": "10s",
"jitter_percent": 0.5,
"connection_pool_size": 10,
"publish_timeout": "5s",
"enable_batching": false,
"batch_size": 100,
"batch_timeout": "1s"
},
"pool": {
"min_workers": 1,
"max_workers": 100,
"queue_size": 1000,
"max_memory_load": 1073741824,
"task_timeout": "30s",
"idle_worker_timeout": "5m",
"enable_dynamic_scaling": true,
"scaling_factor": 1.5,
"scaling_interval": "1m",
"max_queue_wait_time": "10s",
"enable_work_stealing": false,
"enable_priority_scheduling": true,
"graceful_shutdown_timeout": "30s"
},
"security": {
"enable_tls": false,
"tls_cert_path": "",
"tls_key_path": "",
"tls_ca_path": "",
"tls_insecure_skip_verify": false,
"enable_authentication": false,
"authentication_method": "basic",
"enable_authorization": false,
"enable_encryption": false,
"encryption_key": "",
"enable_audit_log": false,
"audit_log_path": "/var/log/mq/audit.log",
"session_timeout": "30m",
"max_login_attempts": 3,
"lockout_duration": "15m"
},
"monitoring": {
"enable_metrics": true,
"metrics_port": 9090,
"metrics_path": "/metrics",
"enable_health_check": true,
"health_check_port": 8081,
"health_check_path": "/health",
"health_check_interval": "30s",
"enable_tracing": false,
"tracing_endpoint": "",
"tracing_sample_rate": 0.1,
"enable_logging": true,
"log_level": "info",
"log_format": "json",
"log_output": "stdout",
"log_file_path": "/var/log/mq/app.log",
"log_max_size": 100,
"log_max_backups": 10,
"log_max_age": 30,
"enable_profiling": false,
"profiling_port": 6060
},
"persistence": {
"enable_persistence": false,
"storage_type": "memory",
"connection_string": "",
"max_connections": 10,
"connection_timeout": "10s",
"retention_period": "168h",
"cleanup_interval": "1h",
"backup_enabled": false,
"backup_interval": "6h",
"backup_path": "/var/backup/mq",
"compression_enabled": true,
"encryption_enabled": false,
"replication_enabled": false,
"replication_nodes": [ ]
},
"clustering": {
"enable_clustering": false,
"node_id": "",
"cluster_nodes": [ ],
"discovery_method": "static",
"discovery_endpoint": "",
"heartbeat_interval": "5s",
"election_timeout": "15s",
"enable_load_balancing": false,
"load_balancing_strategy": "round_robin",
"enable_failover": false,
"failover_timeout": "30s",
"enable_replication": false,
"replication_factor": 3,
"consistency_level": "strong"
},
"rate_limit": {
"enable_broker_rate_limit": false,
"broker_rate": 1000,
"broker_burst": 100,
"enable_consumer_rate_limit": false,
"consumer_rate": 100,
"consumer_burst": 10,
"enable_publisher_rate_limit": false,
"publisher_rate": 100,
"publisher_burst": 10,
"enable_per_queue_rate_limit": false,
"per_queue_rate": 50,
"per_queue_burst": 5
}
}

View File

@@ -1,15 +0,0 @@
package main
import (
"context"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
n := &tasks.Node6{}
consumer1 := mq.NewConsumer("F", "queue1", n.ProcessTask, mq.WithBrokerURL(":8081"), mq.WithHTTPApi(true), mq.WithWorkerPool(100, 4, 50000))
consumer1.Consume(context.Background())
}

View File

@@ -1,122 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/oarkflow/json"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/examples/tasks"
)
func subDAG() *dag.DAG {
f := dag.NewDAG("Sub DAG", "sub-dag", func(taskID string, result mq.Result) {
fmt.Printf("Sub DAG Final result for task %s: %s\n", taskID, string(result.Payload))
}, mq.WithSyncMode(true))
f.
AddNode(dag.Function, "Store data", "store:data", &tasks.StoreData{Operation: dag.Operation{Type: dag.Function}}, true).
AddNode(dag.Function, "Send SMS", "send:sms", &tasks.SendSms{Operation: dag.Operation{Type: dag.Function}}).
AddNode(dag.Function, "Notification", "notification", &tasks.InAppNotification{Operation: dag.Operation{Type: dag.Function}}).
AddEdge(dag.Simple, "Store Payload to send sms", "store:data", "send:sms").
AddEdge(dag.Simple, "Store Payload to notification", "send:sms", "notification")
return f
}
func main() {
flow := dag.NewDAG("Sample DAG", "sample-dag", func(taskID string, result mq.Result) {
fmt.Printf("DAG Final result for task %s: %s\n", taskID, string(result.Payload))
})
flow.AddNode(dag.Function, "GetData", "GetData", &GetData{}, true)
flow.AddNode(dag.Function, "Loop", "Loop", &Loop{})
flow.AddNode(dag.Function, "ValidateAge", "ValidateAge", &ValidateAge{})
flow.AddNode(dag.Function, "ValidateGender", "ValidateGender", &ValidateGender{})
flow.AddNode(dag.Function, "Final", "Final", &Final{})
flow.AddDAGNode(dag.Function, "Check", "persistent", subDAG())
flow.AddEdge(dag.Simple, "GetData", "GetData", "Loop")
flow.AddEdge(dag.Iterator, "Validate age for each item", "Loop", "ValidateAge")
flow.AddCondition("ValidateAge", map[string]string{"pass": "ValidateGender", "default": "persistent"})
flow.AddEdge(dag.Simple, "Mark as Done", "Loop", "Final")
// flow.Start(":8080")
data := []byte(`[{"age": "15", "gender": "female"}, {"age": "18", "gender": "male"}]`)
if flow.Error != nil {
panic(flow.Error)
}
rs := flow.Process(context.Background(), data)
if rs.Error != nil {
panic(rs.Error)
}
fmt.Println(rs.Status, rs.Topic, string(rs.Payload))
}
type GetData struct {
dag.Operation
}
func (p *GetData) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return mq.Result{Ctx: ctx, Payload: task.Payload}
}
type Loop struct {
dag.Operation
}
func (p *Loop) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return mq.Result{Ctx: ctx, Payload: task.Payload}
}
type ValidateAge struct {
dag.Operation
}
func (p *ValidateAge) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("ValidateAge Error: %s", err.Error()), Ctx: ctx}
}
var status string
if data["age"] == "18" {
status = "pass"
} else {
status = "default"
}
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx, ConditionStatus: status}
}
type ValidateGender struct {
dag.Operation
}
func (p *ValidateGender) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("ValidateGender Error: %s", err.Error()), Ctx: ctx}
}
data["female_voter"] = data["gender"] == "female"
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type Final struct {
dag.Operation
}
func (p *Final) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data []map[string]any
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: fmt.Errorf("Final Error: %s", err.Error()), Ctx: ctx}
}
for i, row := range data {
row["done"] = true
data[i] = row
}
updatedPayload, err := json.Marshal(data)
if err != nil {
panic(err)
}
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}

View File

@@ -1,35 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
d := dag.NewDAG("Sample DAG", "sample-dag", func(taskID string, result mq.Result) {
fmt.Println("Final", string(result.Payload))
},
mq.WithSyncMode(true),
mq.WithNotifyResponse(tasks.NotifyResponse),
)
d.AddNode(dag.Function, "C", "C", &tasks.Node3{}, true)
d.AddNode(dag.Function, "D", "D", &tasks.Node4{})
d.AddNode(dag.Function, "E", "E", &tasks.Node5{})
d.AddNode(dag.Function, "F", "F", &tasks.Node6{})
d.AddNode(dag.Function, "G", "G", &tasks.Node7{})
d.AddNode(dag.Function, "H", "H", &tasks.Node8{})
d.AddCondition("C", map[string]string{"PASS": "D", "FAIL": "E"})
d.AddEdge(dag.Simple, "Label 1", "B", "C")
d.AddEdge(dag.Simple, "Label 2", "D", "F")
d.AddEdge(dag.Simple, "Label 3", "E", "F")
d.AddEdge(dag.Simple, "Label 4", "F", "G", "H")
d.AssignTopic("queue")
err := d.Consume(context.Background())
if err != nil {
panic(err)
}
}

View File

@@ -1,827 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/handlers"
)
func main() {
fmt.Println("=== Data Transformation Handlers Examples ===")
// Test each handler with sample data
testFormatHandler()
testGroupHandler()
testSplitJoinHandler()
testFlattenHandler()
testJSONHandler()
testFieldHandler()
testDataHandler()
// Example of chaining handlers
exampleDAGChaining()
}
func testFormatHandler() {
fmt.Println("\n1. FORMAT HANDLER TESTS")
fmt.Println("========================")
// Test uppercase formatting
testData := map[string]any{
"name": "john doe",
"title": "software engineer",
"age": 30,
}
handler := handlers.NewFormatHandler("format-test")
config := dag.Payload{
Data: map[string]any{
"format_type": "uppercase",
"fields": []string{"name", "title"},
},
}
handler.SetConfig(config)
result := runHandler(handler, testData, "Uppercase Format")
printResult("Uppercase formatting", result)
printRequestConfigResult(testData, config, result)
// Test currency formatting
currencyData := map[string]any{
"price": 99.99,
"tax": "15.50",
"total": 115.49,
}
currencyHandler := handlers.NewFormatHandler("currency-test")
currencyConfig := dag.Payload{
Data: map[string]any{
"format_type": "currency",
"fields": []string{"price", "tax", "total"},
"currency": "$",
},
}
currencyHandler.SetConfig(currencyConfig)
result = runHandler(currencyHandler, currencyData, "Currency Format")
printResult("Currency formatting", result)
printRequestConfigResult(currencyData, currencyConfig, result)
// Test date formatting
dateData := map[string]any{
"created_at": "2023-06-15T10:30:00Z",
"updated_at": "2023-06-20",
}
dateHandler := handlers.NewFormatHandler("date-test")
dateConfig := dag.Payload{
Data: map[string]any{
"format_type": "date",
"fields": []string{"created_at", "updated_at"},
"date_format": "2006-01-02",
},
}
dateHandler.SetConfig(dateConfig)
result = runHandler(dateHandler, dateData, "Date Format")
printResult("Date formatting", result)
printRequestConfigResult(dateData, dateConfig, result)
}
func testGroupHandler() {
fmt.Println("\n2. GROUP HANDLER TESTS")
fmt.Println("======================")
// Test data grouping with aggregation
testData := map[string]any{
"data": []interface{}{
map[string]any{"department": "Engineering", "salary": 80000, "age": 30, "name": "John"},
map[string]any{"department": "Engineering", "salary": 90000, "age": 25, "name": "Jane"},
map[string]any{"department": "Marketing", "salary": 60000, "age": 35, "name": "Bob"},
map[string]any{"department": "Marketing", "salary": 65000, "age": 28, "name": "Alice"},
map[string]any{"department": "Engineering", "salary": 95000, "age": 32, "name": "Mike"},
},
}
handler := handlers.NewGroupHandler("group-test")
config := dag.Payload{
Data: map[string]any{
"group_by": []string{"department"},
"aggregations": map[string]any{
"salary": "sum",
"age": "avg",
"name": "concat",
},
"concat_separator": ", ",
},
}
handler.SetConfig(config)
result := runHandler(handler, testData, "Group by Department")
printResult("Data grouping", result)
printRequestConfigResult(testData, config, result)
}
func testSplitJoinHandler() {
fmt.Println("\n3. SPLIT/JOIN HANDLER TESTS")
fmt.Println("============================")
// Test split operation
testData := map[string]any{
"full_name": "John Michael Doe",
"tags": "go,programming,backend,api",
"skills": "golang python javascript",
}
splitHandler := handlers.NewSplitHandler("split-test")
splitConfig := dag.Payload{
Data: map[string]any{
"operation": "split",
"fields": []string{"full_name", "skills"},
"separator": " ",
},
}
splitHandler.SetConfig(splitConfig)
result := runHandler(splitHandler, testData, "Split Operation (space)")
printResult("String splitting with space", result)
printRequestConfigResult(testData, splitConfig, result)
// Test split with comma
splitHandler2 := handlers.NewSplitHandler("split-test-2")
splitConfig2 := dag.Payload{
Data: map[string]any{
"operation": "split",
"fields": []string{"tags"},
"separator": ",",
},
}
splitHandler2.SetConfig(splitConfig2)
result = runHandler(splitHandler2, testData, "Split Operation (comma)")
printResult("String splitting with comma", result)
printRequestConfigResult(testData, splitConfig2, result)
// Test join operation
joinData := map[string]any{
"first_name": "John",
"middle_name": "Michael",
"last_name": "Doe",
"title": "Mr.",
}
joinHandler := handlers.NewJoinHandler("join-test")
joinConfig := dag.Payload{
Data: map[string]any{
"operation": "join",
"source_fields": []string{"title", "first_name", "middle_name", "last_name"},
"target_field": "full_name_with_title",
"separator": " ",
},
}
joinHandler.SetConfig(joinConfig)
result = runHandler(joinHandler, joinData, "Join Operation")
printResult("String joining", result)
printRequestConfigResult(joinData, joinConfig, result)
fmt.Printf("Split Test Data: %+v\n", testData)
fmt.Printf("Split Config: %+v\n", splitConfig.Data)
fmt.Printf("Split Result: %+v\n", result)
fmt.Printf("Split Test Data (comma): %+v\n", testData)
fmt.Printf("Split Config (comma): %+v\n", splitConfig2.Data)
fmt.Printf("Split Result (comma): %+v\n", result)
fmt.Printf("Join Test Data: %+v\n", joinData)
fmt.Printf("Join Config: %+v\n", joinConfig.Data)
fmt.Printf("Join Result: %+v\n", result)
}
func testFlattenHandler() {
fmt.Println("\n4. FLATTEN HANDLER TESTS")
fmt.Println("=========================")
// Test flatten settings
testData := map[string]any{
"user_id": 123,
"settings": []interface{}{
map[string]any{"key": "theme", "value": "dark", "value_type": "string"},
map[string]any{"key": "notifications", "value": "true", "value_type": "boolean"},
map[string]any{"key": "max_items", "value": "50", "value_type": "integer"},
map[string]any{"key": "timeout", "value": "30.5", "value_type": "float"},
},
}
handler := handlers.NewFlattenHandler("flatten-test")
config := dag.Payload{
Data: map[string]any{
"operation": "flatten_settings",
"source_field": "settings",
"target_field": "user_config",
},
}
handler.SetConfig(config)
result := runHandler(handler, testData, "Flatten Settings")
printResult("Settings flattening", result)
printRequestConfigResult(testData, config, result)
// Test flatten key-value pairs
kvData := map[string]any{
"user_id": 456,
"properties": []interface{}{
map[string]any{"name": "color", "val": "blue"},
map[string]any{"name": "size", "val": "large"},
map[string]any{"name": "weight", "val": "heavy"},
},
}
kvHandler := handlers.NewFlattenHandler("kv-test")
kvConfig := dag.Payload{
Data: map[string]any{
"operation": "flatten_key_value",
"source_field": "properties",
"key_field": "name",
"value_field": "val",
"target_field": "flattened_props",
},
}
kvHandler.SetConfig(kvConfig)
result = runHandler(kvHandler, kvData, "Flatten Key-Value")
printResult("Key-value flattening", result)
printRequestConfigResult(kvData, kvConfig, result)
// Test flatten nested objects
nestedData := map[string]any{
"user": map[string]any{
"id": 123,
"profile": map[string]any{
"name": "John Doe",
"email": "john@example.com",
"address": map[string]any{
"street": "123 Main St",
"city": "New York",
"country": "USA",
},
"preferences": map[string]any{
"theme": "dark",
"language": "en",
},
},
},
}
nestedHandler := handlers.NewFlattenHandler("nested-test")
nestedConfig := dag.Payload{
Data: map[string]any{
"operation": "flatten_nested_objects",
"separator": "_",
},
}
nestedHandler.SetConfig(nestedConfig)
result = runHandler(nestedHandler, nestedData, "Flatten Nested Objects")
printResult("Nested object flattening", result)
printRequestConfigResult(nestedData, nestedConfig, result)
}
func testJSONHandler() {
fmt.Println("\n5. JSON HANDLER TESTS")
fmt.Println("=====================")
// Test JSON parsing
testData := map[string]any{
"config": `{"theme": "dark", "language": "en", "notifications": true, "max_items": 100}`,
"metadata": `["tag1", "tag2", "tag3"]`,
"user": `{"id": 123, "name": "John Doe", "active": true}`,
}
parseHandler := handlers.NewJSONHandler("json-parse-test")
parseConfig := dag.Payload{
Data: map[string]any{
"operation": "parse",
"fields": []string{"config", "metadata", "user"},
},
}
parseHandler.SetConfig(parseConfig)
result := runHandler(parseHandler, testData, "JSON Parsing")
printResult("JSON parsing", result)
printRequestConfigResult(testData, parseConfig, result)
// Test JSON stringifying
objData := map[string]any{
"user": map[string]any{
"id": 123,
"name": "John Doe",
"active": true,
"roles": []string{"admin", "user"},
},
"preferences": map[string]any{
"theme": "dark",
"notifications": true,
"language": "en",
},
}
stringifyHandler := handlers.NewJSONHandler("json-stringify-test")
stringifyConfig := dag.Payload{
Data: map[string]any{
"operation": "stringify",
"fields": []string{"user", "preferences"},
"indent": true,
},
}
stringifyHandler.SetConfig(stringifyConfig)
result = runHandler(stringifyHandler, objData, "JSON Stringifying")
printResult("JSON stringifying", result)
printRequestConfigResult(objData, stringifyConfig, result)
// Test JSON validation
validationData := map[string]any{
"valid_json": `{"key": "value"}`,
"invalid_json": `{"key": value}`, // Missing quotes around value
"valid_array": `[1, 2, 3]`,
}
validateHandler := handlers.NewJSONHandler("json-validate-test")
validateConfig := dag.Payload{
Data: map[string]any{
"operation": "validate",
"fields": []string{"valid_json", "invalid_json", "valid_array"},
},
}
validateHandler.SetConfig(validateConfig)
result = runHandler(validateHandler, validationData, "JSON Validation")
printResult("JSON validation", result)
printRequestConfigResult(validationData, validateConfig, result)
}
func testFieldHandler() {
fmt.Println("\n6. FIELD HANDLER TESTS")
fmt.Println("======================")
testData := map[string]any{
"id": 123,
"first_name": "John",
"last_name": "Doe",
"email_addr": "john@example.com",
"phone_number": "555-1234",
"internal_id": "INT-123",
"created_at": "2023-01-15",
"updated_at": "2023-06-20",
"is_active": true,
"salary": 75000.50,
}
// Test field filtering/selection
filterHandler := handlers.NewFieldHandler("filter-test")
filterConfig := dag.Payload{
Data: map[string]any{
"operation": "filter",
"fields": []string{"id", "first_name", "last_name", "email_addr", "is_active"},
},
}
filterHandler.SetConfig(filterConfig)
result := runHandler(filterHandler, testData, "Filter/Select Fields")
printResult("Field filtering", result)
printRequestConfigResult(testData, filterConfig, result)
// Test field exclusion/removal
excludeHandler := handlers.NewFieldHandler("exclude-test")
excludeConfig := dag.Payload{
Data: map[string]any{
"operation": "exclude",
"fields": []string{"internal_id", "created_at", "updated_at"},
},
}
excludeHandler.SetConfig(excludeConfig)
result = runHandler(excludeHandler, testData, "Exclude Fields")
printResult("Field exclusion", result)
printRequestConfigResult(testData, excludeConfig, result)
// Test field renaming
renameHandler := handlers.NewFieldHandler("rename-test")
renameConfig := dag.Payload{
Data: map[string]any{
"operation": "rename",
"mapping": map[string]any{
"first_name": "firstName",
"last_name": "lastName",
"email_addr": "email",
"phone_number": "phone",
"created_at": "createdAt",
"updated_at": "updatedAt",
"is_active": "active",
},
},
}
renameHandler.SetConfig(renameConfig)
result = runHandler(renameHandler, testData, "Rename Fields")
printResult("Field renaming", result)
printRequestConfigResult(testData, renameConfig, result)
// Test adding new fields
addHandler := handlers.NewFieldHandler("add-test")
addConfig := dag.Payload{
Data: map[string]any{
"operation": "add",
"new_fields": map[string]any{
"status": "active",
"version": "1.0",
"is_verified": true,
"last_login": "2023-06-20T10:30:00Z",
"department": "Engineering",
"access_level": 3,
},
},
}
addHandler.SetConfig(addConfig)
result = runHandler(addHandler, testData, "Add Fields")
printResult("Adding fields", result)
printRequestConfigResult(testData, addConfig, result)
// Test field copying
copyHandler := handlers.NewFieldHandler("copy-test")
copyConfig := dag.Payload{
Data: map[string]any{
"operation": "copy",
"mapping": map[string]any{
"first_name": "display_name",
"email_addr": "contact_email",
"id": "user_id",
},
},
}
copyHandler.SetConfig(copyConfig)
result = runHandler(copyHandler, testData, "Copy Fields")
printResult("Field copying", result)
printRequestConfigResult(testData, copyConfig, result)
// Test key transformation
transformHandler := handlers.NewFieldHandler("transform-test")
transformConfig := dag.Payload{
Data: map[string]any{
"operation": "transform_keys",
"transformation": "snake_case",
},
}
transformHandler.SetConfig(transformConfig)
result = runHandler(transformHandler, testData, "Transform Keys")
printResult("Key transformation", result)
printRequestConfigResult(testData, transformConfig, result)
}
func testDataHandler() {
fmt.Println("\n7. DATA HANDLER TESTS")
fmt.Println("=====================")
// Test data sorting
testData := map[string]any{
"data": []interface{}{
map[string]any{"name": "John", "age": 30, "salary": 80000, "department": "Engineering"},
map[string]any{"name": "Jane", "age": 25, "salary": 90000, "department": "Engineering"},
map[string]any{"name": "Bob", "age": 35, "salary": 75000, "department": "Marketing"},
map[string]any{"name": "Alice", "age": 28, "salary": 85000, "department": "Marketing"},
},
}
sortHandler := handlers.NewDataHandler("sort-test")
sortConfig := dag.Payload{
Data: map[string]any{
"operation": "sort",
"sort_field": "salary",
"sort_order": "desc",
},
}
sortHandler.SetConfig(sortConfig)
result := runHandler(sortHandler, testData, "Sort Data by Salary (Desc)")
printResult("Data sorting", result)
printRequestConfigResult(testData, sortConfig, result)
// Test field calculations
calcData := map[string]any{
"base_price": 100.0,
"tax_rate": 0.15,
"shipping_cost": 10.0,
"discount": 5.0,
"quantity": 2,
}
calcHandler := handlers.NewDataHandler("calc-test")
calcConfig := dag.Payload{
Data: map[string]any{
"operation": "calculate",
"calculations": map[string]any{
"tax_amount": map[string]any{
"operation": "multiply",
"fields": []string{"base_price", "tax_rate"},
},
"subtotal": map[string]any{
"operation": "sum",
"fields": []string{"base_price", "tax_amount", "shipping_cost"},
},
"total": map[string]any{
"operation": "subtract",
"fields": []string{"subtotal", "discount"},
},
"grand_total": map[string]any{
"operation": "multiply",
"fields": []string{"total", "quantity"},
},
},
},
}
calcHandler.SetConfig(calcConfig)
result = runHandler(calcHandler, calcData, "Field Calculations")
printResult("Field calculations", result)
printRequestConfigResult(calcData, calcConfig, result)
// Test data deduplication
dupData := map[string]any{
"data": []interface{}{
map[string]any{"email": "john@example.com", "name": "John Doe", "id": 1},
map[string]any{"email": "jane@example.com", "name": "Jane Smith", "id": 2},
map[string]any{"email": "john@example.com", "name": "John D.", "id": 3}, // duplicate email
map[string]any{"email": "bob@example.com", "name": "Bob Jones", "id": 4},
map[string]any{"email": "jane@example.com", "name": "Jane S.", "id": 5}, // duplicate email
},
}
dedupHandler := handlers.NewDataHandler("dedup-test")
dedupConfig := dag.Payload{
Data: map[string]any{
"operation": "deduplicate",
"dedupe_fields": []string{"email"},
},
}
dedupHandler.SetConfig(dedupConfig)
result = runHandler(dedupHandler, dupData, "Data Deduplication")
printResult("Data deduplication", result)
printRequestConfigResult(dupData, dedupConfig, result)
// Test type casting
castData := map[string]any{
"user_id": "123",
"age": "30",
"salary": "75000.50",
"is_active": "true",
"score": "95.5",
"name": 123,
"is_verified": "false",
}
castHandler := handlers.NewDataHandler("cast-test")
castConfig := dag.Payload{
Data: map[string]any{
"operation": "type_cast",
"cast": map[string]any{
"user_id": "int",
"age": "int",
"salary": "float",
"is_active": "bool",
"score": "float",
"name": "string",
"is_verified": "bool",
},
},
}
castHandler.SetConfig(castConfig)
result = runHandler(castHandler, castData, "Type Casting")
printResult("Type casting", result)
printRequestConfigResult(castData, castConfig, result)
// Test conditional field setting
condData := map[string]any{
"age": 25,
"salary": 60000,
"years_experience": 3,
}
condHandler := handlers.NewDataHandler("conditional-test")
condConfig := dag.Payload{
Data: map[string]any{
"operation": "conditional_set",
"conditions": map[string]any{
"salary_level": map[string]any{
"condition": "salary > 70000",
"if_true": "high",
"if_false": "standard",
},
"experience_level": map[string]any{
"condition": "years_experience >= 5",
"if_true": "senior",
"if_false": "junior",
},
},
},
}
condHandler.SetConfig(condConfig)
result = runHandler(condHandler, condData, "Conditional Field Setting")
printResult("Conditional setting", result)
printRequestConfigResult(condData, condConfig, result)
}
// Helper functions
func runHandler(handler dag.Processor, data map[string]any, description string) map[string]any {
fmt.Printf("\n--- Testing: %s ---\n", description)
// Convert data to JSON payload
payload, err := json.Marshal(data)
if err != nil {
log.Printf("Error marshaling test data: %v", err)
return nil
}
// Create a task
task := &mq.Task{
ID: mq.NewID(),
Payload: payload,
}
// Process the task
ctx := context.Background()
result := handler.ProcessTask(ctx, task)
if result.Error != nil {
log.Printf("Handler error: %v", result.Error)
return nil
}
// Parse result payload
var resultData map[string]any
if err := json.Unmarshal(result.Payload, &resultData); err != nil {
log.Printf("Error unmarshaling result: %v", err)
return nil
}
return resultData
}
func printResult(operation string, result map[string]any) {
if result == nil {
fmt.Printf("❌ %s failed\n", operation)
return
}
fmt.Printf("✅ %s succeeded\n", operation)
// Pretty print the result (truncated for readability)
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
fmt.Printf("Error formatting result: %v\n", err)
return
}
// Truncate very long results
resultStr := string(resultJSON)
if len(resultStr) > 1000 {
resultStr = resultStr[:997] + "..."
}
}
func printRequestConfigResult(requestData map[string]any, config dag.Payload, result map[string]any) {
fmt.Println("\n=== Request Data ===")
requestJSON, err := json.MarshalIndent(requestData, "", " ")
if err != nil {
fmt.Printf("Error formatting request data: %v\n", err)
} else {
fmt.Println(string(requestJSON))
}
fmt.Println("\n=== Configuration ===")
configJSON, err := json.MarshalIndent(config.Data, "", " ")
if err != nil {
fmt.Printf("Error formatting configuration: %v\n", err)
} else {
fmt.Println(string(configJSON))
}
fmt.Println("\n=== Result ===")
if result == nil {
fmt.Println("❌ Operation failed")
} else {
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
fmt.Printf("Error formatting result: %v\n", err)
} else {
fmt.Println(string(resultJSON))
}
}
}
// Example of chaining handlers in a DAG workflow
func exampleDAGChaining() {
fmt.Println("\n=== CHAINING HANDLERS EXAMPLE ===")
fmt.Println("==================================")
// Sample input data with nested JSON and various formatting needs
inputData := map[string]any{
"user_data": `{"firstName": "john", "lastName": "doe", "age": "30", "salary": "75000.50", "isActive": "true"}`,
"metadata": `{"department": "engineering", "level": "senior", "skills": ["go", "python", "javascript"]}`,
}
fmt.Println("🔗 Chaining multiple handlers to transform data...")
fmt.Printf("Input data: %+v\n", inputData)
// Step 1: Parse JSON strings
jsonHandler := handlers.NewJSONHandler("json-step")
jsonConfig := dag.Payload{
Data: map[string]any{
"operation": "parse",
"fields": []string{"user_data", "metadata"},
},
}
jsonHandler.SetConfig(jsonConfig)
step1Result := runHandler(jsonHandler, inputData, "Step 1: Parse JSON strings")
if step1Result != nil {
// Step 2: Flatten the parsed nested data
flattenHandler := handlers.NewFlattenHandler("flatten-step")
flattenConfig := dag.Payload{
Data: map[string]any{
"operation": "flatten_nested_objects",
"separator": "_",
},
}
flattenHandler.SetConfig(flattenConfig)
step2Result := runHandler(flattenHandler, step1Result, "Step 2: Flatten nested objects")
if step2Result != nil {
// Step 3: Format name fields to proper case
formatHandler := handlers.NewFormatHandler("format-step")
formatConfig := dag.Payload{
Data: map[string]any{
"format_type": "capitalize",
"fields": []string{"user_data_parsed_firstName", "user_data_parsed_lastName"},
},
}
formatHandler.SetConfig(formatConfig)
step3Result := runHandler(formatHandler, step2Result, "Step 3: Format names to proper case")
if step3Result != nil {
// Step 4: Rename fields to standard naming
fieldHandler := handlers.NewFieldHandler("rename-step")
renameConfig := dag.Payload{
Data: map[string]any{
"operation": "rename",
"mapping": map[string]any{
"user_data_parsed_firstName": "first_name",
"user_data_parsed_lastName": "last_name",
"user_data_parsed_age": "age",
"user_data_parsed_salary": "salary",
"user_data_parsed_isActive": "is_active",
"metadata_parsed_department": "department",
"metadata_parsed_level": "level",
},
},
}
fieldHandler.SetConfig(renameConfig)
step4Result := runHandler(fieldHandler, step3Result, "Step 4: Rename fields")
if step4Result != nil {
// Step 5: Cast data types
dataHandler := handlers.NewDataHandler("cast-step")
castConfig := dag.Payload{
Data: map[string]any{
"operation": "type_cast",
"cast": map[string]any{
"age": "int",
"salary": "float",
"is_active": "bool",
},
},
}
dataHandler.SetConfig(castConfig)
finalResult := runHandler(dataHandler, step4Result, "Step 5: Cast data types")
printResult("🎉 Final chained transformation result", finalResult)
}
}
}
}
}

View File

@@ -1,58 +0,0 @@
package main
import (
"context"
"fmt"
"time"
"github.com/oarkflow/json"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
handler := tasks.SchedulerHandler
callback := tasks.SchedulerCallback
// Initialize the pool with various parameters.
pool := mq.NewPool(3,
mq.WithTaskQueueSize(5),
mq.WithMaxMemoryLoad(1000),
mq.WithHandler(handler),
mq.WithPoolCallback(callback),
mq.WithTaskStorage(mq.NewMemoryTaskStorage(10*time.Minute)),
)
scheduler := mq.NewScheduler(pool)
scheduler.Start()
ctx := context.Background()
// Example: Schedule an email task with deduplication.
// DedupKey here is set to the recipient's email (e.g., "user@example.com") to avoid duplicate email tasks.
emailPayload := json.RawMessage(`{"email": "user@example.com", "message": "Hello, Customer!"}`)
scheduler.AddTask(ctx, mq.NewTask("Email Task", emailPayload, "email",
mq.WithDedupKey("user@example.com"),
),
mq.WithScheduleSpec("@every 1m"), // runs every minute for demonstration
mq.WithRecurring(),
)
scheduler.AddTask(ctx, mq.NewTask("Duplicate Email Task", emailPayload, "email",
mq.WithDedupKey("user@example.com"),
),
mq.WithScheduleSpec("@every 1m"),
mq.WithRecurring(),
)
go func() {
for {
for _, task := range scheduler.ListScheduledTasks() {
fmt.Println("Scheduled.....", task)
}
time.Sleep(1 * time.Minute)
}
}()
time.Sleep(10 * time.Minute)
}

View File

@@ -1,190 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Contact Us - Email Notification System</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 700px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
min-height: 100vh;
}
.form-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);
border: 1px solid rgba(255, 255, 255, 0.2);
}
h1 {
text-align: center;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
font-size: 2.2em;
}
.subtitle {
text-align: center;
margin-bottom: 30px;
opacity: 0.9;
font-size: 1.1em;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
font-size: 1.1em;
}
input,
textarea,
select {
width: 100%;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 16px;
background: rgba(255, 255, 255, 0.2);
color: white;
backdrop-filter: blur(5px);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.3);
}
input:focus,
textarea:focus,
select:focus {
outline: none;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
input::placeholder,
textarea::placeholder {
color: rgba(255, 255, 255, 0.7);
}
textarea {
height: 120px;
resize: vertical;
}
select {
cursor: pointer;
}
select option {
background: #2a5298;
color: white;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
button {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
color: white;
padding: 18px 40px;
border: none;
border-radius: 30px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
width: 100%;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
text-transform: uppercase;
letter-spacing: 1px;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
}
.info-box {
background: rgba(255, 255, 255, 0.15);
padding: 20px;
border-radius: 12px;
margin-bottom: 25px;
text-align: center;
border-left: 4px solid #4ECDC4;
}
.feature-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.feature-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
text-align: center;
font-size: 14px;
}
</style>
</head>
<body>
<div class="form-container">
<h1>📧 Contact Us</h1>
<div class="subtitle">Advanced Email Notification System with DAG Workflow</div>
<div class="info-box">
<p><strong>🔄 Smart Routing:</strong> Our system automatically routes your message based on your user type
and preferences.</p>
</div>
<div class="feature-list">
<div class="feature-item">
<strong>📱 Instant Notifications</strong><br>
Real-time email delivery
</div>
<div class="feature-item">
<strong>🎯 Smart Targeting</strong><br>
User-specific content
</div>
<div class="feature-item">
<strong>🔒 Secure Processing</strong><br>
Enterprise-grade security
</div>
</div>
<form {{form_attributes}}>
<div class="form-container p-4 bg-white shadow-md rounded">
{{form_groups}}
<div class="mt-4 flex flex-col items-center gap-2">
{{form_buttons}}
</div>
</div>
</form>
</div>
</body>
</html>

View File

@@ -1,134 +0,0 @@
<!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

@@ -1,210 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Message Sent Successfully</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 700px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
}
.result-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;
}
.success-icon {
font-size: 80px;
margin-bottom: 20px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
h1 {
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
font-size: 2.5em;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
text-align: left;
}
.info-item {
background: rgba(255, 255, 255, 0.15);
padding: 20px;
border-radius: 12px;
border-left: 4px solid #4ECDC4;
}
.info-label {
font-weight: bold;
margin-bottom: 8px;
opacity: 0.9;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.info-value {
font-size: 16px;
word-break: break-word;
}
.message-preview {
background: rgba(255, 255, 255, 0.1);
padding: 25px;
border-radius: 12px;
margin: 30px 0;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.actions {
margin-top: 40px;
}
.btn {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
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);
}
.status-badge {
background: #4CAF50;
color: white;
padding: 8px 20px;
border-radius: 25px;
font-size: 14px;
font-weight: bold;
display: inline-block;
margin: 10px 0;
text-transform: uppercase;
letter-spacing: 1px;
}
.workflow-info {
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 12px;
margin-top: 30px;
font-size: 14px;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="result-container">
<div class="success-icon"></div>
<h1>Message Sent Successfully!</h1>
<div class="status-badge">{{email_status}}</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">👤 Recipient</div>
<div class="info-value">{{full_name}}</div>
</div>
<div class="info-item">
<div class="info-label">📧 Email Address</div>
<div class="info-value">{{email}}</div>
</div>
<div class="info-item">
<div class="info-label">🆔 Email ID</div>
<div class="info-value">{{email_id}}</div>
</div>
<div class="info-item">
<div class="info-label">⏰ Sent At</div>
<div class="info-value">{{sent_at}}</div>
</div>
<div class="info-item">
<div class="info-label">📨 Email Type</div>
<div class="info-value">{{email_type}}</div>
</div>
<div class="info-item">
<div class="info-label">👥 User Type</div>
<div class="info-value">{{user_type}}</div>
</div>
<div class="info-item">
<div class="info-label">🚨 Priority</div>
<div class="info-value">{{priority}}</div>
</div>
<div class="info-item">
<div class="info-label">🚚 Delivery</div>
<div class="info-value">{{delivery_estimate}}</div>
</div>
</div>
<div class="message-preview">
<div class="info-label">📋 Subject:</div>
<div class="info-value" style="margin: 10px 0; font-weight: bold; font-size: 18px;">
{{subject}}
</div>
<div class="info-label">💬 Message ({{message_length}} chars):</div>
<div class="info-value" style="margin-top: 15px; font-style: italic; line-height: 1.6;">
"{{message}}"
</div>
</div>
<div class="actions">
<a href="/" class="btn">📧 Send Another Message</a>
<a href="/api/metrics" class="btn">📊 View Metrics</a>
</div>
<div class="workflow-info">
<strong>🔄 Workflow Details:</strong><br>
Gateway: {{gateway}} | Template: {{email_template}} | Processed: {{processed_at}}<br>
This message was processed through our advanced DAG workflow system with conditional routing.
</div>
</div>
</body>
</html>

View File

@@ -1,460 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/oarkflow/json"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/handlers"
"github.com/oarkflow/mq/utils"
"github.com/oarkflow/jet"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/consts"
)
type ValidateLoginNode struct {
dag.Operation
}
func (v *ValidateLoginNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: fmt.Errorf("invalid input data: %v", err), Ctx: ctx}
}
username, _ := inputData["username"].(string)
password, _ := inputData["password"].(string)
if username == "" || password == "" {
inputData["validation_error"] = "Username and password are required"
inputData["error_field"] = "credentials"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Simulate user validation
if username == "admin" && password == "password" {
inputData["validation_status"] = "success"
} else {
inputData["validation_error"] = "Invalid username or password"
inputData["error_field"] = "credentials"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
validatedData := map[string]any{
"username": username,
"validated_at": time.Now().Format("2006-01-02 15:04:05"),
"validation_status": "success",
}
bt, _ := json.Marshal(validatedData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"}
}
func loginDAG() *dag.DAG {
flow := dag.NewDAG("Login Flow", "login-flow", func(taskID string, result mq.Result) {
fmt.Printf("Login flow completed for task %s: %s\n", taskID, string(result.Payload))
}, mq.WithSyncMode(true), mq.WithLogger(nil))
renderHTML := handlers.NewRenderHTMLNode("render-html")
renderHTML.Payload.Data = map[string]any{
"schema_file": "login.json",
"template_file": "app/templates/basic.html",
}
flow.AddNode(dag.Page, "Login Form", "LoginForm", renderHTML, true)
flow.AddNode(dag.Function, "Validate Login", "ValidateLogin", &ValidateLoginNode{})
flow.AddNode(dag.Page, "Error Page", "ErrorPage", &EmailErrorPageNode{})
flow.AddNode(dag.Function, "Output", "Output", &handlers.OutputHandler{})
flow.AddEdge(dag.Simple, "Validate Login", "LoginForm", "ValidateLogin")
flow.AddCondition("ValidateLogin", map[string]string{
"invalid": "ErrorPage",
"valid": "Output",
})
return flow
}
func main() {
flow := dag.NewDAG("Email Notification System", "email-notification", func(taskID string, result mq.Result) {
fmt.Printf("Email notification workflow completed for task %s: %s\n", taskID, string(utils.RemoveRecursiveFromJSON(result.Payload, "html_content")))
}, mq.WithSyncMode(true), mq.WithLogger(nil))
renderHTML := handlers.NewRenderHTMLNode("render-html")
renderHTML.Payload.Data = map[string]any{
"schema_file": "schema.json",
"template_file": "app/templates/basic.html",
}
flow.AddDAGNode(dag.Page, "Check Login", "Login", loginDAG(), true)
flow.AddNode(dag.Page, "Contact Form", "ContactForm", renderHTML)
flow.AddNode(dag.Function, "Validate Contact Data", "ValidateContact", &ValidateContactNode{})
flow.AddNode(dag.Function, "Check User Type", "CheckUserType", &CheckUserTypeNode{})
flow.AddNode(dag.Function, "Send Welcome Email", "SendWelcomeEmail", &SendWelcomeEmailNode{})
flow.AddNode(dag.Function, "Send Premium Email", "SendPremiumEmail", &SendPremiumEmailNode{})
flow.AddNode(dag.Function, "Send Standard Email", "SendStandardEmail", &SendStandardEmailNode{})
flow.AddNode(dag.Page, "Success Page", "SuccessPage", &SuccessPageNode{})
flow.AddNode(dag.Page, "Error Page", "ErrorPage", &EmailErrorPageNode{})
flow.AddEdge(dag.Simple, "Login to Contact", "Login.Output", "ContactForm")
// Define conditional flow
flow.AddEdge(dag.Simple, "Form to Validation", "ContactForm", "ValidateContact")
flow.AddCondition("ValidateContact", map[string]string{
"valid": "CheckUserType",
"invalid": "ErrorPage",
})
flow.AddCondition("CheckUserType", map[string]string{
"new_user": "SendWelcomeEmail",
"premium_user": "SendPremiumEmail",
"standard_user": "SendStandardEmail",
})
flow.AddCondition("SendWelcomeEmail", map[string]string{
"sent": "SuccessPage",
"failed": "ErrorPage",
})
flow.AddCondition("SendPremiumEmail", map[string]string{
"sent": "SuccessPage",
"failed": "ErrorPage",
})
flow.AddCondition("SendStandardEmail", map[string]string{
"sent": "SuccessPage",
"failed": "ErrorPage",
})
// Start the flow
if flow.Error != nil {
panic(flow.Error)
}
fmt.Println("Starting Email Notification DAG server on http://0.0.0.0:8084")
fmt.Println("Navigate to the URL to access the contact form")
flow.Start(context.Background(), "0.0.0.0:8084")
}
// RenderHTMLNode - Page node with JSONSchema-based fields and custom HTML layout
// Usage: Pass JSONSchema and HTML layout to the node for dynamic form rendering and validation
// ValidateContactNode - Validates contact form data
type ValidateContactNode struct {
dag.Operation
}
func (v *ValidateContactNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{
Error: fmt.Errorf("invalid input data: %v", err),
Ctx: ctx,
}
}
// Extract form data
firstName, _ := inputData["first_name"].(string)
lastName, _ := inputData["last_name"].(string)
email, _ := inputData["email"].(string)
userType, _ := inputData["user_type"].(string)
priority, _ := inputData["priority"].(string)
subject, _ := inputData["subject"].(string)
message, _ := inputData["message"].(string)
// Validate required fields
if firstName == "" {
inputData["validation_error"] = "First name is required"
inputData["error_field"] = "first_name"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if lastName == "" {
inputData["validation_error"] = "Last name is required"
inputData["error_field"] = "last_name"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if email == "" {
inputData["validation_error"] = "Email address is required"
inputData["error_field"] = "email"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Validate email format
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(email) {
inputData["validation_error"] = "Please enter a valid email address"
inputData["error_field"] = "email"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if userType == "" {
inputData["validation_error"] = "Please select your user type"
inputData["error_field"] = "user_type"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if priority == "" {
inputData["validation_error"] = "Please select a priority level"
inputData["error_field"] = "priority"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if subject == "" {
inputData["validation_error"] = "Subject is required"
inputData["error_field"] = "subject"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if message == "" {
inputData["validation_error"] = "Message is required"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Check for spam patterns
spamPatterns := []string{"click here", "free money", "act now", "limited time"}
messageLower := strings.ToLower(message)
subjectLower := strings.ToLower(subject)
for _, pattern := range spamPatterns {
if strings.Contains(messageLower, pattern) || strings.Contains(subjectLower, pattern) {
inputData["validation_error"] = "Message contains prohibited content"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
}
// All validations passed
validatedData := map[string]any{
"first_name": firstName,
"last_name": lastName,
"full_name": fmt.Sprintf("%s %s", firstName, lastName),
"email": email,
"user_type": userType,
"priority": priority,
"subject": subject,
"message": message,
"validated_at": time.Now().Format("2006-01-02 15:04:05"),
"validation_status": "success",
"message_length": len(message),
}
bt, _ := json.Marshal(validatedData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"}
}
// CheckUserTypeNode - Determines routing based on user type
type CheckUserTypeNode struct {
dag.Operation
}
func (c *CheckUserTypeNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
userType, _ := inputData["user_type"].(string)
// Add timestamp and additional metadata
inputData["processed_at"] = time.Now().Format("2006-01-02 15:04:05")
inputData["routing_decision"] = userType
var conditionStatus string
switch userType {
case "new":
conditionStatus = "new_user"
inputData["email_template"] = "welcome"
case "premium":
conditionStatus = "premium_user"
inputData["email_template"] = "premium"
case "standard":
conditionStatus = "standard_user"
inputData["email_template"] = "standard"
default:
conditionStatus = "standard_user"
inputData["email_template"] = "standard"
}
fmt.Printf("🔀 Routing decision: %s -> %s\n", userType, conditionStatus)
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: conditionStatus}
}
// Email sending nodes
type SendWelcomeEmailNode struct {
dag.Operation
}
func (s *SendWelcomeEmailNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return s.sendEmail(ctx, task, "Welcome to our platform! 🎉")
}
type SendPremiumEmailNode struct {
dag.Operation
}
func (s *SendPremiumEmailNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return s.sendEmail(ctx, task, "Premium Support Response 💎")
}
type SendStandardEmailNode struct {
dag.Operation
}
func (s *SendStandardEmailNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return s.sendEmail(ctx, task, "Thank you for contacting us ⭐")
}
// Helper method for email sending
func (s *SendWelcomeEmailNode) sendEmail(ctx context.Context, task *mq.Task, emailType string) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
email, _ := inputData["email"].(string)
// Simulate email sending delay
time.Sleep(300 * time.Millisecond)
// Simulate occasional failures for demo purposes
timestamp := time.Now()
success := timestamp.Second()%15 != 0 // 93% success rate
if !success {
errorData := inputData
errorData["email_status"] = "failed"
errorData["error_message"] = "Email gateway temporarily unavailable. Please try again."
errorData["sent_at"] = timestamp.Format("2006-01-02 15:04:05")
errorData["retry_suggested"] = true
bt, _ := json.Marshal(errorData)
return mq.Result{
Payload: bt,
Ctx: ctx,
ConditionStatus: "failed",
}
}
// Generate mock email ID and response
emailID := fmt.Sprintf("EMAIL_%d_%s", timestamp.Unix(), email[0:3])
resultData := inputData
resultData["email_status"] = "sent"
resultData["email_id"] = emailID
resultData["email_type"] = emailType
resultData["sent_at"] = timestamp.Format("2006-01-02 15:04:05")
resultData["delivery_estimate"] = "Instant"
resultData["gateway"] = "MockEmail Gateway"
fmt.Printf("📧 Email sent successfully! Type: %s, ID: %s, To: %s\n", emailType, emailID, email)
bt, _ := json.Marshal(resultData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "sent"}
}
// Helper methods for other email nodes
func (s *SendPremiumEmailNode) sendEmail(ctx context.Context, task *mq.Task, emailType string) mq.Result {
node := &SendWelcomeEmailNode{}
return node.sendEmail(ctx, task, emailType)
}
func (s *SendStandardEmailNode) sendEmail(ctx context.Context, task *mq.Task, emailType string) mq.Result {
node := &SendWelcomeEmailNode{}
return node.sendEmail(ctx, task, emailType)
}
// SuccessPageNode - Shows successful email result
type SuccessPageNode struct {
dag.Operation
}
func (s *SuccessPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
htmlTemplate, err := os.ReadFile("email/success.html")
if err != nil {
return mq.Result{Error: fmt.Errorf("failed to read success template: %v", err)}
}
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(string(htmlTemplate), inputData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
finalData := map[string]any{
"html_content": rs,
"result": inputData,
"step": "success",
}
bt, _ := json.Marshal(finalData)
return mq.Result{Payload: bt, Ctx: ctx}
}
// EmailErrorPageNode - Shows validation or sending errors
type EmailErrorPageNode struct {
dag.Operation
}
func (e *EmailErrorPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
// Determine error type and message
errorMessage, _ := inputData["validation_error"].(string)
errorField, _ := inputData["error_field"].(string)
emailError, _ := inputData["error_message"].(string)
if errorMessage == "" && emailError != "" {
errorMessage = emailError
errorField = "email_sending"
}
if errorMessage == "" {
errorMessage = "An unknown error occurred"
}
htmlTemplate, err := os.ReadFile("email/error.html")
if err != nil {
return mq.Result{Error: fmt.Errorf("failed to read error template: %v", err)}
}
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
templateData := map[string]any{
"error_message": errorMessage,
"error_field": errorField,
"retry_suggested": inputData["retry_suggested"],
}
rs, err := parser.ParseTemplate(string(htmlTemplate), templateData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
finalData := map[string]any{
"html_content": rs,
"error_data": inputData,
"step": "error",
}
bt, _ := json.Marshal(finalData)
return mq.Result{Payload: bt, Ctx: ctx}
}

View File

@@ -1,164 +0,0 @@
// This is a demo showing the enhanced worker pool capabilities
// Run with: go run enhanced_pool_demo.go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/oarkflow/mq"
)
// DemoHandler demonstrates a simple task handler
func DemoHandler(ctx context.Context, task *mq.Task) mq.Result {
fmt.Printf("Processing task: %s\n", task.ID)
// Simulate some work
time.Sleep(100 * time.Millisecond)
return mq.Result{
TaskID: task.ID,
Status: mq.Completed,
Payload: task.Payload,
}
}
// DemoCallback demonstrates result processing
func DemoCallback(ctx context.Context, result mq.Result) error {
if result.Error != nil {
fmt.Printf("Task %s failed: %v\n", result.TaskID, result.Error)
} else {
fmt.Printf("Task %s completed successfully\n", result.TaskID)
}
return nil
}
func main() {
fmt.Println("=== Enhanced Worker Pool Demo ===")
// Create task storage
storage := mq.NewMemoryTaskStorage(1 * time.Hour)
// Configure circuit breaker
circuitBreaker := mq.CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 5,
ResetTimeout: 30 * time.Second,
}
// Create pool with enhanced configuration
pool := mq.NewPool(5,
mq.WithHandler(DemoHandler),
mq.WithPoolCallback(DemoCallback),
mq.WithTaskStorage(storage),
mq.WithBatchSize(3),
mq.WithMaxMemoryLoad(50*1024*1024), // 50MB
mq.WithCircuitBreaker(circuitBreaker),
)
fmt.Printf("Worker pool created with %d workers\n", 5)
// Enqueue some tasks
fmt.Println("\n=== Enqueueing Tasks ===")
for i := 0; i < 10; i++ {
task := mq.NewTask(
fmt.Sprintf("demo-task-%d", i),
[]byte(fmt.Sprintf(`{"message": "Hello from task %d", "timestamp": "%s"}`, i, time.Now().Format(time.RFC3339))),
"demo",
)
// Add some tasks with higher priority
priority := 1
if i%3 == 0 {
priority = 5 // Higher priority
}
err := pool.EnqueueTask(context.Background(), task, priority)
if err != nil {
log.Printf("Failed to enqueue task %d: %v", i, err)
} else {
fmt.Printf("Enqueued task %s with priority %d\n", task.ID, priority)
}
}
// Monitor progress
fmt.Println("\n=== Monitoring Progress ===")
for i := 0; i < 10; i++ {
time.Sleep(500 * time.Millisecond)
metrics := pool.FormattedMetrics()
health := pool.GetHealthStatus()
fmt.Printf("Progress: %d/%d completed, %d errors, Queue: %d, Healthy: %v\n",
metrics.CompletedTasks,
metrics.TotalTasks,
metrics.ErrorCount,
health.QueueDepth,
health.IsHealthy,
)
if metrics.CompletedTasks >= 10 {
break
}
}
// Display final metrics
fmt.Println("\n=== Final Metrics ===")
finalMetrics := pool.FormattedMetrics()
fmt.Printf("Total Tasks: %d\n", finalMetrics.TotalTasks)
fmt.Printf("Completed: %d\n", finalMetrics.CompletedTasks)
fmt.Printf("Errors: %d\n", finalMetrics.ErrorCount)
fmt.Printf("Memory Used: %s\n", finalMetrics.CurrentMemoryUsed)
fmt.Printf("Execution Time: %s\n", finalMetrics.CumulativeExecution)
fmt.Printf("Average Execution: %s\n", finalMetrics.AverageExecution)
// Test dynamic worker scaling
fmt.Println("\n=== Dynamic Scaling Demo ===")
fmt.Printf("Current workers: %d\n", 5)
pool.AdjustWorkerCount(8)
fmt.Printf("Scaled up to: %d workers\n", 8)
time.Sleep(1 * time.Second)
pool.AdjustWorkerCount(3)
fmt.Printf("Scaled down to: %d workers\n", 3)
// Test health status
fmt.Println("\n=== Health Status ===")
health := pool.GetHealthStatus()
fmt.Printf("Health Status: %+v\n", health)
// Test DLQ (simulate some failures)
fmt.Println("\n=== Dead Letter Queue Demo ===")
dlqTasks := pool.DLQ().Tasks()
fmt.Printf("Tasks in DLQ: %d\n", len(dlqTasks))
// Test configuration update
fmt.Println("\n=== Configuration Update Demo ===")
currentConfig := pool.GetCurrentConfig()
fmt.Printf("Current batch size: %d\n", currentConfig.BatchSize)
newConfig := currentConfig
newConfig.BatchSize = 5
newConfig.NumberOfWorkers = 4
err := pool.UpdateConfig(&newConfig)
if err != nil {
log.Printf("Failed to update config: %v", err)
} else {
fmt.Printf("Updated batch size to: %d\n", newConfig.BatchSize)
fmt.Printf("Updated worker count to: %d\n", newConfig.NumberOfWorkers)
}
// Graceful shutdown
fmt.Println("\n=== Graceful Shutdown ===")
fmt.Println("Shutting down pool...")
pool.Stop()
fmt.Println("Pool shutdown completed")
fmt.Println("\n=== Demo Complete ===")
}

View File

@@ -1,160 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/oarkflow/json"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/jet"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/consts"
)
func main() {
flow := dag.NewDAG("Multi-Step Form", "multi-step-form", func(taskID string, result mq.Result) {
fmt.Printf("Final result for task %s: %s\n", taskID, string(result.Payload))
})
flow.AddNode(dag.Page, "Form Step1", "FormStep1", &FormStep1{})
flow.AddNode(dag.Page, "Form Step2", "FormStep2", &FormStep2{})
flow.AddNode(dag.Page, "Form Result", "FormResult", &FormResult{})
// Define edges
flow.AddEdge(dag.Simple, "Form Step1", "FormStep1", "FormStep2")
flow.AddEdge(dag.Simple, "Form Step2", "FormStep2", "FormResult")
// Start the flow
if flow.Error != nil {
panic(flow.Error)
}
flow.Start(context.Background(), "0.0.0.0:8082")
}
type FormStep1 struct {
dag.Operation
}
func (p *FormStep1) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
bt := []byte(`
<html>
<body>
<form method="post" action="/process?task_id={{task_id}}&next=true">
<label>Name:</label>
<input type="text" name="name" required>
<label>Age:</label>
<input type="number" name="age" required>
<button type="submit">Next</button>
</form>
</body
</html
`)
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(string(bt), map[string]any{
"task_id": ctx.Value("task_id"),
})
if err != nil {
fmt.Println("FormStep1", string(task.Payload))
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
data := map[string]any{
"html_content": rs,
}
bt, _ = json.Marshal(data)
return mq.Result{Payload: bt, Ctx: ctx}
}
type FormStep2 struct {
dag.Operation
}
func (p *FormStep2) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// Parse input from Step 1
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
// Determine dynamic content
isEligible := inputData["age"] == "18"
inputData["show_voting_controls"] = isEligible
bt := []byte(`
<html>
<body>
<form method="post" action="/process?task_id={{task_id}}&next=true">
{{ if show_voting_controls }}
<label>Do you want to register to vote?</label>
<input type="checkbox" name="register_vote">
<button type="submit">Next</button>
{{ else }}
<p>You are not eligible to vote.</p>
{{ end }}
</form>
</body>
</html>
`)
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
inputData["task_id"] = ctx.Value("task_id")
rs, err := parser.ParseTemplate(string(bt), inputData)
if err != nil {
fmt.Println("FormStep2", inputData)
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
inputData["html_content"] = rs
bt, _ = json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx}
}
type FormResult struct {
dag.Operation
}
func (p *FormResult) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// Load HTML template for results
bt := []byte(`
<html>
<body>
<h1>Form Summary</h1>
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
{{ if register_vote }}
<p>You have registered to vote!</p>
{{ else }}
<p>You did not register to vote.</p>
{{ end }}
</body>
</html>
`)
var inputData map[string]any
if task.Payload != nil {
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
}
if inputData != nil {
if isEligible, ok := inputData["register_vote"].(string); ok {
inputData["register_vote"] = isEligible
} else {
inputData["register_vote"] = false
}
}
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(string(bt), inputData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
inputData["html_content"] = rs
bt, _ = json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx}
}

View File

@@ -1,44 +0,0 @@
{
"name": "Multi-Step Form",
"key": "multi-step-form",
"nodes": [
{
"name": "Form Step1",
"id": "FormStep1",
"node": "Page",
"data": {
"template": "<html><body><form method=\\\"post\\\" action=\\\"/process?task_id={{task_id}}&next=true\\\"><label>Name:</label><input type=\\\"text\\\" name=\\\"name\\\" required><label>Age:</label><input type=\\\"number\\\" name=\\\"age\\\" required><button type=\\\"submit\\\">Next</button></form></body></html>",
"mapping": {}
},
"first_node": true
},
{
"name": "Form Step2",
"id": "FormStep2",
"node": "Page",
"data": {
"template": "<html><body><form method=\\\"post\\\" action=\\\"/process?task_id={{task_id}}&next=true\\\">{{ if show_voting_controls }}<label>Do you want to register to vote?</label><input type=\\\"checkbox\\\" name=\\\"register_vote\\\"><button type=\\\"submit\\\">Next</button>{{ else }}<p>You are not eligible to vote.</p>{{ end }}</form></body></html>",
"mapping": {}
}
},
{
"name": "Form Result",
"id": "FormResult",
"node": "Page",
"data": {
"template": "<html><body><h1>Form Summary</h1><p>Name: {{ name }}</p><p>Age: {{ age }}</p>{{ if register_vote }}<p>You have registered to vote!</p>{{ else }}<p>You did not register to vote.</p>{{ end }}</body></html>",
"mapping": {}
}
}
],
"edges": [
{
"source": "FormStep1",
"target": ["FormStep2"]
},
{
"source": "FormStep2",
"target": ["FormResult"]
}
]
}

View File

@@ -1,468 +0,0 @@
package main
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"net"
"os"
"time"
"github.com/oarkflow/json"
)
func main() {
caCertDER, caPrivateKey := generateCA("P384", "My Secure CA")
saveCertificate("ca.crt", caCertDER)
signFileContent("ca.crt", caPrivateKey)
caCert, err := x509.ParseCertificate(caCertDER)
if err != nil {
log.Fatal("Failed to parse CA certificate:", err)
}
serverCertDER, serverKey := generateServerCert(caCert, caPrivateKey, "P256", "server.example.com",
[]string{"localhost", "server.example.com"}, []net.IP{net.ParseIP("127.0.0.1")})
saveCertificate("server.crt", serverCertDER)
signFileContent("server.crt", caPrivateKey)
clientCertDER, clientKey := generateClientCert(caCert, caPrivateKey, "client-user")
saveCertificate("client.crt", clientCertDER)
signFileContent("client.crt", caPrivateKey)
codeSignCertDER, codeSignKey := generateCodeSigningCert(caCert, caPrivateKey, 2048, "file-signer")
saveCertificate("code_sign.crt", codeSignCertDER)
signFileContent("code_sign.crt", caPrivateKey)
revokedCerts := []pkix.RevokedCertificate{
{SerialNumber: big.NewInt(1), RevocationTime: time.Now()},
}
generateCRL(caCert, caPrivateKey, revokedCerts)
if err := validateClientCert("client.crt", "ca.crt"); err != nil {
log.Fatal("Client certificate validation failed:", err)
}
log.Println("All certificates generated and signed successfully.")
dss := NewDigitalSignatureService(clientKey)
plainText := "The quick brown fox jumps over the lazy dog."
textSig, err := dss.SignText(plainText)
if err != nil {
log.Fatal("Failed to sign plain text:", err)
}
if err := dss.VerifyText(plainText, textSig); err != nil {
log.Fatal("Plain text signature verification failed:", err)
}
log.Println("Plain text signature verified.")
jsonData := map[string]interface{}{
"name": "Alice",
"age": 30,
"premium": true,
}
jsonSig, err := dss.SignJSON(jsonData)
if err != nil {
log.Fatal("Failed to sign JSON data:", err)
}
fmt.Println(string(jsonSig))
if err := dss.VerifyJSON(jsonData, jsonSig); err != nil {
log.Fatal("JSON signature verification failed:", err)
}
log.Println("JSON signature verified.")
_ = serverKey
_ = codeSignKey
}
type DigitalSignatureService struct {
Signer crypto.Signer
PublicKey crypto.PublicKey
}
func NewDigitalSignatureService(signer crypto.Signer) *DigitalSignatureService {
return &DigitalSignatureService{
Signer: signer,
PublicKey: signer.Public(),
}
}
func (dss *DigitalSignatureService) SignData(data []byte) ([]byte, error) {
switch dss.Signer.(type) {
case ed25519.PrivateKey:
return dss.Signer.Sign(rand.Reader, data, crypto.Hash(0))
default:
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
return dss.Signer.Sign(rand.Reader, hashed, crypto.SHA256)
}
}
func (dss *DigitalSignatureService) VerifyData(data, signature []byte) error {
switch pub := dss.PublicKey.(type) {
case ed25519.PublicKey:
if ed25519.Verify(pub, data, signature) {
return nil
}
return errors.New("ed25519 signature verification failed")
case *rsa.PublicKey:
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed, signature)
case *ecdsa.PublicKey:
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
var sig struct {
R, S *big.Int
}
if _, err := asn1.Unmarshal(signature, &sig); err != nil {
return err
}
if ecdsa.Verify(pub, hashed, sig.R, sig.S) {
return nil
}
return errors.New("ecdsa signature verification failed")
default:
return errors.New("unsupported public key type")
}
}
func (dss *DigitalSignatureService) SignText(text string) ([]byte, error) {
return dss.SignData([]byte(text))
}
func (dss *DigitalSignatureService) VerifyText(text string, signature []byte) error {
return dss.VerifyData([]byte(text), signature)
}
func (dss *DigitalSignatureService) SignJSON(v interface{}) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
return dss.SignData(b)
}
func (dss *DigitalSignatureService) VerifyJSON(v interface{}, signature []byte) error {
b, err := json.Marshal(v)
if err != nil {
return err
}
return dss.VerifyData(b, signature)
}
func generateCA(curveName string, commonName string) ([]byte, crypto.Signer) {
privKey, err := generatePrivateKey("ECDSA", curveName)
if err != nil {
log.Fatal("CA key generation failed:", err)
}
subject := pkix.Name{
CommonName: commonName,
Organization: []string{"Secure CA Org"},
Country: []string{"US"},
}
template := x509.Certificate{
SerialNumber: randomSerial(),
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
MaxPathLenZero: true,
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, privKey.Public(), privKey)
if err != nil {
log.Fatal("CA cert creation failed:", err)
}
return certBytes, privKey
}
func generateServerCert(caCert *x509.Certificate, caKey crypto.Signer, keyType string, commonName string, dnsNames []string, ips []net.IP) ([]byte, crypto.Signer) {
privKey, err := generatePrivateKey("ECDSA", keyType)
if err != nil {
log.Fatal("Server key generation failed:", err)
}
template := x509.Certificate{
SerialNumber: randomSerial(),
Subject: pkix.Name{
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: dnsNames,
IPAddresses: ips,
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, privKey.Public(), caKey)
if err != nil {
log.Fatal("Server cert creation failed:", err)
}
return certBytes, privKey
}
func generateClientCert(caCert *x509.Certificate, caKey crypto.Signer, commonName string) ([]byte, crypto.Signer) {
privKey, err := generatePrivateKey("Ed25519", nil)
if err != nil {
log.Fatal("Client key generation failed:", err)
}
template := x509.Certificate{
SerialNumber: randomSerial(),
Subject: pkix.Name{
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, privKey.Public(), caKey)
if err != nil {
log.Fatal("Client cert creation failed:", err)
}
return certBytes, privKey
}
func generateCodeSigningCert(caCert *x509.Certificate, caKey crypto.Signer, rsaBits int, commonName string) ([]byte, crypto.Signer) {
privKey, err := generatePrivateKey("RSA", rsaBits)
if err != nil {
log.Fatal("Code signing key generation failed:", err)
}
template := x509.Certificate{
SerialNumber: randomSerial(),
Subject: pkix.Name{
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(5, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, privKey.Public(), caKey)
if err != nil {
log.Fatal("Code signing cert creation failed:", err)
}
return certBytes, privKey
}
func generateCRL(caCert *x509.Certificate, caKey crypto.Signer, revoked []pkix.RevokedCertificate) {
crlTemplate := &x509.RevocationList{
SignatureAlgorithm: caCert.SignatureAlgorithm,
RevokedCertificates: revoked,
Number: randomSerial(),
ThisUpdate: time.Now(),
NextUpdate: time.Now().AddDate(0, 1, 0),
Issuer: caCert.Subject,
}
crlBytes, err := x509.CreateRevocationList(rand.Reader, crlTemplate, caCert, caKey)
if err != nil {
log.Fatal("CRL creation failed:", err)
}
saveCRL("ca.crl", crlBytes)
signFileContent("ca.crl", caKey)
}
func generatePrivateKey(algo string, param interface{}) (crypto.Signer, error) {
switch algo {
case "RSA":
bits, ok := param.(int)
if !ok {
return nil, fmt.Errorf("invalid RSA parameter")
}
return rsa.GenerateKey(rand.Reader, bits)
case "ECDSA":
curveName, ok := param.(string)
if !ok {
return nil, fmt.Errorf("invalid ECDSA parameter")
}
var curve elliptic.Curve
switch curveName {
case "P224":
curve = elliptic.P224()
case "P256":
curve = elliptic.P256()
case "P384":
curve = elliptic.P384()
case "P521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("unsupported curve")
}
return ecdsa.GenerateKey(curve, rand.Reader)
case "Ed25519":
_, priv, err := ed25519.GenerateKey(rand.Reader)
return priv, err
default:
return nil, fmt.Errorf("unsupported algorithm")
}
}
func saveCertificate(filename string, cert []byte) {
err := os.WriteFile(filename, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
}), 0644)
if err != nil {
log.Fatal("Failed to save certificate:", err)
}
}
func savePrivateKey(filename string, key crypto.Signer) {
keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
log.Fatal("Failed to marshal private key:", err)
}
err = os.WriteFile(filename, pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyBytes,
}), 0600)
if err != nil {
log.Fatal("Failed to save private key:", err)
}
}
func saveCRL(filename string, crl []byte) {
err := os.WriteFile(filename, pem.EncodeToMemory(&pem.Block{
Type: "X509 CRL",
Bytes: crl,
}), 0644)
if err != nil {
log.Fatal("Failed to save CRL:", err)
}
}
func signFileContent(filename string, signer crypto.Signer) {
data, err := os.ReadFile(filename)
if err != nil {
log.Fatal("Failed to read file for signing:", err)
}
sig, err := signData(data, signer)
if err != nil {
log.Fatal("Failed to sign file content:", err)
}
err = os.WriteFile(filename+".sig", sig, 0644)
if err != nil {
log.Fatal("Failed to save signature file:", err)
}
}
func signData(data []byte, key crypto.Signer) ([]byte, error) {
switch key.(type) {
case ed25519.PrivateKey:
return key.Sign(rand.Reader, data, crypto.Hash(0))
default:
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
return key.Sign(rand.Reader, hashed, crypto.SHA256)
}
}
func verifyDataSignature(data, signature []byte, pub crypto.PublicKey) error {
switch pub := pub.(type) {
case ed25519.PublicKey:
if ed25519.Verify(pub, data, signature) {
return nil
}
return errors.New("ed25519 signature verification failed")
case *rsa.PublicKey:
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed, signature)
case *ecdsa.PublicKey:
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
var sig struct {
R, S *big.Int
}
if _, err := asn1.Unmarshal(signature, &sig); err != nil {
return err
}
if ecdsa.Verify(pub, hashed, sig.R, sig.S) {
return nil
}
return errors.New("ecdsa signature verification failed")
default:
return errors.New("unsupported public key type")
}
}
func verifyFileContentSignature(filename, sigFilename string, pub crypto.PublicKey) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
sig, err := os.ReadFile(sigFilename)
if err != nil {
return fmt.Errorf("failed to read signature file: %w", err)
}
return verifyDataSignature(data, sig, pub)
}
func validateClientCert(clientCertPath, caCertPath string) error {
caCertBytes, err := os.ReadFile(caCertPath)
if err != nil {
return err
}
block, _ := pem.Decode(caCertBytes)
if block == nil {
return fmt.Errorf("failed to decode CA cert PEM")
}
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
clientCertBytes, err := os.ReadFile(clientCertPath)
if err != nil {
return err
}
block, _ = pem.Decode(clientCertBytes)
if block == nil {
return fmt.Errorf("failed to decode client cert PEM")
}
clientCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
opts := x509.VerifyOptions{
Roots: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
opts.Roots.AddCert(caCert)
if _, err = clientCert.Verify(opts); err != nil {
return fmt.Errorf("certificate chain verification failed: %w", err)
}
if err = verifyFileContentSignature(clientCertPath, clientCertPath+".sig", caCert.PublicKey); err != nil {
return fmt.Errorf("file signature verification failed: %w", err)
}
return nil
}
func randomSerial() *big.Int {
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
log.Fatal("failed to generate serial number:", err)
}
return serial
}

View File

@@ -1,26 +0,0 @@
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func generateHMACKey() ([]byte, error) {
key := make([]byte, 32) // 32 bytes = 256 bits
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return key, nil
}
func main() {
hmacKey, err := generateHMACKey()
if err != nil {
fmt.Println("Error generating HMAC key:", err)
return
}
fmt.Println("HMAC Key (hex):", hex.EncodeToString(hmacKey))
}

View File

@@ -1,166 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"github.com/oarkflow/jet"
"github.com/oarkflow/json"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/consts"
"github.com/oarkflow/mq/dag"
)
type Data struct {
Template string `json:"template"`
Mapping map[string]string `json:"mapping"`
AdditionalData map[string]any `json:"additional_data"`
}
type Node struct {
Name string `json:"name"`
ID string `json:"id"`
Node string `json:"node"`
Data Data `json:"data"`
FirstNode bool `json:"first_node"`
}
type Edge struct {
Source string `json:"source"`
Target []string `json:"target"`
}
type Handler struct {
Name string `json:"name"`
Key string `json:"key"`
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
func CreateDAGFromJSON(config string) (*dag.DAG, error) {
var handler Handler
err := json.Unmarshal([]byte(config), &handler)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
flow := dag.NewDAG(handler.Name, handler.Key, func(taskID string, result mq.Result) {
fmt.Printf("Final result for task %s: %s\n", taskID, string(result.Payload))
})
nodeMap := make(map[string]mq.Processor)
for _, node := range handler.Nodes {
op := &HTMLProcessor{
Operation: dag.Operation{
Payload: dag.Payload{
Mapping: node.Data.Mapping,
Data: node.Data.AdditionalData,
},
},
Template: node.Data.Template,
}
nodeMap[node.ID] = op
flow.AddNode(dag.Page, node.Name, node.ID, op, node.FirstNode)
}
for _, edge := range handler.Edges {
for _, target := range edge.Target {
flow.AddEdge(dag.Simple, edge.Source, edge.Source, target)
}
}
if flow.Error != nil {
return nil, flow.Error
}
return flow, nil
}
type HTMLProcessor struct {
dag.Operation
Template string
}
func (p *HTMLProcessor) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
data := map[string]interface{}{
"task_id": ctx.Value("task_id"),
}
p.Debug(ctx, task)
for key, value := range p.Payload.Mapping {
data[key] = value
}
rs, err := parser.ParseTemplate(p.Template, data)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
response := map[string]interface{}{
"html_content": rs,
}
payload, _ := json.Marshal(response)
return mq.Result{Payload: payload, Ctx: ctx}
}
func main() {
// JSON configuration
jsonConfig := `{
"name": "Multi-Step Form",
"key": "multi-step-form",
"nodes": [
{
"name": "Form Step1",
"id": "FormStep1",
"node": "Page",
"data": {
"additional_data": {"debug": true},
"template": "<html>\n<body>\n<form method=\"post\" action=\"/process?task_id={{task_id}}&next=true\">\n <label>Name:</label>\n <input type=\"text\" name=\"name\" required>\n <label>Age:</label>\n <input type=\"number\" name=\"age\" required>\n <button type=\"submit\">Next</button>\n</form>\n</body>\n</html>",
"mapping": {}
},
"first_node": true
},
{
"name": "Form Step2",
"id": "FormStep2",
"node": "Page",
"data": {
"template": "<html>\n<body>\n<form method=\"post\" action=\"/process?task_id={{task_id}}&next=true\">\n {{ if show_voting_controls }}\n <label>Do you want to register to vote?</label>\n <input type=\"checkbox\" name=\"register_vote\">\n <button type=\"submit\">Next</button>\n {{ else }}\n <p>You are not eligible to vote.</p>\n {{ end }}\n</form>\n</body>\n</html>",
"mapping": {
"show_voting_controls": "conditional"
}
}
},
{
"name": "Form Result",
"id": "FormResult",
"node": "Page",
"data": {
"template": "<html>\n<body>\n<h1>Form Summary</h1>\n<p>Name: {{ name }}</p>\n<p>Age: {{ age }}</p>\n{{ if register_vote }}\n <p>You have registered to vote!</p>\n{{ else }}\n <p>You did not register to vote.</p>\n{{ end }}\n</body>\n</html>",
"mapping": {}
}
}
],
"edges": [
{
"source": "FormStep1",
"target": ["FormStep2"]
},
{
"source": "FormStep2",
"target": ["FormResult"]
}
]
}`
flow, err := CreateDAGFromJSON(jsonConfig)
if err != nil {
log.Fatalf("Error creating DAG: %v", err)
}
flow.Start(context.Background(), "0.0.0.0:8082")
}

View File

@@ -1,63 +0,0 @@
{
"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": "/process?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

@@ -1,121 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/logger"
)
// Example: Proper timeout patterns for different connection types
func demonstrateTimeoutPatterns() {
fmt.Println("\n=== Connection Timeout Patterns ===")
// ✅ GOOD: No I/O timeout for persistent broker-consumer connections
fmt.Println("✅ Broker-Consumer: NO I/O timeouts (persistent connection)")
fmt.Println(" - Uses TCP keep-alive for network detection")
fmt.Println(" - Uses context cancellation for graceful shutdown")
fmt.Println(" - Connection stays open indefinitely waiting for messages")
// ✅ GOOD: Timeout for connection establishment
fmt.Println("✅ Connection Setup: WITH timeout (initial connection)")
fmt.Println(" - 10-30 second timeout for initial TCP handshake")
fmt.Println(" - Prevents hanging on unreachable servers")
// ✅ GOOD: Timeout for individual task processing
fmt.Println("✅ Task Processing: WITH timeout (individual operations)")
fmt.Println(" - 30 second timeout per task (configurable)")
fmt.Println(" - Prevents individual tasks from hanging forever")
// ❌ BAD: I/O timeout on persistent connections
fmt.Println("❌ Persistent I/O: NO timeouts on read/write operations")
fmt.Println(" - Would cause 'i/o timeout' errors on idle connections")
fmt.Println(" - Breaks the persistent connection model")
fmt.Println("\n💡 Key Principle:")
fmt.Println(" - Connection timeouts: Only for establishment")
fmt.Println(" - I/O timeouts: Only for request/response patterns")
fmt.Println(" - Persistent connections: Use keep-alive + context cancellation")
}
func main() {
fmt.Println("=== Minimal Admin Server Test ===")
// Demonstrate proper timeout patterns
demonstrateTimeoutPatterns()
// Create logger
lg := logger.NewDefaultLogger()
fmt.Println("\n✅ Logger created")
// Create broker with NO I/O timeouts for persistent connections
broker := mq.NewBroker(mq.WithLogger(lg), mq.WithBrokerURL(":8081"))
fmt.Println("✅ Broker created (NO I/O timeouts - persistent connections)")
// Start broker
ctx := context.Background()
fmt.Println("🚀 Starting broker...")
// Start broker in goroutine since it blocks
go func() {
if err := broker.Start(ctx); err != nil {
log.Printf("❌ Broker error: %v", err)
}
}()
// Give broker time to start
time.Sleep(500 * time.Millisecond)
fmt.Println("✅ Broker started")
defer broker.Close()
// Create admin server
fmt.Println("🔧 Creating admin server...")
adminServer := mq.NewAdminServer(broker, ":8090", lg)
fmt.Println("✅ Admin server created")
// Start admin server
fmt.Println("🚀 Starting admin server...")
if err := adminServer.Start(); err != nil {
log.Fatalf("❌ Failed to start admin server: %v", err)
}
defer adminServer.Stop()
fmt.Println("✅ Admin server started")
// Wait and test
fmt.Println("⏳ Waiting 2 seconds...")
time.Sleep(2 * time.Second)
fmt.Println("🔍 Testing connectivity...")
// ✅ GOOD: HTTP client WITH timeout (request/response pattern)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("http://localhost:8090/api/admin/health")
if err != nil {
fmt.Printf("❌ Connection failed: %v\n", err)
} else {
fmt.Printf("✅ Connection successful! Status: %d\n", resp.StatusCode)
resp.Body.Close()
}
fmt.Println("\n🌐 Admin Dashboard: http://localhost:8090/admin")
fmt.Println("📊 Health API: http://localhost:8090/api/admin/health")
fmt.Println("📡 Broker API: http://localhost:8090/api/admin/broker")
fmt.Println("📋 Queue API: http://localhost:8090/api/admin/queues")
fmt.Println("\n💡 Connection Patterns Demonstrated:")
fmt.Println(" 🔗 Broker ↔ Consumer: Persistent (NO I/O timeouts)")
fmt.Println(" 🌐 HTTP Client ↔ Admin: Request/Response (WITH timeouts)")
fmt.Println(" ⚡ WebSocket ↔ Dashboard: Persistent (NO I/O timeouts)")
fmt.Println("\n⚠ Server running - Press Ctrl+C to stop")
fmt.Println(" - Broker connections will remain persistent")
fmt.Println(" - No 'i/o timeout' errors should occur")
fmt.Println(" - TCP keep-alive handles network detection")
// Keep running
select {}
}

View File

@@ -1,284 +0,0 @@
# MQ Admin Dashboard
The MQ Admin Dashboard provides a comprehensive web-based interface for managing and monitoring your MQ broker, queues, consumers, and worker pools. It offers real-time metrics, control capabilities, and health monitoring similar to RabbitMQ's management interface.
## Features
### 🌟 **Comprehensive Dashboard**
- **Real-time Monitoring**: Live charts showing throughput, queue depth, and system metrics
- **Broker Management**: Monitor broker status, uptime, and configuration
- **Queue Management**: View queue depths, consumer counts, and flush capabilities
- **Consumer Control**: Monitor consumer status, pause/resume operations, and performance metrics
- **Worker Pool Management**: Track pool status, worker counts, and memory usage
- **Health Checks**: System health monitoring with detailed status reports
### 📊 **Real-time Visualizations**
- Interactive charts powered by Chart.js
- Live WebSocket updates for real-time data
- Throughput history and trend analysis
- System resource monitoring (CPU, Memory, Goroutines)
### 🎛️ **Management Controls**
- Pause/Resume consumers
- Adjust worker pool settings
- Flush queues
- Restart/Stop broker operations
- Configuration management
## Quick Start
### 1. Run the Admin Demo
```bash
cd examples/admin
go run main.go
```
This will start:
- MQ Broker (background)
- Admin Dashboard on http://localhost:8090/admin
- Sample task simulation for demonstration
### 2. Access the Dashboard
Open your browser and navigate to:
```
http://localhost:8090/admin
```
### 3. Explore the Interface
The dashboard consists of several tabs:
- **📈 Overview**: High-level metrics and system status
- **🔧 Broker**: Broker configuration and control
- **📋 Queues**: Queue monitoring and management
- **👥 Consumers**: Consumer status and control
- **🏊 Pools**: Worker pool monitoring
- **❤️ Monitoring**: Health checks and system metrics
## Integration in Your Application
### Basic Usage
```go
package main
import (
"context"
"log"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/logger"
)
func main() {
// Create logger
lg := logger.NewDefaultLogger()
// Create broker
broker := mq.NewBroker(mq.WithLogger(lg))
// Start broker
ctx := context.Background()
if err := broker.Start(ctx); err != nil {
log.Fatalf("Failed to start broker: %v", err)
}
defer broker.Close()
// Create admin server
adminServer := mq.NewAdminServer(broker, ":8090", lg)
if err := adminServer.Start(); err != nil {
log.Fatalf("Failed to start admin server: %v", err)
}
defer adminServer.Stop()
// Your application logic here...
}
```
### Configuration Options
The admin server can be configured with different options:
```go
// Custom port
adminServer := mq.NewAdminServer(broker, ":9090", lg)
// With custom logger
customLogger := logger.NewDefaultLogger()
adminServer := mq.NewAdminServer(broker, ":8090", customLogger)
```
## API Endpoints
The admin server exposes several REST API endpoints:
### Core Endpoints
- `GET /admin` - Main dashboard interface
- `GET /api/admin/metrics` - Real-time metrics
- `GET /api/admin/broker` - Broker information
- `GET /api/admin/queues` - Queue status
- `GET /api/admin/consumers` - Consumer information
- `GET /api/admin/pools` - Worker pool status
- `GET /api/admin/health` - Health checks
### Control Endpoints
- `POST /api/admin/broker/restart` - Restart broker
- `POST /api/admin/broker/stop` - Stop broker
- `POST /api/admin/queues/flush` - Flush all queues
### Example API Usage
```bash
# Get current metrics
curl http://localhost:8090/api/admin/metrics
# Get broker status
curl http://localhost:8090/api/admin/broker
# Flush all queues
curl -X POST http://localhost:8090/api/admin/queues/flush
```
## Dashboard Features
### 🎨 **Modern UI Design**
- Responsive design with Tailwind CSS
- Clean, intuitive interface
- Dark/light theme support
- Mobile-friendly layout
### 📊 **Real-time Charts**
- Live throughput monitoring
- Queue depth trends
- System resource usage
- Error rate tracking
### ⚡ **Interactive Controls**
- Consumer pause/resume buttons
- Pool configuration modals
- Queue management tools
- Real-time status updates
### 🔄 **WebSocket Integration**
- Live data updates without page refresh
- Real-time event streaming
- Automatic reconnection
- Low-latency monitoring
## File Structure
The admin interface consists of:
```
static/admin/
├── index.html # Main dashboard interface
├── css/
│ └── admin.css # Custom styling
└── js/
└── admin.js # Dashboard JavaScript logic
```
## Metrics and Monitoring
### System Metrics
- **Throughput**: Messages processed per second
- **Queue Depth**: Number of pending messages
- **Active Consumers**: Currently running consumers
- **Error Rate**: Failed message percentage
- **Memory Usage**: System memory consumption
- **CPU Usage**: System CPU utilization
### Queue Metrics
- Message count per queue
- Consumer count per queue
- Processing rate per queue
- Error count per queue
### Consumer Metrics
- Messages processed
- Error count
- Last activity timestamp
- Configuration parameters
### Pool Metrics
- Active workers
- Queue size
- Memory load
- Task distribution
## Customization
### Styling
The interface uses Tailwind CSS and can be customized by modifying `static/admin/css/admin.css`.
### JavaScript
Dashboard functionality can be extended by modifying `static/admin/js/admin.js`.
### Backend
Additional API endpoints can be added to `admin_server.go`.
## Production Considerations
### Security
- Add authentication/authorization
- Use HTTPS in production
- Implement rate limiting
- Add CORS configuration
### Performance
- Enable caching for static assets
- Use compression middleware
- Monitor memory usage
- Implement connection pooling
### Monitoring
- Add logging for admin operations
- Implement audit trails
- Monitor admin API usage
- Set up alerting for critical events
## Troubleshooting
### Common Issues
1. **Dashboard not loading**
- Check if static files are in the correct location
- Verify server is running on correct port
- Check browser console for errors
2. **API endpoints returning 404**
- Ensure admin server is started
- Verify correct port configuration
- Check route registration
3. **Real-time updates not working**
- Check WebSocket connection in browser dev tools
- Verify broker is running and accessible
- Check for CORS issues
### Debug Mode
Enable debug logging:
```go
lg := logger.NewDefaultLogger()
lg.Debug("Admin server starting")
```
## License
This admin dashboard is part of the MQ package and follows the same license terms.
## Contributing
To contribute to the admin dashboard:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
For issues or feature requests, please open an issue on the main repository.

View File

@@ -1,550 +0,0 @@
/* Admin Dashboard Custom Styles */
:root {
--primary-color: #1e40af;
--secondary-color: #3b82f6;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--info-color: #06b6d4;
--dark-color: #374151;
--light-color: #f9fafb;
}
/* Chart containers to prevent infinite resize loops */
.chart-container {
position: relative;
width: 100%;
max-width: 100%;
overflow: hidden;
}
.chart-container canvas {
display: block;
width: 100% !important;
height: auto !important;
max-width: 100%;
}
/* Custom scrollbar */
.scrollbar {
scrollbar-width: thin;
scrollbar-color: #d1d5db #e5e7eb;
}
.scrollbar::-webkit-scrollbar {
width: 8px;
}
.scrollbar::-webkit-scrollbar-track {
background: #e5e7eb;
}
.scrollbar::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 9999px;
}
/* Tab styles */
.tab-btn {
border-bottom-color: transparent;
color: #6b7280;
transition: all 0.2s ease;
}
.tab-btn.active {
border-bottom-color: var(--primary-color);
color: var(--primary-color);
}
.tab-btn:hover:not(.active) {
color: #374151;
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease-in;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Status indicators */
.status-indicator {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.status-running {
background-color: #d1fae5;
color: #065f46;
}
.status-paused {
background-color: #fef3c7;
color: #92400e;
}
.status-stopped {
background-color: #fee2e2;
color: #991b1b;
}
.status-healthy {
background-color: #d1fae5;
color: #065f46;
}
.status-warning {
background-color: #fef3c7;
color: #92400e;
}
.status-error {
background-color: #fee2e2;
color: #991b1b;
}
/* Connection status */
.connection-status .connected {
background-color: var(--success-color);
}
.connection-status .disconnected {
background-color: var(--danger-color);
}
/* Card hover effects */
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
/* Button styles */
.btn-primary {
background-color: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s ease;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #6b7280;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s ease;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-success {
background-color: var(--success-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s ease;
}
.btn-success:hover {
background-color: #059669;
}
.btn-warning {
background-color: var(--warning-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s ease;
}
.btn-warning:hover {
background-color: #d97706;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
transition: background-color 0.2s ease;
}
.btn-danger:hover {
background-color: #dc2626;
}
/* Table styles */
.table-container {
max-height: 500px;
overflow-y: auto;
}
.table-row:hover {
background-color: #f9fafb;
}
/* Modal styles */
.modal {
z-index: 1000;
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-50px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Chart containers */
.chart-container {
position: relative;
height: 300px;
}
/* Activity feed */
.activity-item {
display: flex;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid #e5e7eb;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 2rem;
height: 2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.activity-icon.success {
background-color: var(--success-color);
color: white;
}
.activity-icon.warning {
background-color: var(--warning-color);
color: white;
}
.activity-icon.error {
background-color: var(--danger-color);
color: white;
}
.activity-icon.info {
background-color: var(--info-color);
color: white;
}
/* Metrics cards */
.metric-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.metric-label {
font-size: 0.875rem;
opacity: 0.8;
}
/* Health check styles */
.health-check {
display: flex;
align-items: center;
justify-content: between;
padding: 1rem;
border-radius: 0.375rem;
margin-bottom: 0.5rem;
}
.health-check.healthy {
background-color: #d1fae5;
border: 1px solid #10b981;
}
.health-check.warning {
background-color: #fef3c7;
border: 1px solid #f59e0b;
}
.health-check.error {
background-color: #fee2e2;
border: 1px solid #ef4444;
}
.health-check-icon {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.75rem;
}
/* Loading spinner */
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid var(--primary-color);
border-radius: 50%;
width: 1rem;
height: 1rem;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 0.5rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive adjustments */
@media (max-width: 768px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
.grid-cols-2 {
grid-template-columns: 1fr;
}
.tab-btn {
padding: 0.5rem 0.25rem;
font-size: 0.875rem;
}
.modal-content {
margin: 1rem;
max-width: calc(100% - 2rem);
}
}
/* Toast notifications */
.toast {
position: fixed;
top: 1rem;
right: 1rem;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
padding: 1rem;
z-index: 2000;
max-width: 300px;
transform: translateX(400px);
transition: transform 0.3s ease;
}
.toast.show {
transform: translateX(0);
}
.toast.success {
border-left: 4px solid var(--success-color);
}
.toast.warning {
border-left: 4px solid var(--warning-color);
}
.toast.error {
border-left: 4px solid var(--danger-color);
}
.toast.info {
border-left: 4px solid var(--info-color);
}
/* Progress bars */
.progress-bar {
width: 100%;
height: 0.5rem;
background-color: #e5e7eb;
border-radius: 9999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: var(--primary-color);
transition: width 0.3s ease;
}
.progress-fill.success {
background-color: var(--success-color);
}
.progress-fill.warning {
background-color: var(--warning-color);
}
.progress-fill.danger {
background-color: var(--danger-color);
}
/* Form styles */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: #374151;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input:invalid {
border-color: var(--danger-color);
}
/* Custom toggle switch */
.toggle {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.25rem;
width: 1.25rem;
left: 0.125rem;
bottom: 0.125rem;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
.toggle input:checked + .toggle-slider {
background-color: var(--primary-color);
}
.toggle input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
/* Tooltips */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 120px;
background-color: #374151;
color: white;
text-align: center;
border-radius: 0.375rem;
padding: 0.5rem;
font-size: 0.75rem;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1f2937;
--surface-color: #374151;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
}
}

View File

@@ -1,628 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MQ Admin Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
<link rel="stylesheet" href="/static/admin/css/admin.css">
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Navigation Header -->
<nav class="bg-blue-800 text-white shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold">MQ Admin Dashboard</h1>
</div>
<div class="flex items-center space-x-4">
<div class="connection-status flex items-center space-x-2">
<div id="connectionIndicator" class="w-3 h-3 bg-red-500 rounded-full"></div>
<span id="connectionStatus">Disconnected</span>
</div>
<button id="refreshBtn" class="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm">
Refresh
</button>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Tab Navigation -->
<div class="border-b border-gray-200 mb-6">
<nav class="-mb-px flex space-x-8">
<button class="tab-btn active py-2 px-1 border-b-2 font-medium text-sm" data-tab="overview">
Overview
</button>
<button class="tab-btn py-2 px-1 border-b-2 font-medium text-sm" data-tab="broker">
Broker
</button>
<button class="tab-btn py-2 px-1 border-b-2 font-medium text-sm" data-tab="queues">
Queues
</button>
<button class="tab-btn py-2 px-1 border-b-2 font-medium text-sm" data-tab="consumers">
Consumers
</button>
<button class="tab-btn py-2 px-1 border-b-2 font-medium text-sm" data-tab="pools">
Worker Pools
</button>
<button class="tab-btn py-2 px-1 border-b-2 font-medium text-sm" data-tab="tasks">
Tasks
</button>
<button class="tab-btn py-2 px-1 border-b-2 font-medium text-sm" data-tab="monitoring">
Monitoring
</button>
</nav>
</div>
<!-- Tab Content -->
<div id="tabContent">
<!-- Overview Tab -->
<div id="overview" class="tab-content active">
<!-- System Status Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path
d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z">
</path>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Messages</dt>
<dd class="text-lg font-medium text-gray-900" id="totalMessages">0</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3z">
</path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Consumers</dt>
<dd class="text-lg font-medium text-gray-900" id="activeConsumers">0</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z">
</path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Queues</dt>
<dd class="text-lg font-medium text-gray-900" id="activeQueues">0</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Failed Messages</dt>
<dd class="text-lg font-medium text-gray-900" id="failedMessages">0</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Real-time Charts -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-medium text-gray-900 mb-4">Message Throughput</h3>
<div class="chart-container" style="position: relative; height: 200px;">
<canvas id="throughputChart"></canvas>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-medium text-gray-900 mb-4">Queue Depths</h3>
<div class="chart-container" style="position: relative; height: 200px;">
<canvas id="queueDepthChart"></canvas>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Recent Activity</h3>
<div class="flow-root">
<ul id="activityFeed" class="divide-y divide-gray-200">
<!-- Activity items will be populated here -->
</ul>
</div>
</div>
</div>
</div>
<!-- Broker Tab -->
<div id="broker" class="tab-content hidden">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Broker Status -->
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Broker Status</h3>
<div class="space-y-4">
<div class="flex justify-between">
<span class="text-gray-600">Status:</span>
<span id="brokerStatus"
class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">Running</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Address:</span>
<span id="brokerAddress">localhost:8080</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Uptime:</span>
<span id="brokerUptime">0h 0m</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Connections:</span>
<span id="brokerConnections">0</span>
</div>
</div>
</div>
<!-- Broker Controls -->
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Broker Controls</h3>
<div class="space-y-4">
<button id="restartBroker"
class="w-full bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded">
Restart Broker
</button>
<button id="stopBroker"
class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
Stop Broker
</button>
<button id="flushQueues"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Flush All Queues
</button>
</div>
</div>
</div>
<!-- Broker Configuration -->
<div class="mt-6 bg-white shadow rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Broker Configuration</h3>
<div id="brokerConfig" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Configuration will be loaded here -->
</div>
</div>
</div>
<!-- Queues Tab -->
<div id="queues" class="tab-content hidden">
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">Queue Management</h3>
<button id="createQueue"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
Create Queue
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Queue Name</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Messages</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Consumers</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rate/sec</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions</th>
</tr>
</thead>
<tbody id="queuesTable" class="bg-white divide-y divide-gray-200">
<!-- Queue rows will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Consumers Tab -->
<div id="consumers" class="tab-content hidden">
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Consumer Management</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Consumer ID</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Queue</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Processed</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Errors</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions</th>
</tr>
</thead>
<tbody id="consumersTable" class="bg-white divide-y divide-gray-200">
<!-- Consumer rows will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Worker Pools Tab -->
<div id="pools" class="tab-content hidden">
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Worker Pool Management</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pool ID</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Workers</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Queue Size</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Active Tasks</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions</th>
</tr>
</thead>
<tbody id="poolsTable" class="bg-white divide-y divide-gray-200">
<!-- Pool rows will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Monitoring Tab -->
<div id="monitoring" class="tab-content hidden">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-medium text-gray-900 mb-4">System Performance</h3>
<div class="chart-container" style="position: relative; height: 200px;">
<canvas id="systemChart"></canvas>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-medium text-gray-900 mb-4">Error Rate</h3>
<div class="chart-container" style="position: relative; height: 200px;">
<canvas id="errorChart"></canvas>
</div>
</div>
</div>
<!-- Health Checks -->
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Health Checks</h3>
<div id="healthChecks" class="space-y-4">
<!-- Health check items will be populated here -->
</div>
</div>
</div>
<!-- Tasks Tab -->
<div id="tasks" class="tab-content hidden">
<!-- Tasks Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Tasks</dt>
<dd class="text-lg font-medium text-gray-900" id="activeTasks">0</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"></path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Completed</dt>
<dd class="text-lg font-medium text-gray-900" id="completedTasks">0</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-red-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Failed</dt>
<dd class="text-lg font-medium text-gray-900" id="failedTasks">0</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
clip-rule="evenodd"></path>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Queued</dt>
<dd class="text-lg font-medium text-gray-900" id="queuedTasks">0</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Tasks Table -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">Real-time Tasks</h3>
<div class="flex space-x-2">
<button id="refreshTasks"
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clip-rule="evenodd"></path>
</svg>
Refresh
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Task ID
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Queue
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Retry Count
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created At
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Payload
</th>
</tr>
</thead>
<tbody id="tasksTableBody" class="bg-white divide-y divide-gray-200">
<!-- Tasks will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<!-- Consumer Configuration Modal -->
<div id="consumerModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg max-w-lg w-full p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Configure Consumer</h3>
<form id="consumerForm">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Consumer ID</label>
<input type="text" id="consumerIdField"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" readonly>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Max Concurrent Tasks</label>
<input type="number" id="maxConcurrentTasks"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Task Timeout (seconds)</label>
<input type="number" id="taskTimeout"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Max Retries</label>
<input type="number" id="maxRetries"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button type="button" id="cancelConsumerConfig"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Cancel
</button>
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Save
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Pool Configuration Modal -->
<div id="poolModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg max-w-lg w-full p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Configure Worker Pool</h3>
<form id="poolForm">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Pool ID</label>
<input type="text" id="poolIdField"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm" readonly>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Number of Workers</label>
<input type="number" id="numWorkers"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Queue Size</label>
<input type="number" id="queueSize"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Max Memory Load (bytes)</label>
<input type="number" id="maxMemoryLoad"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button type="button" id="cancelPoolConfig"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Cancel
</button>
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Save
</button>
</div>
</form>
</div>
</div>
</div>
<script src="/static/admin/js/socket.js"></script>
<script src="/static/admin/js/admin.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,249 +0,0 @@
class Socket {
events = {}
reconnectInterval
reconnectOpts = { enabled: true, replayOnConnect: true, intervalMS: 5000 }
reconnecting = false
connectedOnce = false
headerStartCharCode = 1
headerStartChar
dataStartCharCode = 2
dataStartChar
subProtocol = 'sac-sock'
ws
reconnected = false
maxAttempts = 3
totalAttempts = 0
kickedOut = false
userID
url
constructor(url, userID, opts = { reconnectOpts: {} }) {
opts = opts || { reconnectOpts: {} };
this.headerStartChar = String.fromCharCode(this.headerStartCharCode)
this.dataStartChar = String.fromCharCode(this.dataStartCharCode)
this.url = url
this.userID = userID
if (typeof opts.reconnectOpts == 'object') {
for (let i in opts.reconnectOpts) {
if (!opts.reconnectOpts.hasOwnProperty(i)) continue;
this.reconnectOpts[i] = opts.reconnectOpts[i];
}
}
}
noop() { }
async connect(timeout = 10000) {
try {
this.ws = new WebSocket(this.url, this.subProtocol);
this.ws.binaryType = 'arraybuffer';
this.handleEvents()
const isOpened = () => (this.ws.readyState === WebSocket.OPEN)
if (this.ws.readyState !== WebSocket.CONNECTING) {
return isOpened()
}
} catch (err) {
console.log("Error on reconnection", err)
}
}
handleEvents() {
let self = this
this.onConnect(this.noop)
this.onDisconnect(this.noop)
this.ws.onmessage = function (e) {
let msg = e.data,
headers = {},
eventName = '',
data = '',
chr = null,
i, msgLen;
if (typeof msg === 'string') {
let dataStarted = false,
headerStarted = false;
for (i = 0, msgLen = msg.length; i < msgLen; i++) {
chr = msg[i];
if (!dataStarted && !headerStarted && chr !== self.dataStartChar && chr !== self.headerStartChar) {
eventName += chr;
} else if (!headerStarted && chr === self.headerStartChar) {
headerStarted = true;
} else if (headerStarted && !dataStarted && chr !== self.dataStartChar) {
headers[chr] = true;
} else if (!dataStarted && chr === self.dataStartChar) {
dataStarted = true;
} else {
data += chr;
}
}
} else if (msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined) {
let dv = new DataView(msg),
headersStarted = false;
for (i = 0, msgLen = dv.byteLength; i < msgLen; i++) {
chr = dv.getUint8(i);
if (chr !== self.dataStartCharCode && chr !== self.headerStartCharCode && !headersStarted) {
eventName += String.fromCharCode(chr);
} else if (chr === self.headerStartCharCode && !headersStarted) {
headersStarted = true;
} else if (headersStarted && chr !== self.dataStartCharCode) {
headers[String.fromCharCode(chr)] = true;
} else if (chr === self.dataStartCharCode) {
// @ts-ignore
data = dv.buffer.slice(i + 1);
break;
}
}
}
if (eventName.length === 0) return; //no event to dispatch
if (typeof self.events[eventName] === 'undefined') return;
// @ts-ignore
self.events[eventName].call(self, (headers.J) ? JSON.parse(data) : data);
}
}
startReconnect(timeout = 10000) {
let self = this
setTimeout(async function () {
try {
if (self.maxAttempts > self.totalAttempts) {
return
}
let newWS = new WebSocket(self.url, self.subProtocol);
self.totalAttempts += 1
console.log("attempt to reconnect...", self.totalAttempts)
newWS.onmessage = self.ws.onmessage;
newWS.onclose = self.ws.onclose;
newWS.binaryType = self.ws.binaryType;
self.handleEvents()
//we need to run the initially set onConnect function on the first successful connecting,
//even if replayOnConnect is disabled. The server might not be available on a first
//connection attempt.
if (self.reconnectOpts.replayOnConnect || !self.connectedOnce) {
newWS.onopen = self.ws.onopen;
}
self.ws = newWS;
if (!self.reconnectOpts.replayOnConnect && self.connectedOnce) {
self.onConnect(self.noop);
}
self.ws = newWS
const isOpened = () => (self.ws.readyState === WebSocket.OPEN)
if (self.ws.readyState !== WebSocket.CONNECTING) {
const opened = isOpened()
if (!opened) {
self.startReconnect(timeout)
} else {
console.log("connected with signal server")
}
return opened
}
else {
const intrasleep = 100
const ttl = timeout / intrasleep // time to loop
let loop = 0
while (self.ws.readyState === WebSocket.CONNECTING && loop < ttl) {
await new Promise(resolve => setTimeout(resolve, intrasleep))
loop++
}
const opened = isOpened()
if (!opened) {
self.startReconnect(timeout)
} else {
console.log("connected with signal server")
}
return opened
}
} catch (err) {
console.log("Error on reconnection", err)
}
}, self.reconnectOpts.intervalMS);
}
onConnect(callback) {
let self = this
this.ws.onopen = function () {
self.connectedOnce = true;
callback.apply(self, arguments);
if (self.reconnecting) {
self.reconnecting = false;
}
};
};
onDisconnect(callback) {
let self = this
this.ws.onclose = function () {
if (!self.reconnecting && self.connectedOnce) {
callback.apply(self, arguments);
}
if (self.reconnectOpts.enabled && !self.kickedOut) {
self.reconnecting = true;
self.startReconnect();
}
};
};
on(eventName, callback, override) {
override = override || false
if (!this.events.hasOwnProperty(eventName)) {
this.events[eventName] = callback;
} else if (override) {
this.off(eventName)
this.events[eventName] = callback;
}
}
off(eventName) {
if (this.events[eventName]) {
delete this.events[eventName];
}
}
emit(eventName, data) {
let rs = this.ws.readyState;
if (rs === 0) {
console.warn("websocket is not open yet");
return;
} else if (rs === 2 || rs === 3) {
console.error("websocket is closed");
return;
}
let msg;
if (data instanceof ArrayBuffer) {
let ab = new ArrayBuffer(data.byteLength + eventName.length + 1),
newBuf = new DataView(ab),
oldBuf = new DataView(data),
i = 0;
for (let evtLen = eventName.length; i < evtLen; i++) {
newBuf.setUint8(i, eventName.charCodeAt(i));
}
newBuf.setUint8(i, this.dataStartCharCode);
i++;
for (let x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++) {
newBuf.setUint8(i, oldBuf.getUint8(x));
}
msg = ab;
} else if (typeof data === 'object') {
msg = eventName + this.dataStartChar + JSON.stringify(data);
} else {
msg = eventName + this.dataStartChar + data;
}
this.ws.send(msg);
}
close() {
this.reconnectOpts.enabled = false;
return this.ws.close(1000);
}
}
// Initialize dashboard when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.Socket = Socket;
});

View File

@@ -1,12 +0,0 @@
package main
import (
"fmt"
"github.com/oarkflow/form"
)
func main() {
queryString := []byte("fields[0][method]=GET&fields[0][path]=/user/:id&fields[0][handlerMsg]=User Profile&fields[1][method]=POST&fields[1][path]=/user/create&fields[1][handlerMsg]=Create User")
fmt.Println(form.DecodeForm(queryString))
}

View File

@@ -1,66 +0,0 @@
package main
import (
"context"
"fmt"
"math/rand"
"os/signal"
"syscall"
"time"
"github.com/oarkflow/json"
v1 "github.com/oarkflow/mq"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
pool := v1.NewPool(5,
v1.WithTaskStorage(v1.NewMemoryTaskStorage(10*time.Minute)),
v1.WithHandler(func(ctx context.Context, payload *v1.Task) v1.Result {
v1.Logger.Info().Str("taskID", payload.ID).Msg("Processing task payload")
time.Sleep(500 * time.Millisecond)
return v1.Result{}
}),
v1.WithPoolCallback(func(ctx context.Context, result v1.Result) error {
v1.Logger.Info().Msg("Task callback invoked")
return nil
}),
v1.WithCircuitBreaker(v1.CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 3,
ResetTimeout: 5 * time.Second,
}),
v1.WithWarningThresholds(v1.ThresholdConfig{
HighMemory: v1.Config.WarningThreshold.HighMemory,
LongExecution: v1.Config.WarningThreshold.LongExecution,
}),
v1.WithDiagnostics(true),
v1.WithMetricsRegistry(v1.NewInMemoryMetricsRegistry()),
v1.WithGracefulShutdown(10*time.Second),
v1.WithPlugin(&v1.DefaultPlugin{}),
)
defer func() {
metrics := pool.Metrics()
v1.Logger.Info().Msgf("Metrics: %+v", metrics)
pool.Stop()
v1.Logger.Info().Msgf("Dead Letter Queue has %d tasks", len(pool.DLQ().Tasks()))
}()
go func() {
for i := 0; i < 50; i++ {
task := &v1.Task{
ID: "",
Payload: json.RawMessage(fmt.Sprintf("Task Payload %d", i)),
}
if err := pool.EnqueueTask(context.Background(), task, rand.Intn(10)); err != nil {
v1.Logger.Error().Err(err).Msg("Failed to enqueue task")
}
time.Sleep(200 * time.Millisecond)
}
}()
<-ctx.Done()
v1.Logger.Info().Msg("Received shutdown signal, exiting...")
}

View File

@@ -1,34 +0,0 @@
package main
import (
"context"
"time"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
pool := mq.NewPool(2,
mq.WithTaskQueueSize(5),
mq.WithMaxMemoryLoad(1000),
mq.WithHandler(tasks.SchedulerHandler),
mq.WithPoolCallback(tasks.SchedulerCallback),
mq.WithTaskStorage(mq.NewMemoryTaskStorage(10*time.Minute)),
mq.WithDiagnostics(false),
)
for i := 0; i < 100; i++ {
if i%10 == 0 {
pool.EnqueueTask(context.Background(), &mq.Task{ID: "High Priority Task: I'm high"}, 10)
} else if i%15 == 0 {
pool.EnqueueTask(context.Background(), &mq.Task{ID: "Super High Priority Task: {}"}, 15)
} else {
pool.EnqueueTask(context.Background(), &mq.Task{ID: "Low Priority Task"}, 1)
}
}
time.Sleep(15 * time.Second)
pool.Metrics()
pool.Stop()
}

View File

@@ -1,24 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/oarkflow/mq"
)
func main() {
payload := []byte(`{"phone": "+123456789", "email": "abc.xyz@gmail.com", "age": 12}`)
task := mq.Task{
Payload: payload,
}
publisher := mq.NewPublisher("publish-1", mq.WithBrokerURL(":8081"))
for i := 0; i < 2; i++ {
// publisher := mq.NewPublisher("publish-1", mq.WithTLS(true, "./certs/server.crt", "./certs/server.key"))
err := publisher.Publish(context.Background(), task, "queue1")
if err != nil {
panic(err)
}
}
fmt.Println("Async task published successfully")
}

View File

@@ -1,108 +0,0 @@
package main
import (
"context"
"fmt"
"time"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
handler := tasks.SchedulerHandler
callback := tasks.SchedulerCallback
// Initialize the pool with various parameters.
pool := mq.NewPool(3,
mq.WithTaskQueueSize(5),
mq.WithMaxMemoryLoad(1000),
mq.WithHandler(handler),
mq.WithPoolCallback(callback),
mq.WithTaskStorage(mq.NewMemoryTaskStorage(10*time.Minute)),
)
scheduler := mq.NewScheduler(pool)
scheduler.Start()
ctx := context.Background()
// -------------------------------
// Special String Examples
// -------------------------------
// Task scheduled with @every for a duration of 30 seconds.
scheduler.AddTask(ctx, &mq.Task{ID: "Every 30 Seconds Task"},
mq.WithScheduleSpec("@every 30s"),
mq.WithRecurring(),
)
// Task scheduled with @every using a 1-minute duration.
scheduler.AddTask(ctx, &mq.Task{ID: "Every Minute Task"},
mq.WithScheduleSpec("@every 1m"),
mq.WithRecurring(),
)
// Task scheduled with @daily (runs at midnight by default).
scheduler.AddTask(ctx, &mq.Task{ID: "Daily Task"},
mq.WithScheduleSpec("@daily"),
mq.WithRecurring(),
)
// Task scheduled with @weekly (runs at midnight on Sundays).
scheduler.AddTask(ctx, &mq.Task{ID: "Weekly Task"},
mq.WithScheduleSpec("@weekly"),
mq.WithRecurring(),
)
// Task scheduled with @monthly (runs on the 1st of every month at midnight).
scheduler.AddTask(ctx, &mq.Task{ID: "Monthly Task"},
mq.WithScheduleSpec("@monthly"),
mq.WithRecurring(),
)
// Task scheduled with @yearly (or @annually) runs on January 1st at midnight.
scheduler.AddTask(ctx, &mq.Task{ID: "Yearly Task"},
mq.WithScheduleSpec("@yearly"),
mq.WithRecurring(),
)
// -------------------------------
// Cron Spec Examples
// -------------------------------
// Example using a standard 5-field cron expression:
// "0 * * * *" means at minute 0 of every hour.
scheduler.AddTask(ctx, &mq.Task{ID: "Cron 5-field Task"},
mq.WithScheduleSpec("0 * * * *"),
mq.WithRecurring(),
)
// Example using an extended 6-field cron expression:
// "30 * * * * *" means at 30 seconds past every minute.
scheduler.AddTask(ctx, &mq.Task{ID: "Cron 6-field Task"},
mq.WithScheduleSpec("30 * * * * *"),
mq.WithRecurring(),
)
// -------------------------------
// Example Task Enqueuing (Immediate Tasks)
// -------------------------------
// These tasks are enqueued immediately into the pool queue.
pool.EnqueueTask(context.Background(), &mq.Task{ID: "Immediate Task 1"}, 1)
time.Sleep(1 * time.Second)
pool.EnqueueTask(context.Background(), &mq.Task{ID: "Immediate Task 2"}, 5)
time.Sleep(10 * time.Minute)
// Remove scheduled tasks after demonstration.
scheduler.RemoveTask("Every 30 Seconds Task")
scheduler.RemoveTask("Every Minute Task")
scheduler.RemoveTask("Daily Task")
scheduler.RemoveTask("Weekly Task")
scheduler.RemoveTask("Monthly Task")
scheduler.RemoveTask("Yearly Task")
scheduler.RemoveTask("Cron 5-field Task")
scheduler.RemoveTask("Cron 6-field Task")
scheduler.Close()
// Retrieve metrics, then stop the pool.
fmt.Println(pool.FormattedMetrics())
}

View File

@@ -1,105 +0,0 @@
{
"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": "/process?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

@@ -1,17 +0,0 @@
package main
import (
"context"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
b := mq.NewBroker(mq.WithCallback(tasks.Callback), mq.WithBrokerURL(":8081"))
// b := mq.NewBroker(mq.WithCallback(tasks.Callback), mq.WithTLS(true, "./certs/server.crt", "./certs/server.key"), mq.WithCAPath("./certs/ca.cert"))
b.NewQueue("queue1")
b.NewQueue("queue2")
b.Start(context.Background())
}

View File

@@ -1,708 +0,0 @@
package main
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/oarkflow/json"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq/utils"
"github.com/oarkflow/jet"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/consts"
)
func main() {
flow := dag.NewDAG("SMS Sender", "sms-sender", func(taskID string, result mq.Result) {
fmt.Printf("SMS workflow completed for task %s: %s\n", taskID, string(utils.RemoveRecursiveFromJSON(result.Payload, "html_content")))
})
// Add SMS workflow nodes
// Note: Page nodes have no timeout by default, allowing users unlimited time for form input
flow.AddNode(dag.Page, "SMS Form", "SMSForm", &SMSFormNode{})
flow.AddNode(dag.Function, "Validate Input", "ValidateInput", &ValidateInputNode{})
flow.AddNode(dag.Function, "Send SMS", "SendSMS", &SendSMSNode{})
flow.AddNode(dag.Page, "SMS Result", "SMSResult", &SMSResultNode{})
flow.AddNode(dag.Page, "Error Page", "ErrorPage", &ErrorPageNode{})
// Define edges for SMS workflow
flow.AddEdge(dag.Simple, "Form to Validation", "SMSForm", "ValidateInput")
flow.AddCondition("ValidateInput", map[string]string{"valid": "SendSMS", "invalid": "ErrorPage"})
flow.AddCondition("SendSMS", map[string]string{"sent": "SMSResult", "failed": "ErrorPage"})
// Start the flow
if flow.Error != nil {
panic(flow.Error)
}
fmt.Println("Starting SMS DAG server on http://0.0.0.0:8083")
fmt.Println("Navigate to the URL to access the SMS form")
flow.Start(context.Background(), "0.0.0.0:8083")
}
// SMSFormNode - Initial form to collect SMS data
type SMSFormNode struct {
dag.Operation
}
func (s *SMSFormNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
// Check if this is a form submission
var inputData map[string]any
if task.Payload != nil && len(task.Payload) > 0 {
if err := json.Unmarshal(task.Payload, &inputData); err == nil {
// If we have valid input data, pass it through for validation
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
}
// Otherwise, show the form
htmlTemplate := `
<!DOCTYPE html>
<html>
<head>
<title>SMS Sender</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.form-container {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
h1 {
text-align: center;
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
input, textarea {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
background: rgba(255, 255, 255, 0.2);
color: white;
backdrop-filter: blur(5px);
}
input::placeholder, textarea::placeholder {
color: rgba(255, 255, 255, 0.7);
}
textarea {
height: 100px;
resize: vertical;
}
.char-count {
text-align: right;
font-size: 12px;
margin-top: 5px;
opacity: 0.8;
}
button {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
color: white;
padding: 15px 30px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
width: 100%;
transition: transform 0.2s;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.info {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
}
</style>
</head>
<body>
<div class="form-container">
<h1>📱 SMS Sender</h1>
<div class="info">
<p>Send SMS messages through our secure DAG workflow</p>
</div>
<form method="post" action="/process?task_id={{task_id}}&next=true">
<div class="form-group">
<label for="phone">📞 Phone Number:</label>
<input type="tel" id="phone" name="phone"
placeholder="+1234567890 or 1234567890"
required>
<div class="info" style="margin-top: 5px; font-size: 12px;">
Supports US format: +1234567890 or 1234567890
</div>
</div>
<div class="form-group">
<label for="message">💬 Message:</label>
<textarea id="message" name="message"
placeholder="Enter your message here..."
maxlength="160"
required
oninput="updateCharCount()"></textarea>
<div class="char-count" id="charCount">0/160 characters</div>
</div>
<div class="form-group">
<label for="sender_name">👤 Sender Name (Optional):</label>
<input type="text" id="sender_name" name="sender_name"
placeholder="Your name or organization"
maxlength="50">
</div>
<button type="submit">🚀 Send SMS</button>
</form>
</div>
<script>
function updateCharCount() {
const messageInput = document.getElementById('message');
const charCount = document.getElementById('charCount');
const count = messageInput.value.length;
charCount.textContent = count + '/160 characters';
if (count > 140) {
charCount.style.color = '#FFB6B6';
} else {
charCount.style.color = 'rgba(255, 255, 255, 0.8)';
}
}
// Format phone number as user types
document.getElementById('phone').addEventListener('input', function(e) {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 0 && !value.startsWith('1') && value.length === 10) {
value = '1' + value;
}
if (value.length > 11) {
value = value.substring(0, 11);
}
e.target.value = value;
});
</script>
</body>
</html>`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlTemplate, map[string]any{
"task_id": ctx.Value("task_id"),
})
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
data := map[string]any{
"html_content": rs,
"step": "form",
}
bt, _ := json.Marshal(data)
return mq.Result{Payload: bt, Ctx: ctx}
}
// ValidateInputNode - Validates phone number and message
type ValidateInputNode struct {
dag.Operation
}
func (v *ValidateInputNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{
Error: fmt.Errorf("invalid input data: %v", err),
Ctx: ctx,
}
}
// Extract form data
phone, _ := inputData["phone"].(string)
message, _ := inputData["message"].(string)
senderName, _ := inputData["sender_name"].(string)
// Validate phone number
if phone == "" {
inputData["validation_error"] = "Phone number is required"
inputData["error_field"] = "phone"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Clean and validate phone number format
cleanPhone := regexp.MustCompile(`\D`).ReplaceAllString(phone, "")
// Check for valid US phone number (10 or 11 digits)
if len(cleanPhone) == 10 {
cleanPhone = "1" + cleanPhone // Add country code
} else if len(cleanPhone) != 11 || !strings.HasPrefix(cleanPhone, "1") {
inputData["validation_error"] = "Invalid phone number format. Please use US format: +1234567890 or 1234567890"
inputData["error_field"] = "phone"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Validate message
if message == "" {
inputData["validation_error"] = "Message is required"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
if len(message) > 160 {
inputData["validation_error"] = "Message too long. Maximum 160 characters allowed"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
// Check for potentially harmful content
forbiddenWords := []string{"spam", "scam", "fraud", "hack"}
messageLower := strings.ToLower(message)
for _, word := range forbiddenWords {
if strings.Contains(messageLower, word) {
inputData["validation_error"] = "Message contains prohibited content"
inputData["error_field"] = "message"
bt, _ := json.Marshal(inputData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "invalid"}
}
}
// All validations passed
validatedData := map[string]any{
"phone": cleanPhone,
"message": message,
"sender_name": senderName,
"validated_at": time.Now().Format("2006-01-02 15:04:05"),
"validation_status": "success",
"formatted_phone": formatPhoneForDisplay(cleanPhone),
"char_count": len(message),
}
bt, _ := json.Marshal(validatedData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "valid"}
}
// SendSMSNode - Simulates sending SMS
type SendSMSNode struct {
dag.Operation
}
func (s *SendSMSNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
phone, _ := inputData["phone"].(string)
message, _ := inputData["message"].(string)
senderName, _ := inputData["sender_name"].(string)
// Simulate SMS sending delay
time.Sleep(500 * time.Millisecond)
// Simulate occasional failures for demo purposes
timestamp := time.Now()
success := timestamp.Second()%10 != 0 // 90% success rate
if !success {
errorData := map[string]any{
"phone": phone,
"message": message,
"sender_name": senderName,
"sms_status": "failed",
"error_message": "SMS gateway temporarily unavailable. Please try again.",
"sent_at": timestamp.Format("2006-01-02 15:04:05"),
"retry_suggested": true,
}
bt, _ := json.Marshal(errorData)
return mq.Result{
Payload: bt,
Ctx: ctx,
ConditionStatus: "failed",
}
}
// Generate mock SMS ID and response
smsID := fmt.Sprintf("SMS_%d_%s", timestamp.Unix(), phone[len(phone)-4:])
resultData := map[string]any{
"phone": phone,
"formatted_phone": formatPhoneForDisplay(phone),
"message": message,
"sender_name": senderName,
"sms_status": "sent",
"sms_id": smsID,
"sent_at": timestamp.Format("2006-01-02 15:04:05"),
"delivery_estimate": "1-2 minutes",
"cost_estimate": "$0.02",
"gateway": "MockSMS Gateway",
"char_count": len(message),
}
fmt.Printf("📱 SMS sent successfully! ID: %s, Phone: %s\n", smsID, formatPhoneForDisplay(phone))
bt, _ := json.Marshal(resultData)
return mq.Result{Payload: bt, Ctx: ctx, ConditionStatus: "sent"}
}
// SMSResultNode - Shows successful SMS result
type SMSResultNode struct {
dag.Operation
}
func (r *SMSResultNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
htmlTemplate := `
<!DOCTYPE html>
<html>
<head>
<title>SMS Sent Successfully</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
}
.result-container {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
}
.success-icon {
font-size: 60px;
margin-bottom: 20px;
}
h1 {
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin: 20px 0;
text-align: left;
}
.info-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
}
.info-label {
font-weight: bold;
margin-bottom: 5px;
opacity: 0.8;
}
.info-value {
font-size: 16px;
}
.message-preview {
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
text-align: left;
}
.actions {
margin-top: 30px;
}
.btn {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
color: white;
padding: 12px 25px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin: 0 10px;
text-decoration: none;
display: inline-block;
transition: transform 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.status-badge {
background: #4CAF50;
color: white;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="result-container">
<div class="success-icon">✅</div>
<h1>SMS Sent Successfully!</h1>
<div class="status-badge">{{sms_status}}</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">📱 Phone Number</div>
<div class="info-value">{{formatted_phone}}</div>
</div>
<div class="info-item">
<div class="info-label">🆔 SMS ID</div>
<div class="info-value">{{sms_id}}</div>
</div>
<div class="info-item">
<div class="info-label">⏰ Sent At</div>
<div class="info-value">{{sent_at}}</div>
</div>
<div class="info-item">
<div class="info-label">🚚 Delivery</div>
<div class="info-value">{{delivery_estimate}}</div>
</div>
{{if sender_name}}
<div class="info-item">
<div class="info-label">👤 Sender</div>
<div class="info-value">{{sender_name}}</div>
</div>
{{end}}
<div class="info-item">
<div class="info-label">💰 Cost</div>
<div class="info-value">{{cost_estimate}}</div>
</div>
</div>
<div class="message-preview">
<div class="info-label">💬 Message Sent ({{char_count}} chars):</div>
<div class="info-value" style="margin-top: 10px; font-style: italic;">
"{{message}}"
</div>
</div>
<div class="actions">
<a href="/" class="btn">📱 Send Another SMS</a>
<a href="/api/metrics" class="btn">📊 View Metrics</a>
</div>
<div style="margin-top: 20px; font-size: 12px; opacity: 0.7;">
Gateway: {{gateway}} | Task completed in DAG workflow
</div>
</div>
</body>
</html>`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(htmlTemplate, inputData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
finalData := map[string]any{
"html_content": rs,
"result": inputData,
"step": "success",
}
bt, _ := json.Marshal(finalData)
return mq.Result{Payload: bt, Ctx: ctx}
}
// ErrorPageNode - Shows validation or sending errors
type ErrorPageNode struct {
dag.Operation
}
func (e *ErrorPageNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var inputData map[string]any
if err := json.Unmarshal(task.Payload, &inputData); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
// Determine error type and message
errorMessage, _ := inputData["validation_error"].(string)
errorField, _ := inputData["error_field"].(string)
smsError, _ := inputData["error_message"].(string)
if errorMessage == "" && smsError != "" {
errorMessage = smsError
errorField = "sms_sending"
}
if errorMessage == "" {
errorMessage = "An unknown error occurred"
}
htmlTemplate := `
<!DOCTYPE html>
<html>
<head>
<title>SMS Error</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
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: 30px;
border-radius: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
}
.error-icon {
font-size: 60px;
margin-bottom: 20px;
}
h1 {
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.error-message {
background: rgba(255, 255, 255, 0.2);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
font-size: 16px;
border-left: 4px solid #FFB6B6;
}
.error-details {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
margin: 20px 0;
text-align: left;
}
.actions {
margin-top: 30px;
}
.btn {
background: linear-gradient(45deg, #4ECDC4, #44A08D);
color: white;
padding: 12px 25px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
margin: 0 10px;
text-decoration: none;
display: inline-block;
transition: transform 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.retry-btn {
background: linear-gradient(45deg, #FFA726, #FF9800);
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">❌</div>
<h1>SMS 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.
</div>
{{end}}
{{if retry_suggested}}
<div class="error-details">
<strong>⚠️ Temporary Issue:</strong> This appears to be a temporary gateway issue.
Please try sending your SMS again in a few moments.
</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: 20px; font-size: 12px; opacity: 0.7;">
DAG Error Handler | SMS Workflow Failed
</div>
</div>
</body>
</html>`
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
templateData := map[string]any{
"error_message": errorMessage,
"error_field": errorField,
"retry_suggested": inputData["retry_suggested"],
}
rs, err := parser.ParseTemplate(htmlTemplate, templateData)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
finalData := map[string]any{
"html_content": rs,
"error_data": inputData,
"step": "error",
}
bt, _ := json.Marshal(finalData)
return mq.Result{Payload: bt, Ctx: ctx}
}
// Helper function to format phone number for display
func formatPhoneForDisplay(phone string) string {
if len(phone) == 11 && strings.HasPrefix(phone, "1") {
// Format as +1 (XXX) XXX-XXXX
return fmt.Sprintf("+1 (%s) %s-%s",
phone[1:4],
phone[4:7],
phone[7:11])
}
return phone
}

View File

@@ -1,94 +0,0 @@
package main
import (
"log"
"mime"
"os"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/rewrite"
)
type Config struct {
Prefix string `json:"prefix"`
Root string `json:"root"`
Index string `json:"index"`
UseIndex bool `json:"use_index"`
Compress bool `json:"compress"`
}
func main() {
app := fiber.New()
config := Config{
Prefix: "/data",
Root: "/Users/sujit/Sites/mq/examples/webroot",
UseIndex: true,
Compress: true,
}
New(app, config)
log.Fatal(app.Listen(":3000"))
}
func New(router fiber.Router, cfg ...Config) {
var config Config
if len(cfg) > 0 {
config = cfg[0]
}
if config.Root == "" {
config.Root = "./"
}
if config.Prefix == "/" {
config.Prefix = ""
}
if config.UseIndex && config.Index == "" {
config.Index = "index.html"
}
if config.Compress {
router.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
}
rules := make(map[string]string)
root := filepath.Clean(config.Root)
filepath.WalkDir(config.Root, func(path string, d os.DirEntry, err error) error {
if !d.IsDir() {
path = strings.TrimPrefix(path, root)
rules[path] = filepath.Join(config.Prefix, path)
}
return nil
})
router.Use(rewrite.New(rewrite.Config{Rules: rules}))
router.Get(config.Prefix+"/*", handleStaticFile(config))
}
func handleStaticFile(config Config) fiber.Handler {
return func(c *fiber.Ctx) error {
fullPath := c.Params("*")
filePath := filepath.Join(config.Root, fullPath)
fileInfo, err := os.Stat(filePath)
if err != nil {
return c.Status(fiber.StatusNotFound).SendString("File not found")
}
if fileInfo.IsDir() {
if !config.UseIndex {
return c.Status(fiber.StatusNotFound).SendString("Invalid file")
}
filePath = filepath.Join(filePath, config.Index)
}
fileContent, err := os.ReadFile(filePath)
if err != nil {
return c.Status(fiber.StatusNotFound).SendString("File not found")
}
ext := filepath.Ext(filePath)
mimeType := mime.TypeByExtension(ext)
if mimeType == "" {
mimeType = "application/octet-stream"
}
c.Set("Content-Type", mimeType)
c.Set("Cache-Control", "public, max-age=31536000")
return c.Send(fileContent)
}
}

View File

@@ -1,135 +0,0 @@
package tasks
import (
"context"
"github.com/oarkflow/json"
v2 "github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq"
)
type GetData struct {
v2.Operation
}
func (e *GetData) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
type Loop struct {
v2.Operation
}
func (e *Loop) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
type Condition struct {
v2.Operation
}
func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
err := json.Unmarshal(task.Payload, &data)
if err != nil {
panic(err)
}
switch email := data["email"].(type) {
case string:
if email == "abc.xyz@gmail.com" {
return mq.Result{Payload: task.Payload, ConditionStatus: "pass", Ctx: ctx}
}
return mq.Result{Payload: task.Payload, ConditionStatus: "fail", Ctx: ctx}
default:
return mq.Result{Payload: task.Payload, ConditionStatus: "fail", Ctx: ctx}
}
}
type PrepareEmail struct {
v2.Operation
}
func (e *PrepareEmail) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
err := json.Unmarshal(task.Payload, &data)
if err != nil {
panic(err)
}
data["email_valid"] = true
d, _ := json.Marshal(data)
return mq.Result{Payload: d, Ctx: ctx}
}
type EmailDelivery struct {
v2.Operation
}
func (e *EmailDelivery) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
err := json.Unmarshal(task.Payload, &data)
if err != nil {
panic(err)
}
data["email_sent"] = true
d, _ := json.Marshal(data)
return mq.Result{Payload: d, Ctx: ctx}
}
type SendSms struct {
v2.Operation
}
func (e *SendSms) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
err := json.Unmarshal(task.Payload, &data)
if err != nil {
panic(err)
}
data["sms_sent"] = true
d, _ := json.Marshal(data)
return mq.Result{Payload: d, Ctx: ctx}
}
type StoreData struct {
v2.Operation
}
func (e *StoreData) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
err := json.Unmarshal(task.Payload, &data)
if err != nil {
panic(err)
}
data["stored"] = true
d, _ := json.Marshal(data)
return mq.Result{Payload: d, Ctx: ctx}
}
type InAppNotification struct {
v2.Operation
}
func (e *InAppNotification) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
err := json.Unmarshal(task.Payload, &data)
if err != nil {
panic(err)
}
data["notified"] = true
d, _ := json.Marshal(data)
return mq.Result{Payload: d, Ctx: ctx}
}
type Final struct {
v2.Operation
}
func (e *Final) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
rs := map[string]any{
"html_content": `<strong>Processed successfully!</strong>`,
}
bt, _ := json.Marshal(rs)
return mq.Result{Payload: bt, Ctx: ctx}
}

View File

@@ -1,20 +0,0 @@
package tasks
import (
"context"
"fmt"
"github.com/oarkflow/mq"
)
func SchedulerHandler(ctx context.Context, task *mq.Task) mq.Result {
fmt.Printf("Processing task: %s\n", task.ID)
return mq.Result{Error: nil}
}
func SchedulerCallback(ctx context.Context, result mq.Result) error {
if result.Error != nil {
fmt.Println("Task failed!", result.Error.Error())
}
return nil
}

View File

@@ -1,110 +0,0 @@
package tasks
import (
"context"
"fmt"
"log"
"github.com/oarkflow/json"
v2 "github.com/oarkflow/mq/dag"
"github.com/oarkflow/mq"
)
type Node1 struct{ v2.Operation }
func (t *Node1) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
fmt.Println("Node 1", string(task.Payload))
return mq.Result{Payload: task.Payload, TaskID: task.ID}
}
type Node2 struct{ v2.Operation }
func (t *Node2) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
fmt.Println("Node 2", string(task.Payload))
return mq.Result{Payload: task.Payload, TaskID: task.ID}
}
type Node3 struct{ v2.Operation }
func (t *Node3) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
var user map[string]any
fmt.Println(string(task.Payload))
err := json.Unmarshal(task.Payload, &user)
if err != nil {
panic(err)
}
age := int(user["age"].(float64))
status := "FAIL"
if age > 20 {
status = "PASS"
}
user["status"] = status
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload, ConditionStatus: status}
}
type Node4 struct{ v2.Operation }
func (t *Node4) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
var user map[string]any
_ = json.Unmarshal(task.Payload, &user)
user["node"] = "D"
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload}
}
type Node5 struct{ v2.Operation }
func (t *Node5) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
var user map[string]any
_ = json.Unmarshal(task.Payload, &user)
user["node"] = "E"
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload}
}
type Node6 struct{ v2.Operation }
func (t *Node6) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
var user map[string]any
_ = json.Unmarshal(task.Payload, &user)
resultPayload, _ := json.Marshal(map[string]any{"storage": user})
return mq.Result{Payload: resultPayload}
}
type Node7 struct{ v2.Operation }
func (t *Node7) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
var user map[string]any
_ = json.Unmarshal(task.Payload, &user)
user["node"] = "G"
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload}
}
type Node8 struct{ v2.Operation }
func (t *Node8) ProcessTask(_ context.Context, task *mq.Task) mq.Result {
var user map[string]any
_ = json.Unmarshal(task.Payload, &user)
user["node"] = "H"
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload}
}
func Callback(_ context.Context, task mq.Result) mq.Result {
fmt.Println("Received task", task.TaskID, "Payload", string(task.Payload), task.Error, task.Topic)
return mq.Result{}
}
func NotifyResponse(_ context.Context, result mq.Result) error {
log.Printf("DAG - FINAL_RESPONSE ~> TaskID: %s, Payload: %s, Topic: %s, Error: %v, Latency: %s", result.TaskID, result.Payload, result.Topic, result.Error, result.Latency)
return nil
}
func NotifySubDAGResponse(_ context.Context, result mq.Result) error {
log.Printf("SUB DAG - FINAL_RESPONSE ~> TaskID: %s, Payload: %s, Topic: %s, Error: %v, Latency: %s", result.TaskID, result.Payload, result.Topic, result.Error, result.Latency)
return nil
}

View File

@@ -1,189 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"github.com/oarkflow/json"
"github.com/gofiber/fiber/v2"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
"github.com/oarkflow/jet"
"github.com/oarkflow/mq/consts"
)
type Form struct {
dag.Operation
}
func (p *Form) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
baseURI := ""
if dg, ok := task.GetFlow().(*dag.DAG); ok {
baseURI = dg.BaseURI()
}
bt, err := os.ReadFile("webroot/form.html")
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(string(bt), map[string]any{
"task_id": ctx.Value("task_id"),
"base_uri": baseURI,
})
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
data := map[string]any{
"html_content": rs,
}
bt, _ = json.Marshal(data)
return mq.Result{Payload: bt, Ctx: ctx}
}
type NodeA struct {
dag.Operation
}
func (p *NodeA) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
data["allowed_voting"] = data["age"] == "18"
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type NodeB struct {
dag.Operation
}
func (p *NodeB) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
data["female_voter"] = data["gender"] == "female"
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type NodeC struct {
dag.Operation
}
func (p *NodeC) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
var data map[string]any
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
data["voted"] = true
updatedPayload, _ := json.Marshal(data)
return mq.Result{Payload: updatedPayload, Ctx: ctx}
}
type Result struct {
dag.Operation
}
func (p *Result) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
baseURI := ""
if dg, ok := task.GetFlow().(*dag.DAG); ok {
baseURI = dg.BaseURI()
}
bt, err := os.ReadFile("webroot/result.html")
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
data := map[string]any{
"base_uri": baseURI,
}
if task.Payload != nil {
if err := json.Unmarshal(task.Payload, &data); err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
}
if bt != nil {
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
rs, err := parser.ParseTemplate(string(bt), data)
if err != nil {
return mq.Result{Error: err, Ctx: ctx}
}
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
data := map[string]any{
"html_content": rs,
}
bt, _ := json.Marshal(data)
return mq.Result{Payload: bt, Ctx: ctx}
}
return mq.Result{Payload: task.Payload, Ctx: ctx}
}
// RemoveHTMLContent recursively removes the "html_content" field from the given JSON.
func RemoveHTMLContent(data json.RawMessage, field string) (json.RawMessage, error) {
var result interface{}
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
removeField(result, field)
return json.Marshal(result)
}
// removeField recursively traverses the structure and removes "html_content" field.
func removeField(v interface{}, field string) {
switch v := v.(type) {
case map[string]interface{}:
// Check if the field is in the map and remove it.
delete(v, field)
// Recursively remove the field from nested objects.
for _, value := range v {
removeField(value, field)
}
case []interface{}:
// If it's an array, recursively process each item.
for _, item := range v {
removeField(item, field)
}
}
}
func notify(taskID string, result mq.Result) {
filteredData, err := RemoveHTMLContent(result.Payload, "html_content")
if err != nil {
panic(err)
}
fmt.Printf("Final result for task %s: %s, status: %s, latency: %s\n", taskID, string(filteredData), result.Status, result.Latency)
}
func main() {
flow := dag.NewDAG("Sample DAG", "sample-dag", notify, mq.WithBrokerURL(":8083"), mq.WithHTTPApi(true))
flow.AddNode(dag.Page, "Form", "Form", &Form{})
flow.AddNode(dag.Function, "NodeA", "NodeA", &NodeA{})
flow.AddNode(dag.Function, "NodeB", "NodeB", &NodeB{})
flow.AddNode(dag.Function, "NodeC", "NodeC", &NodeC{})
flow.AddNode(dag.Page, "Result", "Result", &Result{})
flow.AddEdge(dag.Simple, "Form", "Form", "NodeA")
flow.AddEdge(dag.Simple, "NodeA", "NodeA", "NodeB")
flow.AddEdge(dag.Simple, "NodeB", "NodeB", "NodeC")
flow.AddEdge(dag.Simple, "NodeC", "NodeC", "Result")
dag.AddHandler("Form", func(s string) mq.Processor {
opt := dag.Operation{
Tags: []string{"built-in", "form"},
}
return &Form{Operation: opt}
})
fmt.Println(dag.AvailableHandlers())
if flow.Error != nil {
panic(flow.Error)
}
app := fiber.New()
flowApp := app.Group("/")
flow.Handlers(flowApp, "/")
app.Listen(":8082")
}

View File

@@ -1,25 +0,0 @@
#container{
width: 400px;
margin: 30px auto 0 auto;
}
#messages{
height: 200px;
overflow-y: scroll;
}
.control-item{
margin-top: 15px;
}
#message-send-btn, #clear-messages-btn{
width: 100%;
}
#message-list{
padding-left: 25px;
}
.message-item{
margin-bottom: 8px;
}

View File

@@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Data Form</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f7fc;
}
h1 {
text-align: center;
color: #333;
padding-top: 20px;
}
.container {
width: 80%;
max-width: 600px;
margin: 0 auto;
padding: 30px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
form {
display: grid;
gap: 15px;
}
label {
font-size: 16px;
color: #333;
}
input, select {
width: 100%;
padding: 10px;
font-size: 16px;
border-radius: 4px;
border: 1px solid #ccc;
box-sizing: border-box;
}
input:focus, select:focus {
border-color: #0066cc;
outline: none;
}
input[type="submit"] {
background-color: #0066cc;
color: white;
font-size: 16px;
cursor: pointer;
border: none;
padding: 12px;
border-radius: 4px;
transition: background-color 0.3s;
}
input[type="submit"]:hover {
background-color: #005bb5;
}
.footer {
text-align: center;
margin-top: 40px;
padding-bottom: 20px;
font-size: 14px;
color: #555;
}
</style>
</head>
<body>
<h1>Enter Your Information</h1>
<div class="container" id="result">
<form action="{{base_uri}}/process?task_id={{task_id}}&next=true" method="POST">
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" value="s.baniya.np@gmail.com" required>
</div>
<div>
<label for="age">Age:</label>
<input type="number" id="age" name="age" value="18" required>
</div>
<div>
<label for="gender">Gender:</label>
<select id="gender" name="gender" required>
<option value="male">Male</option>
<option value="female">Female</option>
<option value="other">Other</option>
</select>
</div>
<div>
<input type="submit" value="Submit">
</div>
</form>
</div>
<div class="footer">
<p>&copy; 2024 Task Manager</p>
</div>
</body>
</html>

View File

@@ -1,117 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Status Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.scrollbar {
scrollbar-width: thin;
scrollbar-color: #D1D5DB #E5E7EB;
}
.scrollbar::-webkit-scrollbar {
width: 8px;
}
.scrollbar::-webkit-scrollbar-track {
background: #E5E7EB;
}
.scrollbar::-webkit-scrollbar-thumb {
background-color: #D1D5DB;
border-radius: 9999px;
}
svg path {
transition: fill 0.3s ease;
}
svg path:hover {
opacity: 0.8;
}
.popover {
position: absolute;
z-index: 10;
display: none;
background-color: white;
border: 1px solid #E5E7EB;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
padding: 10px;
width: 200px;
border-radius: 8px;
}
.popover.visible {
display: block;
}
#dag-diagram {
width: 100%;
height: auto;
}
</style>
</head>
<body class="bg-gray-100 h-screen p-4">
<div class="w-[95vw] mx-auto p-6 bg-white shadow-lg rounded-lg">
<div class="flex flex-col gap-2">
<div class="min-h-96">
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="col-span-1 w-full flex justify-center">
<div class="flex flex-col space-y-4">
<div class="p-4 border rounded-md flex items-center justify-center">
<div class="bg-gray-50">
<img id="svg-container" class="w-full" src="" alt="">
</div>
</div>
</div>
</div>
<div class="col-span-3 w-full flex flex-col">
<div class=" flex justify-between">
<textarea id="payload" class="w-full p-2 border rounded-md" placeholder="Enter your JSON Payload here">[{"phone": "+123456789", "email": "abc.xyz@gmail.com"}, {"phone": "+98765412", "email": "xyz.abc@gmail.com"}]</textarea>
<button id="send-request" class="ml-4 py-2 px-6 bg-green-500 text-white rounded-md">Send</button>
</div>
<div id="response">
<h1 class="text-xl font-semibold text-gray-700 mb-4">Table</h1>
<div class="overflow-auto scrollbar h-48">
<table class="min-w-full border-collapse border border-gray-300">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 border border-gray-300">Task ID</th>
<th class="px-4 py-2 border border-gray-300">Created At</th>
<th class="px-4 py-2 border border-gray-300">Processed At</th>
<th class="px-4 py-2 border border-gray-300">Latency</th>
<th class="px-4 py-2 border border-gray-300">Status</th>
<th class="px-4 py-2 border border-gray-300">View</th>
</tr>
</thead>
<tbody id="taskTableBody">
<!-- Dynamic rows will be appended here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div>
<div class="mt-6">
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="col-span-3">
<h2 class="text-lg font-semibold text-gray-700 mb-2">DAG Image view per task</h2>
<div id="task-svg" class="border border-gray-300 p-4 bg-gray-50 min-h-64 flex items-center justify-center">
<svg id="dag-diagram" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
</div>
<div class="col-span-1">
<h2 class="text-lg font-semibold text-gray-700 mb-2">Node Result: <span id="task-id"></span></h2>
<div id="svg-popover" class="p-4 border rounded-md min-h-48 flex flex-wrap justify-center">
<p>Node Result per task</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/socket.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -1,186 +0,0 @@
(function(SS) {
'use strict';
let uiContent
function loadSVG(url) {
fetch(url)
.then(response => response.text())
.then(svgContent => {
const container = document.getElementById('svg-container');
container.src = url;
uiContent = svgContent;
})
.catch(err => console.error('Failed to load SVG:', err));
}
window.onload = function() {
loadSVG('/ui');
};
document.getElementById('send-request').addEventListener('click', function() {
const input = document.getElementById('payload');
const payloadData = JSON.parse(input.value);
const data = { payload: payloadData };
fetch('/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes("text/html")) {
return response.text().then(html => ({ html }));
} else {
return response.json();
}
})
.then(data => {
if (data.html) {
const el = document.getElementById("response");
el.innerHTML = data.html;
} else {
console.log("Success", data);
}
})
.catch(error => console.error('Error:', error));
});
const tasks = {};
function attachSVGNodeEvents() {
const svgNodes = document.querySelectorAll('g.node'); // Adjust selector as per your SVG structure
svgNodes.forEach(node => {
node.classList.add("cursor-pointer")
node.addEventListener('click', handleSVGNodeClick);
});
}
// Function to handle the click on an SVG node and show the popover
function handleSVGNodeClick(event) {
const nodeId = event.currentTarget.id; // Get the node ID (e.g., 'node_store:data')
const nodeData = findNodeDataById(nodeId); // Fetch data related to the node (status, result, etc.)
if (nodeData) {
showSVGPopover(event, nodeData);
}
}
// Function to show the popover next to the clicked SVG node
function showSVGPopover(event, nodeData) {
const popover = document.getElementById('svg-popover');
document.getElementById('task-id').innerHTML = nodeData.task_id
popover.classList.add('visible');
popover.innerHTML = `
<div class="text-sm text-gray-600 space-y-1">
<div class="grid grid-cols-5 gap-x-4">
<strong class="text-gray-800 col-span-1">Node:</strong>
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.topic}</span>
<strong class="text-gray-800 col-span-1">Status:</strong>
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.status}</span>
<strong class="text-gray-800 col-span-1">Result:</strong>
<pre class="text-gray-700 col-span-4 p-2 border border-gray-300 rounded-md max-h-32 overflow-auto bg-gray-50">${JSON.stringify(nodeData.payload, null, 2)}
</pre>
<strong class="text-gray-800 col-span-1">Error:</strong>
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.error || 'N/A'}</span>
<strong class="text-gray-800 col-span-1">Created At:</strong>
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.created_at}</span>
<strong class="text-gray-800 col-span-1">Processed At:</strong>
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.processed_at}</span>
<strong class="text-gray-800 col-span-1">Latency:</strong>
<span class="text-gray-700 flex-grow break-words col-span-4">${nodeData.latency}</span>
</div>
</div>
`;
}
// Function to find node data (status, result, error) by the node's ID
function findNodeDataById(nodeId) {
for (const taskId in tasks) {
const task = tasks[taskId];
const node = task.nodes.find(n => `node_${n.topic}` === nodeId); // Ensure the ID format matches your SVG
if (node) {
return node;
}
}
return null; // Return null if no matching node is found
}
function addOrUpdateTask(message, isFinal = false) {
const taskTableBody = document.getElementById('taskTableBody');
const taskId = message.task_id;
const rowId = `row-${taskId}`;
let existingRow = document.getElementById(rowId);
if (!existingRow) {
const row = document.createElement('tr');
row.id = rowId;
taskTableBody.insertBefore(row, taskTableBody.firstChild);
existingRow = row;
}
tasks[taskId] = tasks[taskId] || { nodes: [], final: null };
if (isFinal) tasks[taskId].final = message;
else tasks[taskId].nodes.push(message);
const latestStatus = isFinal ? message.status : message.status;
const statusColor = latestStatus === 'success' ? 'bg-green-100 text-green-700' :
latestStatus === 'fail' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700';
existingRow.innerHTML = `
<td class="px-4 py-2 border border-gray-300">${taskId}</td>
<td class="px-4 py-2 border border-gray-300">${new Date(message.created_at).toLocaleString()}</td>
<td class="px-4 py-2 border border-gray-300">${new Date(message.processed_at).toLocaleString()}</td>
<td class="px-4 py-2 border border-gray-300">${message.latency}</td>
<td class="px-4 py-2 border border-gray-300 ${statusColor}">${latestStatus}</td>
<td class="px-4 py-2 border border-gray-300">
<button class="view-btn text-blue-600 hover:underline" data-task-id='${taskId}'>View</button>
</td>
`;
attachViewButtonEvent();
}
function attachViewButtonEvent() {
const buttons = document.querySelectorAll('.view-btn');
buttons.forEach(button => {
button.removeEventListener('click', handleViewButtonClick);
button.addEventListener('click', handleViewButtonClick);
});
}
function handleViewButtonClick(event) {
document.getElementById("task-svg").innerHTML = uiContent
attachSVGNodeEvents();
const taskId = event.target.getAttribute('data-task-id');
const task = tasks[taskId];
updateSVGNodes(task);
}
function updateSVGNodes(task) {
console.log(task)
task.nodes.forEach((node) => {
const svgNode = document.querySelector(`#node_${node.topic.replace(':', '\\:')}`);
console.log(svgNode)
if (svgNode) {
const fillColor = node.status === 'success' ? '#A5D6A7' : node.status === 'fail' ? '#EF9A9A' : '#FFE082';
const path = svgNode.querySelector('path');
if (path) path.setAttribute('fill', fillColor);
}
});
}
let ss = new SS('ws://' + window.location.host + '/notify');
ss.onConnect(() => ss.emit('join', "global"));
ss.onDisconnect(() => alert('chat disconnected'));
ss.on('message', msg => addOrUpdateTask(msg, false));
ss.on('final-message', msg => addOrUpdateTask(msg, true));
})(window.SS);

View File

@@ -1,233 +0,0 @@
if (typeof window === 'undefined') {
var window = {};
}
if (typeof module === 'undefined') {
var module = {};
}
(function (window, module) {
'use strict';
var SS = function (url, opts) {
opts = opts || {};
var self = this,
events = {},
reconnectOpts = { enabled: true, replayOnConnect: true, intervalMS: 5000 },
reconnecting = false,
connectedOnce = false,
headerStartCharCode = 1,
headerStartChar = String.fromCharCode(headerStartCharCode),
dataStartCharCode = 2,
dataStartChar = String.fromCharCode(dataStartCharCode),
subProtocol = 'sac-sock',
ws = new WebSocket(url, subProtocol);
//blomp blomp-a noop noop a-noop noop noop
self.noop = function () { };
//we really only support reconnect options for now
if (typeof opts.reconnectOpts == 'object') {
for (var i in opts.reconnectOpts) {
if (!opts.reconnectOpts.hasOwnProperty(i)) continue;
reconnectOpts[i] = opts.reconnectOpts[i];
}
}
//sorry, only supporting arraybuffer at this time
//maybe if there is demand for it, I'll add Blob support
ws.binaryType = 'arraybuffer';
//Parses all incoming messages and dispatches their payload to the appropriate eventName if one has been registered. Messages received for unregistered events will be ignored.
ws.onmessage = function (e) {
var msg = e.data,
headers = {},
eventName = '',
data = '',
chr = null,
i, msgLen;
if (typeof msg === 'string') {
var dataStarted = false,
headerStarted = false;
for (i = 0, msgLen = msg.length; i < msgLen; i++) {
chr = msg[i];
if (!dataStarted && !headerStarted && chr !== dataStartChar && chr !== headerStartChar) {
eventName += chr;
} else if (!headerStarted && chr === headerStartChar) {
headerStarted = true;
} else if (headerStarted && !dataStarted && chr !== dataStartChar) {
headers[chr] = true;
} else if (!dataStarted && chr === dataStartChar) {
dataStarted = true;
} else {
data += chr;
}
}
} else if (msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined) {
var dv = new DataView(msg),
headersStarted = false;
for (i = 0, msgLen = dv.byteLength; i < msgLen; i++) {
chr = dv.getUint8(i);
if (chr !== dataStartCharCode && chr !== headerStartCharCode && !headersStarted) {
eventName += String.fromCharCode(chr);
} else if (chr === headerStartCharCode && !headersStarted) {
headersStarted = true;
} else if (headersStarted && chr !== dataStartCharCode) {
headers[String.fromCharCode(chr)] = true;
} else if (chr === dataStartCharCode) {
data = dv.buffer.slice(i + 1);
break;
}
}
}
if (eventName.length === 0) return; //no event to dispatch
if (typeof events[eventName] === 'undefined') return;
events[eventName].call(self, (headers.J) ? JSON.parse(data) : data);
};
/**
* startReconnect is an internal function for reconnecting after an unexpected disconnect
*
* @function startReconnect
*
*/
function startReconnect() {
setTimeout(function () {
console.log('attempting reconnect');
var newWS = new WebSocket(url, subProtocol);
newWS.onmessage = ws.onmessage;
newWS.onclose = ws.onclose;
newWS.binaryType = ws.binaryType;
//we need to run the initially set onConnect function on first successful connect,
//even if replayOnConnect is disabled. The server might not be available on first
//connection attempt.
if (reconnectOpts.replayOnConnect || !connectedOnce) {
newWS.onopen = ws.onopen;
}
ws = newWS;
if (!reconnectOpts.replayOnConnect && connectedOnce) {
self.onConnect(self.noop);
}
}, reconnectOpts.intervalMS);
}
/**
* onConnect registers a callback to be run when the websocket connection is open.
*
* @method onConnect
* @param {Function} callback(event) - The callback that will be executed when the websocket connection opens.
*
*/
self.onConnect = function (callback) {
ws.onopen = function () {
connectedOnce = true;
var args = arguments;
callback.apply(self, args);
if (reconnecting) {
reconnecting = false;
}
};
};
self.onConnect(self.noop);
/**
* onDisconnect registers a callback to be run when the websocket connection is closed.
*
* @method onDisconnect
* @param {Function} callback(event) - The callback that will be executed when the websocket connection is closed.
*/
self.onDisconnect = function (callback) {
ws.onclose = function () {
var args = arguments;
if (!reconnecting && connectedOnce) {
callback.apply(self, args);
}
if (reconnectOpts.enabled) {
reconnecting = true;
startReconnect();
}
};
};
self.onDisconnect(self.noop);
/**
* on registers an event to be called when the client receives an emit from the server for
* the given eventName.
*
* @method on
* @param {String} eventName - The name of the event being registerd
* @param {Function} callback(payload) - The callback that will be ran whenever the client receives an emit from the server for the given eventName. The payload passed into callback may be of type String, Object, or ArrayBuffer
*
*/
self.on = function (eventName, callback) {
events[eventName] = callback;
};
/**
* off unregisters an emit event
*
* @method off
* @param {String} eventName - The name of event being unregistered
*/
self.off = function (eventName) {
if (events[eventName]) {
delete events[eventName];
}
};
/**
* emit dispatches an event to the server
*
* @method emit
* @param {String} eventName - The event to dispatch
* @param {String|Object|ArrayBuffer} data - The data to be sent to the server. If data is a string then it will be sent as a normal string to the server. If data is an object it will be converted to JSON before being sent to the server. If data is an ArrayBuffer then it will be sent to the server as a uint8 binary payload.
*/
self.emit = function (eventName, data) {
var rs = ws.readyState;
if (rs === 0) {
console.warn("websocket is not open yet");
return;
} else if (rs === 2 || rs === 3) {
console.error("websocket is closed");
return;
}
var msg = '';
if (data instanceof ArrayBuffer) {
var ab = new ArrayBuffer(data.byteLength + eventName.length + 1),
newBuf = new DataView(ab),
oldBuf = new DataView(data),
i = 0;
for (var evtLen = eventName.length; i < evtLen; i++) {
newBuf.setUint8(i, eventName.charCodeAt(i));
}
newBuf.setUint8(i, dataStartCharCode);
i++;
for (var x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++) {
newBuf.setUint8(i, oldBuf.getUint8(x));
}
msg = ab;
} else if (typeof data === 'object') {
msg = eventName + dataStartChar + JSON.stringify(data);
} else {
msg = eventName + dataStartChar + data;
}
ws.send(msg);
};
/**
* close will close the websocket connection, calling the "onDisconnect" event if one has been registered.
*
* @method close
*/
self.close = function () {
reconnectOpts.enabled = false; //don't reconnect if close is called
return ws.close(1000);
};
};
window.SS = SS;
module.exports = SS;
})(window, module);

View File

@@ -1,239 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Result</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f7fc;
}
h1 {
text-align: center;
color: #333;
padding-top: 30px;
}
.container {
width: 90%;
max-width: 900px;
margin: 0 auto;
padding: 30px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table th, table td {
padding: 15px;
border: 1px solid #ddd;
text-align: left;
vertical-align: top;
}
table th {
background-color: #f1f1f1;
color: #333;
font-weight: bold;
}
table tr:nth-child(even) {
background-color: #f9f9f9;
}
table td pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.status-pending {
color: orange;
font-weight: bold;
}
.status-processing {
color: blue;
font-weight: bold;
}
.status-completed {
color: green;
font-weight: bold;
}
.status-failed {
color: red;
font-weight: bold;
}
.node-result {
margin-top: 30px;
}
.node-result h2 {
color: #333;
margin-bottom: 20px;
}
.footer {
text-align: center;
margin-top: 40px;
padding-bottom: 20px;
font-size: 14px;
color: #555;
}
.footer a {
text-decoration: none;
color: #0066cc;
}
.footer a:hover {
text-decoration: underline;
}
.error-message {
color: red;
font-size: 18px;
margin-top: 20px;
}
.success-message {
color: green;
font-size: 18px;
margin-top: 20px;
}
.go-back {
display: inline-block;
margin-top: 10px;
padding: 10px 20px;
background-color: #0066cc;
color: white;
border-radius: 5px;
text-decoration: none;
font-weight: bold;
}
.go-back:hover {
background-color: #005bb5;
}
</style>
</head>
<body>
<h1>Task Result</h1>
<div class="container" id="result">
<p>Loading result...</p>
</div>
<div class="footer">
<p>&copy; 2024 Task Manager</p>
</div>
<script>
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleString();
}
// Fetch the task result
const taskID = new URLSearchParams(window.location.search).get('task_id'); // Get taskID from URL
if (taskID) {
fetch(`{{base_uri}}/task/status?taskID=${taskID}`)
.then(response => response.json())
.then(data => {
if(data?.message) {
document.getElementById('result').innerHTML = `
<p class="error-message">Error loading task result: ${data.message}</p>
<a href="/form" class="go-back">Go back</a>`;
} else {
const container = document.getElementById('result');
let htmlContent = '';
htmlContent += `
<h2 style="display: flex; justify-content: space-between"><span>Final Task Result</span><span><a href="/process">Go Back</a></span></h2>
<table>
<tr>
<th>Task ID</th>
<th>Status</th>
<th>UpdatedAt</th>
<th>Result</th>
</tr>
<tr>
<td>${taskID}</td>
<td class="${getStatusClass(data.Result.Status)}">${data.Result.Status}</td>
<td>${formatDate(data.Result.UpdatedAt)}</td>
<td><pre>${JSON.stringify(data.Result.Result.payload, null, 2)}</pre></td>
</tr>
</table>
`;
htmlContent += `
<div class="node-result">
<h2>Result Per Node</h2>
<table>
<tr>
<th>Node ID</th>
<th>Status</th>
<th>UpdatedAt</th>
<th>Node Result Data</th>
</tr>
`;
for (const nodeID in data) {
if (nodeID !== "Result") {
const node = data[nodeID];
htmlContent += `
<tr>
<td>${node.NodeID}</td>
<td class="${getStatusClass(node.Status)}">${node.Status}</td>
<td>${formatDate(node.UpdatedAt)}</td>
<td><pre>${JSON.stringify(node.Result.payload, null, 2)}</pre></td>
</tr>
`;
}
}
htmlContent += '</table></div>';
container.innerHTML = htmlContent;
}
})
.catch(error => {
console.log(error)
document.getElementById('result').innerHTML = '<p class="error-message">Error loading task result.</p>';
});
} else {
document.getElementById('result').innerHTML = '<p class="error-message">Task ID not provided.</p>';
}
function getStatusClass(status) {
switch (status) {
case 'Pending':
return 'status-pending';
case 'Processing':
return 'status-processing';
case 'Completed':
return 'status-completed';
case 'Failed':
return 'status-failed';
default:
return '';
}
}
</script>
</body>
</html>

8
go.mod
View File

@@ -12,9 +12,9 @@ require (
github.com/oarkflow/form v0.0.0-20241203111156-b1be5636af43
github.com/oarkflow/jet v0.0.4
github.com/oarkflow/json v0.0.28
github.com/oarkflow/log v1.0.79
github.com/oarkflow/log v1.0.83
github.com/oarkflow/xid v1.2.8
golang.org/x/crypto v0.33.0
golang.org/x/crypto v0.41.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/time v0.11.0
)
@@ -25,7 +25,7 @@ require (
github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect
github.com/kaptinlin/go-i18n v0.1.4 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/text v0.28.0 // indirect
)
require (
@@ -40,5 +40,5 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/sys v0.35.0 // indirect
)

8
go.sum
View File

@@ -46,6 +46,8 @@ github.com/oarkflow/jsonschema v0.0.4 h1:n5Sb7WVb7NNQzn/ei9++4VPqKXCPJhhsHeTGJkI
github.com/oarkflow/jsonschema v0.0.4/go.mod h1:AxNG3Nk7KZxnnjRJlHLmS1wE9brtARu5caTFuicCtnA=
github.com/oarkflow/log v1.0.79 h1:DxhtkBGG+pUu6cudSVw5g75FbKEQJkij5w7n5AEN00M=
github.com/oarkflow/log v1.0.79/go.mod h1:U/4chr1DyOiQvS6JiQpjYTCJhK7RGR8xrXPsGlouLzM=
github.com/oarkflow/log v1.0.83 h1:T/38wvjuNeVJ9PDo0wJDTnTUQZ5XeqlcvpbCItuFFJo=
github.com/oarkflow/log v1.0.83/go.mod h1:dMn57z9uq11Y264cx9c9Ac7ska9qM+EBhn4qf9CNlsM=
github.com/oarkflow/xid v1.2.8 h1:uCIX61Binq2RPMsqImZM6pPGzoZTmRyD6jguxF9aAA0=
github.com/oarkflow/xid v1.2.8/go.mod h1:jG4YBh+swbjlWApGWDBYnsJEa7hi3CCpmuqhB3RAxVo=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -65,13 +67,19 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

File diff suppressed because one or more lines are too long

View File

@@ -1,42 +0,0 @@
<!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">
{{form_groups}}
<div class="mt-4 flex gap-2">
{{form_buttons}}
</div>
</div>
</form>
</body>
</html>

View File

@@ -1,134 +0,0 @@
<!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

@@ -1,21 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,88 +0,0 @@
{
"name": "Login Flow",
"key": "login:flow",
"nodes": [
{
"id": "LoginForm",
"first_node": true,
"node": "render-html",
"data": {
"additional_data": {
"schema_file": "login.json",
"template_file": "app/templates/basic.html"
}
}
},
{
"id": "ValidateLogin",
"node": "condition",
"data": {
"mapping": {
"username": "username",
"password": "password"
},
"additional_data": {
"conditions": {
"default": {
"id": "condition:default",
"node": "output"
},
"invalid": {
"id": "condition:invalid_login",
"node": "error-page",
"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": "app/templates/error.html"
}
}
},
{
"id": "output",
"node": "output",
"data": {
"additional_data": {
"output_type": "file",
"file_path": "app/data/login_output.json"
},
"mapping": {
"login_message": "eval.{{'Login successful!'}}"
}
}
}
],
"edges": [
{
"source": "LoginForm",
"target": [ "ValidateLogin" ]
}
]
}

View File

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

View File

@@ -1,29 +0,0 @@
{
"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": "app/templates/basic.html"
}
}
}
],
"edges": [
{
"source": "Login.output",
"label": "on_success",
"target": [ "ContactForm" ]
}
]
}

View File

@@ -1,63 +0,0 @@
{
"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

@@ -1,105 +0,0 @@
{
"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": "/process?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

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

View File

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

View File

@@ -1,36 +0,0 @@
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(fiber.Config{EnablePrintRoutes: true})
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) })
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RAM LIMO LLC.</title>
<script type="module" crossorigin src="/assets/index-B0w5Q249.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bc6Em-gk.css">
</head>
<body>
<div id="root"></div>
</body>
</html>