From 0aba36da71ae499cfc1427147ccb94778a85e253 Mon Sep 17 00:00:00 2001 From: Kushal Date: Wed, 21 Jan 2026 17:46:04 +0000 Subject: [PATCH 01/39] feat: update js sdk to include advanced templating Signed-off-by: Kushal --- .../js-sdk/examples/advanced-templating.ts | 512 ++++++++++++++++++ packages/js-sdk/src/index.ts | 2 + packages/js-sdk/src/modules/template.ts | 310 +++++++++++ packages/js-sdk/src/types/template.ts | 216 ++++++++ 4 files changed, 1040 insertions(+) create mode 100644 packages/js-sdk/examples/advanced-templating.ts create mode 100644 packages/js-sdk/src/modules/template.ts create mode 100644 packages/js-sdk/src/types/template.ts diff --git a/packages/js-sdk/examples/advanced-templating.ts b/packages/js-sdk/examples/advanced-templating.ts new file mode 100644 index 0000000..9faeaa7 --- /dev/null +++ b/packages/js-sdk/examples/advanced-templating.ts @@ -0,0 +1,512 @@ +/** + * TurboTemplate Advanced Templating Examples + * + * This file demonstrates the advanced templating features introduced + * in the RapidDocxBackend PR #1057. + * + * Key points for variable configuration: + * - Placeholders should include curly braces: "{variable_name}" + * - For objects/arrays, use mimeType: 'json' + * - For expressions on simple values, use mimeType: 'text' with usesAdvancedTemplatingEngine: true + * - Boolean/number values with mimeType: 'json' work for conditionals + */ + +import { TurboTemplate } from '../src'; + +// Configure the client +TurboTemplate.configure({ + apiKey: process.env.TURBODOCX_API_KEY!, + orgId: process.env.TURBODOCX_ORG_ID!, +}); + +/** + * Example 1: Simple Variable Substitution + * + * Template: "Dear {firstName}, your email is {simpleEmail}." + */ +async function simpleSubstitution() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Simple Substitution Document', + description: 'Basic variable substitution example', + variables: [ + { placeholder: '{firstName}', name: 'firstName', mimeType: 'text', value: 'Foo' }, + { placeholder: '{lastName}', name: 'lastName', mimeType: 'text', value: 'Bar' }, + { placeholder: '{simpleEmail}', name: 'simpleEmail', mimeType: 'text', value: 'foo.bar@example.com' }, + ], + }); + + console.log('Document generated:', result.deliverableId); +} + +/** + * Example 2: Nested Objects with Dot Notation + * + * Template: "Name: {user.firstName}, Email: {user.email}" + */ +async function nestedObjects() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Nested Objects Document', + description: 'Nested object with dot notation example', + variables: [ + { + placeholder: '{user}', + name: 'user', + mimeType: 'json', + value: { + firstName: 'Foo', + email: 'foo@example.com', + }, + }, + ], + }); + + console.log('Document with nested data generated:', result.deliverableId); +} + +/** + * Example 3: Deep Nested Objects + * + * Template: "Team Lead: {company.divisions.engineering.teamLead.name}" + */ +async function deepNestedObjects() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Deep Nested Objects Document', + description: 'Deep nested object example', + variables: [ + { + placeholder: '{company}', + name: 'company', + mimeType: 'json', + value: { + divisions: { + engineering: { + teamLead: { + name: 'Person A', + contact: { + phone: '+1-555-0000', + email: 'persona@example.com', + }, + }, + }, + }, + }, + }, + ], + }); + + console.log('Document with deep nested data generated:', result.deliverableId); +} + +/** + * Example 4: Array Loops + * + * Template: + * {#products} + * - {name}: ${price} + * {/products} + */ +async function loopsAndArrays() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Array Loops Document', + description: 'Array loop iteration example', + variables: [ + { + placeholder: '{products}', + name: 'products', + mimeType: 'json', + value: [ + { name: 'Item A', price: 999 }, + { name: 'Item B', price: 29 }, + { name: 'Item C', price: 79 }, + ], + }, + ], + }); + + console.log('Document with loop generated:', result.deliverableId); +} + +/** + * Example 5: Conditionals with Boolean Values + * + * Template: + * {#isActive}User is active{/isActive} + * {^isActive}User is inactive{/isActive} + */ +async function conditionals() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Conditionals Document', + description: 'Boolean conditional example', + variables: [ + { placeholder: '{isActive}', name: 'isActive', mimeType: 'json', value: true }, + { placeholder: '{isPremium}', name: 'isPremium', mimeType: 'json', value: false }, + { placeholder: '{score}', name: 'score', mimeType: 'json', value: 85 }, + ], + }); + + console.log('Document with conditionals generated:', result.deliverableId); +} + +/** + * Example 6: Expressions and Calculations + * + * For arithmetic expressions, use mimeType: 'text' with string/number values + * and usesAdvancedTemplatingEngine: true + * + * Template: "Total: {price + tax}", "Result: {a + b}" + */ +async function expressionsAndCalculations() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Expressions Document', + description: 'Arithmetic expressions example', + variables: [ + { + placeholder: '{price}', + name: 'price', + mimeType: 'text', + value: '100', + usesAdvancedTemplatingEngine: true, + }, + { + placeholder: '{tax}', + name: 'tax', + mimeType: 'text', + value: '15', + usesAdvancedTemplatingEngine: true, + }, + { + placeholder: '{a}', + name: 'a', + mimeType: 'text', + value: 20, + usesAdvancedTemplatingEngine: true, + }, + { + placeholder: '{b}', + name: 'b', + mimeType: 'text', + value: 0, + usesAdvancedTemplatingEngine: true, + }, + ], + }); + + console.log('Document with expressions generated:', result.deliverableId); +} + +/** + * Example 7: Complex Expressions + * + * Template: "Final: {basePrice * quantity + shipping - discount}" + */ +async function complexExpressions() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Complex Expressions Document', + description: 'Complex arithmetic expressions example', + variables: [ + { placeholder: '{basePrice}', name: 'basePrice', mimeType: 'text', value: '50', usesAdvancedTemplatingEngine: true }, + { placeholder: '{quantity}', name: 'quantity', mimeType: 'text', value: '3', usesAdvancedTemplatingEngine: true }, + { placeholder: '{shipping}', name: 'shipping', mimeType: 'text', value: '10', usesAdvancedTemplatingEngine: true }, + { placeholder: '{discount}', name: 'discount', mimeType: 'text', value: '25', usesAdvancedTemplatingEngine: true }, + ], + }); + + console.log('Document with complex expressions generated:', result.deliverableId); +} + +/** + * Example 8: Object Property Expressions + * + * Template: "Item Total: {item.price * item.quantity}" + */ +async function objectPropertyExpressions() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Object Property Expressions Document', + description: 'Object property in expressions example', + variables: [ + { + placeholder: '{item}', + name: 'item', + mimeType: 'json', + value: { + price: 25, + quantity: 4, + }, + }, + ], + }); + + console.log('Document with object property expressions generated:', result.deliverableId); +} + +/** + * Example 9: Nested Loops with Objects + * + * Template: + * {#departments} + * Department: {deptName} + * {#employees} + * - {employeeName}: {title} + * {/employees} + * {/departments} + */ +async function nestedLoops() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Nested Loops Document', + description: 'Nested array loops example', + variables: [ + { + placeholder: '{departments}', + name: 'departments', + mimeType: 'json', + value: [ + { + deptName: 'Dept X', + employees: [ + { employeeName: 'Person A', title: 'Role 1' }, + { employeeName: 'Person B', title: 'Role 2' }, + ], + }, + { + deptName: 'Dept Y', + employees: [{ employeeName: 'Person C', title: 'Role 3' }], + }, + ], + }, + ], + }); + + console.log('Document with nested loops generated:', result.deliverableId); +} + +/** + * Example 10: Conditionals Inside Loops + * + * Template: + * {#orderItems} + * - {productName}: ${itemPrice} {#isOnSale}(ON SALE!){/isOnSale} + * {/orderItems} + */ +async function conditionalsInLoops() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Conditionals In Loops Document', + description: 'Conditionals inside loops example', + variables: [ + { + placeholder: '{orderItems}', + name: 'orderItems', + mimeType: 'json', + value: [ + { productName: 'Product X', itemPrice: 10, isOnSale: true, qty: 3 }, + { productName: 'Product Y', itemPrice: 25, isOnSale: false, qty: 10 }, + { productName: 'Product Z', itemPrice: 15, isOnSale: true, qty: 2 }, + ], + }, + ], + }); + + console.log('Document with conditionals in loops generated:', result.deliverableId); +} + +/** + * Example 11: Complex Invoice (Full Example) + * + * Combines: nested objects, loops, conditionals, expressions + */ +async function complexInvoice() { + const result = await TurboTemplate.generate({ + templateId: 'invoice-template-id', + name: 'Invoice - Company ABC', + description: 'Monthly invoice', + variables: [ + // Invoice header + { + placeholder: '{invoice}', + name: 'invoice', + mimeType: 'json', + value: { + number: 'INV-0000-00000', + date: 'January 1, 2024', + dueDate: 'February 1, 2024', + }, + }, + + // Customer with nested address + { + placeholder: '{invCustomer}', + name: 'invCustomer', + mimeType: 'json', + value: { + company: 'Company ABC LLC', + address: { + line1: '123 Test Street', + line2: 'Suite 100', + city: 'Test City', + state: 'TS', + zip: '00000', + }, + contact: { + name: 'Contact Person', + email: 'contact@example.com', + }, + }, + }, + + // Line items (array for loop) + { + placeholder: '{invLineItems}', + name: 'invLineItems', + mimeType: 'json', + value: [ + { sku: 'SKU-001', lineDesc: 'Service A', lineQty: 10, linePrice: 500, isTaxExempt: false }, + { sku: 'SKU-002', lineDesc: 'Service B', lineQty: 1, linePrice: 2000, isTaxExempt: false }, + { sku: 'SKU-003', lineDesc: 'Service C', lineQty: 5, linePrice: 200, isTaxExempt: true }, + ], + }, + + // Totals + { + placeholder: '{invTotals}', + name: 'invTotals', + mimeType: 'json', + value: { + subtotal: 8000, + hasDiscount: true, + discountCode: 'TESTCODE', + discountAmount: 800, + grandTotal: 7776, + }, + }, + + // Tax breakdown (array) + { + placeholder: '{taxBreakdown}', + name: 'taxBreakdown', + mimeType: 'json', + value: [ + { taxName: 'Tax A', rate: 6, taxAmt: 432 }, + { taxName: 'Tax B', rate: 2, taxAmt: 144 }, + ], + }, + + // Payment info + { placeholder: '{paymentTerms}', name: 'paymentTerms', mimeType: 'json', value: 'NET30' }, + { placeholder: '{invIsPaid}', name: 'invIsPaid', mimeType: 'json', value: false }, + ], + }); + + console.log('Complex invoice generated:', result.deliverableId); +} + +/** + * Example 12: Using Helper Functions + * + * Helper functions automatically add curly braces and set correct mimeType + */ +async function usingHelpers() { + const result = await TurboTemplate.generate({ + templateId: 'your-template-id', + name: 'Helper Functions Document', + description: 'Using helper functions example', + variables: [ + // Simple variable - helper adds {} and sets mimeType + TurboTemplate.createSimpleVariable('title', 'Quarterly Report'), + + // Nested object - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: true + TurboTemplate.createNestedVariable('company', { + name: 'Company XYZ', + headquarters: 'Test Location', + employees: 500, + }), + + // Loop variable - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: true + TurboTemplate.createLoopVariable('departments', [ + { name: 'Dept A', headcount: 200 }, + { name: 'Dept B', headcount: 150 }, + { name: 'Dept C', headcount: 100 }, + ]), + + // Conditional - helper sets usesAdvancedTemplatingEngine: true + TurboTemplate.createConditionalVariable('show_financials', true), + + // Image - helper sets mimeType: 'image' + TurboTemplate.createImageVariable('company_logo', 'https://example.com/logo.png'), + ], + }); + + console.log('Document with helpers generated:', result.deliverableId); +} + +/** + * Example 13: Variable Validation + */ +function variableValidation() { + // Valid variable with proper configuration + const validVariable = { + placeholder: '{user}', + name: 'user', + mimeType: 'json' as const, + value: { firstName: 'Foo', email: 'foo@example.com' }, + }; + + const validation1 = TurboTemplate.validateVariable(validVariable); + console.log('Valid variable:', validation1.isValid); // true + + // Variable missing placeholder + const invalidVariable = { + name: 'test', + value: 'test', + }; + + const validation2 = TurboTemplate.validateVariable(invalidVariable as any); + console.log('Invalid variable errors:', validation2.errors); + + // Variable with warnings (array without json mimeType) + const warningVariable = { + placeholder: '{items}', + name: 'items', + value: [1, 2, 3], + }; + + const validation3 = TurboTemplate.validateVariable(warningVariable); + console.log('Variable warnings:', validation3.warnings); +} + +// Run examples +async function main() { + console.log('TurboTemplate Advanced Templating Examples\n'); + + try { + // Uncomment the examples you want to run: + // await simpleSubstitution(); + // await nestedObjects(); + // await deepNestedObjects(); + // await loopsAndArrays(); + // await conditionals(); + // await expressionsAndCalculations(); + // await complexExpressions(); + // await objectPropertyExpressions(); + // await nestedLoops(); + // await conditionalsInLoops(); + // await complexInvoice(); + // await usingHelpers(); + // variableValidation(); + + console.log('\nAll examples completed successfully!'); + } catch (error) { + console.error('Error running examples:', error); + } +} + +// Uncomment to run +// main(); diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 1a31d97..f04ee75 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -4,9 +4,11 @@ // Export modules export { TurboSign } from './modules/sign'; +export { TurboTemplate } from './modules/template'; // Export types export * from './types/sign'; +export * from './types/template'; // Export errors export * from './utils/errors'; diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts new file mode 100644 index 0000000..c31ee8b --- /dev/null +++ b/packages/js-sdk/src/modules/template.ts @@ -0,0 +1,310 @@ +/** + * TurboTemplate Module - Advanced document templating with Angular-like expressions + */ + +import { HttpClient, HttpClientConfig } from '../http'; +import { + TemplateVariable, + GenerateTemplateRequest, + GenerateTemplateResponse, + VariableValidation, + SimpleVariable, + NestedVariable, + LoopVariable, + ConditionalVariable, + ImageVariable, +} from '../types/template'; + +export class TurboTemplate { + private static client: HttpClient; + + /** + * Configure the TurboTemplate module with API credentials + * + * @param config - Configuration object + * @param config.apiKey - TurboDocx API key (required) + * @param config.orgId - Organization ID (required) + * @param config.baseUrl - API base URL (optional, defaults to https://api.turbodocx.com) + * + * @example + * ```typescript + * TurboTemplate.configure({ + * apiKey: process.env.TURBODOCX_API_KEY, + * orgId: process.env.TURBODOCX_ORG_ID + * }); + * ``` + */ + static configure(config: HttpClientConfig): void { + this.client = new HttpClient(config); + } + + /** + * Get the HTTP client instance, initializing if necessary + */ + private static getClient(): HttpClient { + if (!this.client) { + // Auto-initialize with environment variables if not configured + this.client = new HttpClient(); + } + return this.client; + } + + /** + * Generate a document from a template with variables + * + * Supports advanced templating features: + * - Simple variable substitution: {customer_name} + * - Nested objects: {user.firstName} + * - Loops: {#products}...{/products} + * - Conditionals: {#if condition}...{/if} + * - Expressions: {price + tax} + * - Filters: {name | uppercase} + * + * @param request - Template ID and variables + * @returns Generated document + * + * @example + * ```typescript + * // Simple variable substitution + * const result = await TurboTemplate.generate({ + * templateId: 'template-uuid', + * variables: [ + * { placeholder: '{customer_name}', mimeType: 'text', value: 'John Doe' }, + * { placeholder: '{order_total}', mimeType: 'text', value: 1500 } + * ] + * }); + * + * // Advanced: nested objects with dot notation + * const result = await TurboTemplate.generate({ + * templateId: 'template-uuid', + * variables: [ + * { + * placeholder: '{user}', + * mimeType: 'json', + * value: { + * firstName: 'John', + * email: 'john@example.com' + * } + * } + * ] + * }); + * // Template can use: {user.firstName}, {user.email} + * + * // Advanced: loops with arrays + * const result = await TurboTemplate.generate({ + * templateId: 'template-uuid', + * variables: [ + * { + * placeholder: '{products}', + * mimeType: 'json', + * value: [ + * { name: 'Laptop', price: 999 }, + * { name: 'Mouse', price: 29 } + * ] + * } + * ] + * }); + * // Template can use: {#products}{name}: ${price}{/products} + * + * // Advanced: expressions with calculations + * const result = await TurboTemplate.generate({ + * templateId: 'template-uuid', + * variables: [ + * { placeholder: '{price}', mimeType: 'text', value: '100', usesAdvancedTemplatingEngine: true }, + * { placeholder: '{tax}', mimeType: 'text', value: '15', usesAdvancedTemplatingEngine: true } + * ] + * }); + * // Template can use: {price + tax}, {price * 1.15} + * ``` + */ + static async generate(request: GenerateTemplateRequest): Promise { + const client = this.getClient(); + + // Prepare request body - send as JSON + const body: any = { + templateId: request.templateId, + variables: request.variables.map((v) => { + const variable: any = { + placeholder: v.placeholder, + name: v.name, + }; + + // Add mimeType if specified (default to 'text' if not provided) + variable.mimeType = v.mimeType || 'text'; + + // Handle value - keep objects/arrays as-is for JSON serialization + if (v.value !== undefined && v.value !== null) { + variable.value = v.value; + } else if (v.text !== undefined && v.text !== null) { + variable.text = v.text; + } else { + throw new Error(`Variable "${variable.placeholder}" must have either 'value' or 'text' property`); + } + + // Add advanced templating flags if specified + if (v.usesAdvancedTemplatingEngine != null) { + variable.usesAdvancedTemplatingEngine = v.usesAdvancedTemplatingEngine; + } + if (v.nestedInAdvancedTemplatingEngine != null) { + variable.nestedInAdvancedTemplatingEngine = v.nestedInAdvancedTemplatingEngine; + } + if (v.allowRichTextInjection != null) { + variable.allowRichTextInjection = v.allowRichTextInjection; + } + + // Add optional fields + if (v.description) variable.description = v.description; + if (v.defaultValue !== undefined) variable.defaultValue = v.defaultValue; + if (v.nestedVariables) variable.nestedVariables = v.nestedVariables; + if (v.subvariables) variable.subvariables = v.subvariables; + + return variable; + }), + }; + + // Add optional request parameters + if (request.name) body.name = request.name; + if (request.description) body.description = request.description; + if (request.replaceFonts !== undefined) body.replaceFonts = request.replaceFonts; + if (request.defaultFont) body.defaultFont = request.defaultFont; + if (request.outputFormat) body.outputFormat = request.outputFormat; + if (request.metadata) body.metadata = request.metadata; + + const response = await client.post('/v1/deliverable', body); + return response; + } + + /** + * Validate a variable configuration + * + * Checks if a variable is properly configured for advanced templating + * + * @param variable - Variable to validate + * @returns Validation result + */ + static validateVariable(variable: TemplateVariable): VariableValidation { + const errors: string[] = []; + const warnings: string[] = []; + + // Check placeholder/name + if (!variable.placeholder && !variable.name) { + errors.push('Variable must have either "placeholder" or "name" property'); + } + + // Check value/text + const hasValue = variable.value !== undefined && variable.value !== null; + const hasText = variable.text !== undefined && variable.text !== null; + + if (!hasValue && !hasText) { + errors.push('Variable must have either "value" or "text" property'); + } + + // Check advanced templating settings + if (variable.mimeType === 'json' || (typeof variable.value === 'object' && variable.value !== null)) { + if (!variable.mimeType) { + warnings.push('Complex objects should explicitly set mimeType to "json"'); + } + } + + // Check for arrays + if (Array.isArray(variable.value)) { + if (variable.mimeType !== 'json') { + warnings.push('Array values should use mimeType: "json"'); + } + } + + // Check image variables + if (variable.mimeType === 'image') { + if (typeof variable.value !== 'string') { + errors.push('Image variables must have a string value (URL or base64)'); + } + } + + return { + isValid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined, + warnings: warnings.length > 0 ? warnings : undefined, + }; + } + + /** + * Helper: Create a simple text variable + * @param name - Variable name + * @param value - Variable value + * @param placeholder - Optional custom placeholder (defaults to {name}) + */ + static createSimpleVariable(name: string, value: string | number | boolean, placeholder?: string): SimpleVariable { + const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + return { + placeholder: p, + name, + value, + }; + } + + /** + * Helper: Create a nested object variable + * @param name - Variable name + * @param value - Object value + * @param placeholder - Optional custom placeholder (defaults to {name}) + */ + static createNestedVariable(name: string, value: Record, placeholder?: string): NestedVariable { + const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + return { + placeholder: p, + name, + value, + usesAdvancedTemplatingEngine: true, + mimeType: 'json', + }; + } + + /** + * Helper: Create a loop/array variable + * @param name - Variable name + * @param value - Array value + * @param placeholder - Optional custom placeholder (defaults to {name}) + */ + static createLoopVariable(name: string, value: any[], placeholder?: string): LoopVariable { + const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + return { + placeholder: p, + name, + value, + usesAdvancedTemplatingEngine: true, + mimeType: 'json', + }; + } + + /** + * Helper: Create a conditional variable + * @param name - Variable name + * @param value - Conditional value + * @param placeholder - Optional custom placeholder (defaults to {name}) + */ + static createConditionalVariable(name: string, value: any, placeholder?: string): ConditionalVariable { + const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + return { + placeholder: p, + name, + value, + usesAdvancedTemplatingEngine: true, + }; + } + + /** + * Helper: Create an image variable + * @param name - Variable name + * @param imageUrl - Image URL or base64 string + * @param placeholder - Optional custom placeholder (defaults to {name}) + */ + static createImageVariable(name: string, imageUrl: string, placeholder?: string): ImageVariable { + const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + return { + placeholder: p, + name, + value: imageUrl, + mimeType: 'image', + }; + } +} diff --git a/packages/js-sdk/src/types/template.ts b/packages/js-sdk/src/types/template.ts new file mode 100644 index 0000000..f12e6e6 --- /dev/null +++ b/packages/js-sdk/src/types/template.ts @@ -0,0 +1,216 @@ +/** + * TypeScript types for TurboTemplate module - Advanced Templating + */ + +/** + * Variable MIME types supported by TurboDocx + */ +export type VariableMimeType = 'text' | 'html' | 'image' | 'markdown' | 'json'; + +/** + * Variable configuration for template generation + * Supports both simple text replacement and advanced templating with Angular-like expressions + */ +export interface TemplateVariable { + /** Variable name/placeholder (e.g., "customer_name", "order_total") */ + placeholder: string; + + /** Variable name (alternative to placeholder) */ + name: string; + + /** + * Variable value - can be: + * - string for simple text + * - number for numeric values + * - boolean for conditionals + * - object for nested data structures + * - array for loops/iterations + * - null/undefined for optional values + */ + value?: string | number | boolean | object | any[] | null; + + /** + * Text value (legacy, prefer using 'value') + * Either text OR value must be provided + */ + text?: string | number | boolean | object | any[] | null; + + /** MIME type of the variable */ + mimeType?: VariableMimeType; + + /** + * Enable advanced templating engine for this variable + * Allows Angular-like expressions: loops, conditions, filters, etc. + */ + usesAdvancedTemplatingEngine?: boolean; + + /** + * Marks variable as nested within an advanced templating context + * Used for loop iteration variables, nested object properties, etc. + */ + nestedInAdvancedTemplatingEngine?: boolean; + + /** Allow rich text injection (HTML formatting) */ + allowRichTextInjection?: boolean; + + /** Variable description */ + description?: string; + + /** Whether this is a default value */ + defaultValue?: boolean; + + /** Nested variables for complex object structures */ + nestedVariables?: TemplateVariable[]; + + /** Sub-variables (legacy structure) */ + subvariables?: TemplateVariable[]; +} + +/** + * Request for generating a document from template + */ +export interface GenerateTemplateRequest { + /** Template ID to use for generation */ + templateId: string; + + /** Variables to inject into the template */ + variables: TemplateVariable[]; + + /** Document name */ + name: string; + + /** Document description */ + description: string; + + /** Replace fonts in the document */ + replaceFonts?: boolean; + + /** Default font to use when replacing */ + defaultFont?: string; + + /** Output format (default: docx) */ + outputFormat?: 'docx' | 'pdf'; + + /** Additional metadata */ + metadata?: Record; +} + +/** + * Response from template generation + */ +export interface GenerateTemplateResponse { + /** Whether generation was successful */ + success: boolean; + + /** Deliverable ID */ + deliverableId?: string; + + /** Generated document buffer (if returnBuffer is true) */ + buffer?: Buffer; + + /** Document download URL */ + downloadUrl?: string; + + /** Response message */ + message?: string; + + /** Error details if generation failed */ + error?: string; +} + +/** + * Helper types for common templating patterns + */ + +/** Simple key-value variable */ +export interface SimpleVariable { + placeholder: string; + name: string; + value: string | number | boolean; +} + +/** Variable with nested structure (e.g., user.name, user.email) */ +export interface NestedVariable { + placeholder: string; + name: string; + value: Record; + usesAdvancedTemplatingEngine: true; + mimeType: 'json'; +} + +/** Variable for loop iteration (e.g., items array) */ +export interface LoopVariable { + placeholder: string; + name: string; + value: any[]; + usesAdvancedTemplatingEngine: true; + mimeType: 'json'; +} + +/** Variable with conditional logic */ +export interface ConditionalVariable { + placeholder: string; + name: string; + value: any; + usesAdvancedTemplatingEngine: true; +} + +/** Image variable */ +export interface ImageVariable { + placeholder: string; + name: string; + value: string; // URL or base64 + mimeType: 'image'; +} + +/** + * Template context - full data structure for template rendering + */ +export interface TemplateContext { + /** All variables indexed by placeholder */ + variables: Record; + + /** Metadata about the template */ + metadata?: { + templateId: string; + templateName?: string; + version?: string; + }; +} + +/** + * Variable validation result + */ +export interface VariableValidation { + /** Whether the variable is valid */ + isValid: boolean; + + /** Validation errors */ + errors?: string[]; + + /** Validation warnings */ + warnings?: string[]; +} + +/** + * Advanced templating features supported + */ +export interface AdvancedTemplatingFeatures { + /** Support for loops (e.g., {#items}...{/items}) */ + loops: boolean; + + /** Support for conditionals (e.g., {#if condition}...{/if}) */ + conditionals: boolean; + + /** Support for filters (e.g., {value | uppercase}) */ + filters: boolean; + + /** Support for expressions (e.g., {price * quantity}) */ + expressions: boolean; + + /** Support for dot notation (e.g., {user.name}) */ + dotNotation: boolean; + + /** Support for array access (e.g., {items[0]}) */ + arrayAccess: boolean; +} From 78b39f8d8197b3f4e0dd6407098dd1b40bd3b6ee Mon Sep 17 00:00:00 2001 From: Kushal Date: Wed, 21 Jan 2026 17:51:07 +0000 Subject: [PATCH 02/39] feat: add python sdk for advanced templating support Signed-off-by: Kushal --- .../py-sdk/examples/advanced_templating.py | 359 +++++++++++++++++ packages/py-sdk/src/turbodocx_sdk/__init__.py | 2 + .../src/turbodocx_sdk/modules/__init__.py | 3 +- .../src/turbodocx_sdk/modules/template.py | 363 ++++++++++++++++++ .../src/turbodocx_sdk/types/__init__.py | 17 + .../src/turbodocx_sdk/types/template.py | 125 ++++++ 6 files changed, 868 insertions(+), 1 deletion(-) create mode 100644 packages/py-sdk/examples/advanced_templating.py create mode 100644 packages/py-sdk/src/turbodocx_sdk/modules/template.py create mode 100644 packages/py-sdk/src/turbodocx_sdk/types/__init__.py create mode 100644 packages/py-sdk/src/turbodocx_sdk/types/template.py diff --git a/packages/py-sdk/examples/advanced_templating.py b/packages/py-sdk/examples/advanced_templating.py new file mode 100644 index 0000000..56ac664 --- /dev/null +++ b/packages/py-sdk/examples/advanced_templating.py @@ -0,0 +1,359 @@ +""" +TurboTemplate Advanced Templating Examples + +This file demonstrates the advanced templating features introduced +in the RapidDocxBackend PR #1057. + +Key points for variable configuration: +- Placeholders should include curly braces: "{variable_name}" +- For objects/arrays, use mimeType: 'json' +- For expressions on simple values, use mimeType: 'text' with usesAdvancedTemplatingEngine: True +- Boolean/number values with mimeType: 'json' work for conditionals +""" + +import asyncio +import os +from turbodocx_sdk import TurboTemplate + +# Configure the client +TurboTemplate.configure( + api_key=os.environ.get("TURBODOCX_API_KEY"), + org_id=os.environ.get("TURBODOCX_ORG_ID"), +) + + +async def simple_substitution(): + """ + Example 1: Simple Variable Substitution + + Template: "Dear {customer_name}, your order total is ${order_total}." + """ + result = await TurboTemplate.generate({ + "templateId": "your-template-id", + "name": "Simple Substitution Document", + "description": "Basic variable substitution example", + "variables": [ + {"placeholder": "{customer_name}", "name": "customer_name", "mimeType": "text", "value": "Foo Bar"}, + {"placeholder": "{order_total}", "name": "order_total", "mimeType": "text", "value": 1500}, + {"placeholder": "{order_date}", "name": "order_date", "mimeType": "text", "value": "2024-01-01"}, + ], + }) + + print("Document generated:", result.get("deliverableId")) + + +async def nested_objects(): + """ + Example 2: Nested Objects with Dot Notation + + Template: "Name: {user.name}, Email: {user.email}, Company: {user.profile.company}" + """ + result = await TurboTemplate.generate({ + "templateId": "your-template-id", + "name": "Nested Objects Document", + "description": "Nested object with dot notation example", + "variables": [ + { + "placeholder": "{user}", + "name": "user", + "mimeType": "json", + "value": { + "name": "Person A", + "email": "persona@example.com", + "profile": { + "company": "Company XYZ", + "title": "Role 1", + "location": "Test City, TS", + }, + }, + } + ], + }) + + print("Document with nested data generated:", result.get("deliverableId")) + + +async def loops_and_arrays(): + """ + Example 3: Loops/Arrays + + Template: + {#items} + - {name}: {quantity} x ${price} = ${quantity * price} + {/items} + """ + result = await TurboTemplate.generate({ + "templateId": "your-template-id", + "name": "Array Loops Document", + "description": "Array loop iteration example", + "variables": [ + { + "placeholder": "{items}", + "name": "items", + "mimeType": "json", + "value": [ + {"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}, + {"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}, + {"name": "Item C", "quantity": 10, "price": 50, "sku": "SKU-003"}, + ], + } + ], + }) + + print("Document with loop generated:", result.get("deliverableId")) + + +async def conditionals(): + """ + Example 4: Conditionals + + Template: + {#if is_premium} + Premium Member Discount: {discount * 100}% + {/if} + {#if !is_premium} + Become a premium member for exclusive discounts! + {/if} + """ + result = await TurboTemplate.generate({ + "templateId": "your-template-id", + "name": "Conditionals Document", + "description": "Boolean conditional example", + "variables": [ + { + "placeholder": "{is_premium}", + "name": "is_premium", + "mimeType": "json", + "value": True, + }, + { + "placeholder": "{discount}", + "name": "discount", + "mimeType": "json", + "value": 0.2, + }, + ], + }) + + print("Document with conditionals generated:", result.get("deliverableId")) + + +async def expressions_and_calculations(): + """ + Example 5: Expressions and Calculations + + Template: "Subtotal: ${subtotal}, Tax: ${subtotal * tax_rate}, Total: ${subtotal * (1 + tax_rate)}" + """ + result = await TurboTemplate.generate({ + "templateId": "your-template-id", + "name": "Expressions Document", + "description": "Arithmetic expressions example", + "variables": [ + { + "placeholder": "{subtotal}", + "name": "subtotal", + "mimeType": "text", + "value": "1000", + "usesAdvancedTemplatingEngine": True, + }, + { + "placeholder": "{tax_rate}", + "name": "tax_rate", + "mimeType": "text", + "value": "0.08", + "usesAdvancedTemplatingEngine": True, + }, + ], + }) + + print("Document with expressions generated:", result.get("deliverableId")) + + +async def complex_invoice(): + """ + Example 6: Complex Invoice Example + + Combines multiple features: nested objects, loops, conditionals, expressions + """ + result = await TurboTemplate.generate({ + "templateId": "invoice-template-id", + "name": "Invoice - Company ABC", + "description": "Monthly invoice", + "variables": [ + # Customer info (nested object) + { + "placeholder": "{customer}", + "name": "customer", + "mimeType": "json", + "value": { + "name": "Company ABC", + "email": "billing@example.com", + "address": { + "street": "123 Test Street", + "city": "Test City", + "state": "TS", + "zip": "00000", + }, + }, + }, + # Invoice metadata + {"placeholder": "{invoice_number}", "name": "invoice_number", "mimeType": "text", "value": "INV-0000-001"}, + {"placeholder": "{invoice_date}", "name": "invoice_date", "mimeType": "text", "value": "2024-01-01"}, + {"placeholder": "{due_date}", "name": "due_date", "mimeType": "text", "value": "2024-02-01"}, + # Line items (array for loops) + { + "placeholder": "{items}", + "name": "items", + "mimeType": "json", + "value": [ + { + "description": "Service A", + "quantity": 40, + "rate": 150, + }, + { + "description": "Service B", + "quantity": 1, + "rate": 5000, + }, + { + "description": "Service C", + "quantity": 12, + "rate": 500, + }, + ], + }, + # Tax and totals + { + "placeholder": "{tax_rate}", + "name": "tax_rate", + "mimeType": "text", + "value": "0.08", + "usesAdvancedTemplatingEngine": True, + }, + # Premium customer flag + {"placeholder": "{is_premium}", "name": "is_premium", "mimeType": "json", "value": True}, + { + "placeholder": "{premium_discount}", + "name": "premium_discount", + "mimeType": "text", + "value": "0.05", + "usesAdvancedTemplatingEngine": True, + }, + # Payment terms + {"placeholder": "{payment_terms}", "name": "payment_terms", "mimeType": "text", "value": "Net 30"}, + # Notes + { + "placeholder": "{notes}", + "name": "notes", + "mimeType": "text", + "value": "Thank you for your business!", + }, + ], + }) + + print("Complex invoice generated:", result.get("deliverableId")) + + +async def using_helpers(): + """ + Example 7: Using Helper Functions + + Helper functions automatically add curly braces and set correct mimeType + """ + result = await TurboTemplate.generate({ + "templateId": "your-template-id", + "name": "Helper Functions Document", + "description": "Using helper functions example", + "variables": [ + # Simple variable - helper adds {} and sets mimeType + TurboTemplate.create_simple_variable("title", "Quarterly Report"), + # Nested object - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: True + TurboTemplate.create_nested_variable( + "company", + { + "name": "Company XYZ", + "headquarters": "Test Location", + "employees": 500, + }, + ), + # Loop variable - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: True + TurboTemplate.create_loop_variable( + "departments", + [ + {"name": "Dept A", "headcount": 200}, + {"name": "Dept B", "headcount": 150}, + {"name": "Dept C", "headcount": 100}, + ], + ), + # Conditional - helper sets usesAdvancedTemplatingEngine: True + TurboTemplate.create_conditional_variable("show_financials", True), + # Image - helper sets mimeType: 'image' + TurboTemplate.create_image_variable( + "company_logo", "https://example.com/logo.png" + ), + ], + }) + + print("Document with helpers generated:", result.get("deliverableId")) + + +def variable_validation(): + """ + Example 8: Variable Validation + """ + # Valid variable with proper configuration + valid_variable = { + "placeholder": "{user}", + "name": "user", + "mimeType": "json", + "value": {"firstName": "Foo", "email": "foo@example.com"}, + } + + validation1 = TurboTemplate.validate_variable(valid_variable) + print("Valid variable:", validation1["isValid"]) # True + + # Variable missing placeholder + invalid_variable = { + "name": "test", + "value": "test", + } + + validation2 = TurboTemplate.validate_variable(invalid_variable) + print("Invalid variable errors:", validation2.get("errors")) + + # Variable with warnings (array without json mimeType) + warning_variable = { + "placeholder": "{items}", + "name": "items", + "value": [1, 2, 3], + } + + validation3 = TurboTemplate.validate_variable(warning_variable) + print("Variable warnings:", validation3.get("warnings")) + + +async def main(): + """Run examples""" + print("TurboTemplate Advanced Templating Examples\n") + + try: + # Uncomment the examples you want to run: + # await simple_substitution() + # await nested_objects() + # await loops_and_arrays() + # await conditionals() + # await expressions_and_calculations() + # await complex_invoice() + # await using_helpers() + # variable_validation() + + print("\nAll examples completed successfully!") + except Exception as error: + print("Error running examples:", error) + + +if __name__ == "__main__": + # Uncomment to run + # asyncio.run(main()) + pass diff --git a/packages/py-sdk/src/turbodocx_sdk/__init__.py b/packages/py-sdk/src/turbodocx_sdk/__init__.py index e23629e..4517363 100644 --- a/packages/py-sdk/src/turbodocx_sdk/__init__.py +++ b/packages/py-sdk/src/turbodocx_sdk/__init__.py @@ -10,6 +10,7 @@ from typing import Optional from .modules.sign import TurboSign +from .modules.template import TurboTemplate from .http import ( HttpClient, TurboDocxError, @@ -54,6 +55,7 @@ def sign(self) -> type: __all__ = [ "TurboDocxClient", "TurboSign", + "TurboTemplate", "HttpClient", "TurboDocxError", "AuthenticationError", diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/__init__.py b/packages/py-sdk/src/turbodocx_sdk/modules/__init__.py index c029fdb..d8ef8cf 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/__init__.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/__init__.py @@ -1,4 +1,5 @@ # Modules package from .sign import TurboSign +from .template import TurboTemplate -__all__ = ["TurboSign"] +__all__ = ["TurboSign", "TurboTemplate"] diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py new file mode 100644 index 0000000..737fb01 --- /dev/null +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -0,0 +1,363 @@ +""" +TurboTemplate Module - Advanced document templating with Angular-like expressions + +Provides template generation operations: +- generate: Generate document from template with variables +- validate_variable: Validate variable configuration +- Helper functions for creating common variable types +""" + +from typing import Any, Dict, List, Optional, Union + +from ..http import HttpClient +from ..types.template import ( + TemplateVariable, + GenerateTemplateRequest, + GenerateTemplateResponse, + VariableValidation, + VariableMimeType, +) + + +class TurboTemplate: + """TurboTemplate module for advanced document templating""" + + _client: Optional[HttpClient] = None + + @classmethod + def configure( + cls, + api_key: Optional[str] = None, + access_token: Optional[str] = None, + base_url: str = "https://api.turbodocx.com", + org_id: Optional[str] = None, + ) -> None: + """ + Configure the TurboTemplate module with API credentials + + Args: + api_key: TurboDocx API key (required) + access_token: OAuth2 access token (alternative to API key) + base_url: Base URL for the API (optional, defaults to https://api.turbodocx.com) + org_id: Organization ID (required) + + Example: + >>> TurboTemplate.configure( + ... api_key=os.environ.get("TURBODOCX_API_KEY"), + ... org_id=os.environ.get("TURBODOCX_ORG_ID") + ... ) + """ + cls._client = HttpClient( + api_key=api_key, + access_token=access_token, + base_url=base_url, + org_id=org_id, + ) + + @classmethod + def _get_client(cls) -> HttpClient: + """Get the HTTP client instance, raising error if not configured""" + if cls._client is None: + raise RuntimeError( + "TurboTemplate not configured. Call TurboTemplate.configure(api_key='...', org_id='...') first." + ) + return cls._client + + @classmethod + async def generate(cls, request: GenerateTemplateRequest) -> GenerateTemplateResponse: + """ + Generate a document from a template with variables + + Supports advanced templating features: + - Simple variable substitution: {customer_name} + - Nested objects: {user.firstName} + - Loops: {#products}...{/products} + - Conditionals: {#if condition}...{/if} + - Expressions: {price + tax} + - Filters: {name | uppercase} + + Args: + request: Template ID and variables + + Returns: + Generated document response + + Example: + >>> # Simple variable substitution + >>> result = await TurboTemplate.generate({ + ... "templateId": "template-uuid", + ... "variables": [ + ... {"placeholder": "{customer_name}", "mimeType": "text", "value": "John Doe"}, + ... {"placeholder": "{order_total}", "mimeType": "text", "value": 1500} + ... ] + ... }) + + >>> # Advanced: nested objects with dot notation + >>> result = await TurboTemplate.generate({ + ... "templateId": "template-uuid", + ... "variables": [ + ... { + ... "placeholder": "{user}", + ... "mimeType": "json", + ... "value": { + ... "firstName": "John", + ... "email": "john@example.com" + ... } + ... } + ... ] + ... }) + >>> # Template can use: {user.firstName}, {user.email} + + >>> # Advanced: loops with arrays + >>> result = await TurboTemplate.generate({ + ... "templateId": "template-uuid", + ... "variables": [ + ... { + ... "placeholder": "{products}", + ... "mimeType": "json", + ... "value": [ + ... {"name": "Laptop", "price": 999}, + ... {"name": "Mouse", "price": 29} + ... ] + ... } + ... ] + ... }) + >>> # Template can use: {#products}{name}: ${price}{/products} + + >>> # Advanced: expressions with calculations + >>> result = await TurboTemplate.generate({ + ... "templateId": "template-uuid", + ... "variables": [ + ... {"placeholder": "{price}", "mimeType": "text", "value": "100", "usesAdvancedTemplatingEngine": True}, + ... {"placeholder": "{tax}", "mimeType": "text", "value": "15", "usesAdvancedTemplatingEngine": True} + ... ] + ... }) + >>> # Template can use: {price + tax}, {price * 1.15} + """ + client = cls._get_client() + + # Prepare request body + body: Dict[str, Any] = { + "templateId": request["templateId"], + "variables": [], + } + + # Process variables + for v in request["variables"]: + variable: Dict[str, Any] = { + "placeholder": v.get("placeholder"), + "name": v.get("name"), + } + + # Add mimeType (default to 'text' if not provided) + variable["mimeType"] = v.get("mimeType", "text") + + # Handle value - keep objects/arrays as-is for JSON serialization + if "value" in v and v["value"] is not None: + variable["value"] = v["value"] + elif "text" in v and v["text"] is not None: + variable["text"] = v["text"] + else: + raise ValueError( + f'Variable "{variable["placeholder"]}" must have either "value" or "text" property' + ) + + # Add advanced templating flags if specified + if "usesAdvancedTemplatingEngine" in v: + variable["usesAdvancedTemplatingEngine"] = v["usesAdvancedTemplatingEngine"] + if "nestedInAdvancedTemplatingEngine" in v: + variable["nestedInAdvancedTemplatingEngine"] = v["nestedInAdvancedTemplatingEngine"] + if "allowRichTextInjection" in v: + variable["allowRichTextInjection"] = v["allowRichTextInjection"] + + # Add optional fields + if "description" in v: + variable["description"] = v["description"] + if "defaultValue" in v: + variable["defaultValue"] = v["defaultValue"] + if "nestedVariables" in v: + variable["nestedVariables"] = v["nestedVariables"] + if "subvariables" in v: + variable["subvariables"] = v["subvariables"] + + body["variables"].append(variable) + + # Add optional request parameters + if "name" in request: + body["name"] = request["name"] + if "description" in request: + body["description"] = request["description"] + if "replaceFonts" in request: + body["replaceFonts"] = request["replaceFonts"] + if "defaultFont" in request: + body["defaultFont"] = request["defaultFont"] + if "outputFormat" in request: + body["outputFormat"] = request["outputFormat"] + if "metadata" in request: + body["metadata"] = request["metadata"] + + response = await client.post("/v1/deliverable", json=body) + return response + + @classmethod + def validate_variable(cls, variable: TemplateVariable) -> VariableValidation: + """ + Validate a variable configuration + + Checks if a variable is properly configured for advanced templating + + Args: + variable: Variable to validate + + Returns: + Validation result with isValid flag and any errors/warnings + """ + errors: List[str] = [] + warnings: List[str] = [] + + # Check placeholder/name + if not variable.get("placeholder") and not variable.get("name"): + errors.append('Variable must have either "placeholder" or "name" property') + + # Check value/text + has_value = "value" in variable and variable["value"] is not None + has_text = "text" in variable and variable["text"] is not None + + if not has_value and not has_text: + errors.append('Variable must have either "value" or "text" property') + + # Check advanced templating settings + mime_type = variable.get("mimeType") + value = variable.get("value") + + if mime_type == "json" or (isinstance(value, (dict, list)) and value is not None): + if not mime_type: + warnings.append('Complex objects should explicitly set mimeType to "json"') + + # Check for arrays + if isinstance(value, list): + if mime_type != "json": + warnings.append('Array values should use mimeType: "json"') + + # Check image variables + if mime_type == "image": + if not isinstance(value, str): + errors.append("Image variables must have a string value (URL or base64)") + + return { + "isValid": len(errors) == 0, + "errors": errors if errors else None, + "warnings": warnings if warnings else None, + } + + @staticmethod + def create_simple_variable( + name: str, value: Union[str, int, float, bool], placeholder: Optional[str] = None + ) -> TemplateVariable: + """ + Helper: Create a simple text variable + + Args: + name: Variable name + value: Variable value + placeholder: Optional custom placeholder (defaults to {name}) + + Returns: + TemplateVariable configured for simple substitution + """ + p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + return {"placeholder": p, "name": name, "value": value} + + @staticmethod + def create_nested_variable( + name: str, value: Dict[str, Any], placeholder: Optional[str] = None + ) -> TemplateVariable: + """ + Helper: Create a nested object variable + + Args: + name: Variable name + value: Nested object/dict value + placeholder: Optional custom placeholder (defaults to {name}) + + Returns: + TemplateVariable configured for nested object access + """ + p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + return { + "placeholder": p, + "name": name, + "value": value, + "usesAdvancedTemplatingEngine": True, + "mimeType": VariableMimeType.JSON, + } + + @staticmethod + def create_loop_variable( + name: str, value: List[Any], placeholder: Optional[str] = None + ) -> TemplateVariable: + """ + Helper: Create a loop/array variable + + Args: + name: Variable name + value: Array/list value for iteration + placeholder: Optional custom placeholder (defaults to {name}) + + Returns: + TemplateVariable configured for loop iteration + """ + p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + return { + "placeholder": p, + "name": name, + "value": value, + "usesAdvancedTemplatingEngine": True, + "mimeType": VariableMimeType.JSON, + } + + @staticmethod + def create_conditional_variable( + name: str, value: Any, placeholder: Optional[str] = None + ) -> TemplateVariable: + """ + Helper: Create a conditional variable + + Args: + name: Variable name + value: Variable value (typically boolean) + placeholder: Optional custom placeholder (defaults to {name}) + + Returns: + TemplateVariable configured for conditionals + """ + p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + return { + "placeholder": p, + "name": name, + "value": value, + "usesAdvancedTemplatingEngine": True, + } + + @staticmethod + def create_image_variable( + name: str, image_url: str, placeholder: Optional[str] = None + ) -> TemplateVariable: + """ + Helper: Create an image variable + + Args: + name: Variable name + image_url: Image URL or base64 data + placeholder: Optional custom placeholder (defaults to {name}) + + Returns: + TemplateVariable configured for image insertion + """ + p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + return { + "placeholder": p, + "name": name, + "value": image_url, + "mimeType": VariableMimeType.IMAGE, + } diff --git a/packages/py-sdk/src/turbodocx_sdk/types/__init__.py b/packages/py-sdk/src/turbodocx_sdk/types/__init__.py new file mode 100644 index 0000000..44457ec --- /dev/null +++ b/packages/py-sdk/src/turbodocx_sdk/types/__init__.py @@ -0,0 +1,17 @@ +"""Type definitions for TurboDocx SDK""" + +from .template import ( + VariableMimeType, + TemplateVariable, + GenerateTemplateRequest, + GenerateTemplateResponse, + VariableValidation, +) + +__all__ = [ + "VariableMimeType", + "TemplateVariable", + "GenerateTemplateRequest", + "GenerateTemplateResponse", + "VariableValidation", +] diff --git a/packages/py-sdk/src/turbodocx_sdk/types/template.py b/packages/py-sdk/src/turbodocx_sdk/types/template.py new file mode 100644 index 0000000..573199a --- /dev/null +++ b/packages/py-sdk/src/turbodocx_sdk/types/template.py @@ -0,0 +1,125 @@ +""" +Type definitions for TurboTemplate module - Advanced templating +""" + +from enum import Enum +from typing import Any, Dict, List, Optional, Union, TypedDict + + +class VariableMimeType(str, Enum): + """Variable MIME types supported by TurboDocx""" + + TEXT = "text" + HTML = "html" + IMAGE = "image" + MARKDOWN = "markdown" + JSON = "json" + + +class _TemplateVariableRequired(TypedDict): + """Required fields for TemplateVariable""" + + placeholder: str + name: str + + +class TemplateVariable(_TemplateVariableRequired, total=False): + """ + Variable configuration for template generation + + Supports both simple text replacement and advanced templating with Angular-like expressions + + Attributes: + placeholder: Variable placeholder in template (required, e.g., "{customer_name}") + name: Variable name (required, can be different from placeholder) + value: Variable value - can be string, number, boolean, dict, list, or None + text: Text value (legacy, prefer using 'value') + mimeType: MIME type of the variable + usesAdvancedTemplatingEngine: Enable advanced templating for this variable + nestedInAdvancedTemplatingEngine: Marks variable as nested within advanced context + allowRichTextInjection: Allow rich text injection (HTML formatting) + description: Variable description + defaultValue: Whether this is a default value + nestedVariables: Nested variables for complex structures + subvariables: Sub-variables (legacy structure) + """ + + value: Union[str, int, float, bool, Dict[str, Any], List[Any], None] + text: Optional[str] + mimeType: Optional[VariableMimeType] + usesAdvancedTemplatingEngine: Optional[bool] + nestedInAdvancedTemplatingEngine: Optional[bool] + allowRichTextInjection: Optional[bool] + description: Optional[str] + defaultValue: Optional[bool] + nestedVariables: Optional[List["TemplateVariable"]] + subvariables: Optional[List["TemplateVariable"]] + + +class GenerateTemplateRequest(TypedDict, total=False): + """ + Request for generating a document from template + + Attributes: + templateId: Template ID to use for generation + variables: Variables to inject into the template + name: Document name + description: Document description + replaceFonts: Replace fonts in the document + defaultFont: Default font to use when replacing + outputFormat: Output format (default: docx) + metadata: Additional metadata + """ + + templateId: str + variables: List[TemplateVariable] + name: Optional[str] + description: Optional[str] + replaceFonts: Optional[bool] + defaultFont: Optional[str] + outputFormat: Optional[str] # 'docx' or 'pdf' + metadata: Optional[Dict[str, Any]] + + +class GenerateTemplateResponse(TypedDict, total=False): + """ + Response from template generation + + Attributes: + success: Whether generation was successful + deliverableId: Deliverable ID + buffer: Generated document buffer (if returnBuffer is true) + downloadUrl: Document download URL + message: Response message + error: Error details if generation failed + """ + + success: bool + deliverableId: Optional[str] + buffer: Optional[bytes] + downloadUrl: Optional[str] + message: Optional[str] + error: Optional[str] + + +class VariableValidation(TypedDict, total=False): + """ + Variable validation result + + Attributes: + isValid: Whether the variable is valid + errors: Validation errors + warnings: Validation warnings + """ + + isValid: bool + errors: Optional[List[str]] + warnings: Optional[List[str]] + + +# Helper type aliases for common patterns +SimpleVariable = TemplateVariable +NestedVariable = TemplateVariable +LoopVariable = TemplateVariable +ConditionalVariable = TemplateVariable +ImageVariable = TemplateVariable From 02e5dee1487c54432880ec74537c48a20e54a2ef Mon Sep 17 00:00:00 2001 From: Kushal Date: Wed, 21 Jan 2026 17:54:05 +0000 Subject: [PATCH 03/39] feat: add go sdk for advanced templating support Signed-off-by: Kushal --- .../go-sdk/examples/advanced_templating.go | 326 +++++++++++++++++ packages/go-sdk/turbodocx.go | 8 +- packages/go-sdk/turbotemplate.go | 331 ++++++++++++++++++ 3 files changed, 663 insertions(+), 2 deletions(-) create mode 100644 packages/go-sdk/examples/advanced_templating.go create mode 100644 packages/go-sdk/turbotemplate.go diff --git a/packages/go-sdk/examples/advanced_templating.go b/packages/go-sdk/examples/advanced_templating.go new file mode 100644 index 0000000..0738e5f --- /dev/null +++ b/packages/go-sdk/examples/advanced_templating.go @@ -0,0 +1,326 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/turbodocx/sdk/packages/go-sdk" +) + +func main() { + // Configure the client + client, err := turbodocx.NewClientWithConfig(turbodocx.ClientConfig{ + APIKey: os.Getenv("TURBODOCX_API_KEY"), + OrgID: os.Getenv("TURBODOCX_ORG_ID"), + }) + if err != nil { + log.Fatal("Failed to create client:", err) + } + + ctx := context.Background() + + // Uncomment the examples you want to run: + // simpleSubstitution(ctx, client) + // nestedObjects(ctx, client) + // loopsAndArrays(ctx, client) + // conditionals(ctx, client) + // expressionsAndCalculations(ctx, client) + // complexInvoice(ctx, client) + // usingHelpers(ctx, client) + + fmt.Println("Examples ready to run!") +} + +// Example 1: Simple Variable Substitution +// +// Template: "Dear {customer_name}, your order total is ${order_total}." +func simpleSubstitution(ctx context.Context, client *turbodocx.Client) { + name := "Simple Substitution Document" + description := "Basic variable substitution example" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + {Placeholder: "{customer_name}", Name: "customer_name", Value: "Foo Bar"}, + {Placeholder: "{order_total}", Name: "order_total", Value: 1500}, + {Placeholder: "{order_date}", Name: "order_date", Value: "2024-01-01"}, + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Document generated:", *result.DeliverableID) +} + +// Example 2: Nested Objects with Dot Notation +// +// Template: "Name: {user.name}, Email: {user.email}, Company: {user.profile.company}" +func nestedObjects(ctx context.Context, client *turbodocx.Client) { + mimeTypeJSON := turbodocx.MimeTypeJSON + name := "Nested Objects Document" + description := "Nested object with dot notation example" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + { + Placeholder: "{user}", + Name: "user", + MimeType: &mimeTypeJSON, + Value: map[string]interface{}{ + "name": "Person A", + "email": "persona@example.com", + "profile": map[string]interface{}{ + "company": "Company XYZ", + "title": "Role 1", + "location": "Test City, TS", + }, + }, + }, + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Document with nested data generated:", *result.DeliverableID) +} + +// Example 3: Loops/Arrays +// +// Template: +// {#items} +// - {name}: {quantity} x ${price} = ${quantity * price} +// {/items} +func loopsAndArrays(ctx context.Context, client *turbodocx.Client) { + mimeTypeJSON := turbodocx.MimeTypeJSON + name := "Array Loops Document" + description := "Array loop iteration example" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + { + Placeholder: "{items}", + Name: "items", + MimeType: &mimeTypeJSON, + Value: []map[string]interface{}{ + {"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}, + {"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}, + {"name": "Item C", "quantity": 10, "price": 50, "sku": "SKU-003"}, + }, + }, + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Document with loop generated:", *result.DeliverableID) +} + +// Example 4: Conditionals +// +// Template: +// {#if is_premium} +// Premium Member Discount: {discount * 100}% +// {/if} +// {#if !is_premium} +// Become a premium member for exclusive discounts! +// {/if} +func conditionals(ctx context.Context, client *turbodocx.Client) { + mimeTypeJSON := turbodocx.MimeTypeJSON + name := "Conditionals Document" + description := "Boolean conditional example" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + {Placeholder: "{is_premium}", Name: "is_premium", MimeType: &mimeTypeJSON, Value: true}, + {Placeholder: "{discount}", Name: "discount", MimeType: &mimeTypeJSON, Value: 0.2}, + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Document with conditionals generated:", *result.DeliverableID) +} + +// Example 5: Expressions and Calculations +// +// Template: "Subtotal: ${subtotal}, Tax: ${subtotal * tax_rate}, Total: ${subtotal * (1 + tax_rate)}" +func expressionsAndCalculations(ctx context.Context, client *turbodocx.Client) { + mimeTypeText := turbodocx.MimeTypeText + usesAdvanced := true + name := "Expressions Document" + description := "Arithmetic expressions example" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + { + Placeholder: "{subtotal}", + Name: "subtotal", + MimeType: &mimeTypeText, + Value: "1000", + UsesAdvancedTemplatingEngine: &usesAdvanced, + }, + { + Placeholder: "{tax_rate}", + Name: "tax_rate", + MimeType: &mimeTypeText, + Value: "0.08", + UsesAdvancedTemplatingEngine: &usesAdvanced, + }, + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Document with expressions generated:", *result.DeliverableID) +} + +// Example 6: Complex Invoice Example +// +// Combines multiple features: nested objects, loops, conditionals, expressions +func complexInvoice(ctx context.Context, client *turbodocx.Client) { + mimeTypeJSON := turbodocx.MimeTypeJSON + mimeTypeText := turbodocx.MimeTypeText + usesAdvanced := true + + name := "Invoice - Company ABC" + description := "Monthly invoice" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "invoice-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + // Customer info (nested object) + { + Placeholder: "{customer}", + Name: "customer", + MimeType: &mimeTypeJSON, + Value: map[string]interface{}{ + "name": "Company ABC", + "email": "billing@example.com", + "address": map[string]interface{}{ + "street": "123 Test Street", + "city": "Test City", + "state": "TS", + "zip": "00000", + }, + }, + }, + // Invoice metadata + {Placeholder: "{invoice_number}", Name: "invoice_number", Value: "INV-0000-001"}, + {Placeholder: "{invoice_date}", Name: "invoice_date", Value: "2024-01-01"}, + {Placeholder: "{due_date}", Name: "due_date", Value: "2024-02-01"}, + // Line items (array for loops) + { + Placeholder: "{items}", + Name: "items", + MimeType: &mimeTypeJSON, + Value: []map[string]interface{}{ + { + "description": "Service A", + "quantity": 40, + "rate": 150, + }, + { + "description": "Service B", + "quantity": 1, + "rate": 5000, + }, + { + "description": "Service C", + "quantity": 12, + "rate": 500, + }, + }, + }, + // Tax and totals + { + Placeholder: "{tax_rate}", + Name: "tax_rate", + MimeType: &mimeTypeText, + Value: "0.08", + UsesAdvancedTemplatingEngine: &usesAdvanced, + }, + // Premium customer flag + {Placeholder: "{is_premium}", Name: "is_premium", MimeType: &mimeTypeJSON, Value: true}, + { + Placeholder: "{premium_discount}", + Name: "premium_discount", + MimeType: &mimeTypeText, + Value: "0.05", + UsesAdvancedTemplatingEngine: &usesAdvanced, + }, + // Payment terms + {Placeholder: "{payment_terms}", Name: "payment_terms", Value: "Net 30"}, + // Notes + {Placeholder: "{notes}", Name: "notes", Value: "Thank you for your business!"}, + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Complex invoice generated:", *result.DeliverableID) +} + +// Example 7: Using Helper Functions +func usingHelpers(ctx context.Context, client *turbodocx.Client) { + name := "Helper Functions Document" + description := "Using helper functions example" + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-id", + Name: &name, + Description: &description, + Variables: []turbodocx.TemplateVariable{ + // Simple variable + turbodocx.NewSimpleVariable("title", "Quarterly Report"), + + // Nested object + turbodocx.NewNestedVariable("company", map[string]interface{}{ + "name": "Company XYZ", + "headquarters": "Test Location", + "employees": 500, + }), + + // Loop variable + turbodocx.NewLoopVariable("departments", []interface{}{ + map[string]interface{}{"name": "Dept A", "headcount": 200}, + map[string]interface{}{"name": "Dept B", "headcount": 150}, + map[string]interface{}{"name": "Dept C", "headcount": 100}, + }), + + // Conditional + turbodocx.NewConditionalVariable("show_financials", true), + + // Image + turbodocx.NewImageVariable("company_logo", "https://example.com/logo.png"), + }, + }) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Println("Document with helpers generated:", *result.DeliverableID) +} diff --git a/packages/go-sdk/turbodocx.go b/packages/go-sdk/turbodocx.go index d078816..7823635 100644 --- a/packages/go-sdk/turbodocx.go +++ b/packages/go-sdk/turbodocx.go @@ -34,6 +34,9 @@ type Client struct { // TurboSign provides digital signature operations TurboSign *TurboSignClient + // TurboTemplate provides document templating operations + TurboTemplate *TurboTemplateClient + httpClient *HTTPClient } @@ -117,7 +120,8 @@ func NewClientWithConfig(config ClientConfig) (*Client, error) { httpClient := NewHTTPClient(config) return &Client{ - TurboSign: NewTurboSignClient(httpClient), - httpClient: httpClient, + TurboSign: NewTurboSignClient(httpClient), + TurboTemplate: &TurboTemplateClient{httpClient: httpClient}, + httpClient: httpClient, }, nil } diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go new file mode 100644 index 0000000..1fd9c36 --- /dev/null +++ b/packages/go-sdk/turbotemplate.go @@ -0,0 +1,331 @@ +package turbodocx + +import ( + "bytes" + "context" + "encoding/json" + "fmt" +) + +// VariableMimeType represents the MIME type of a template variable +type VariableMimeType string + +const ( + // MimeTypeText represents plain text + MimeTypeText VariableMimeType = "text" + // MimeTypeHTML represents HTML formatted content + MimeTypeHTML VariableMimeType = "html" + // MimeTypeImage represents an image (URL or base64) + MimeTypeImage VariableMimeType = "image" + // MimeTypeMarkdown represents Markdown formatted content + MimeTypeMarkdown VariableMimeType = "markdown" + // MimeTypeJSON represents JSON data (objects/arrays) + MimeTypeJSON VariableMimeType = "json" +) + +// TemplateVariable represents a variable to inject into a template +type TemplateVariable struct { + // Placeholder is the variable placeholder in template (e.g., "{customer_name}", "{order_total}") + Placeholder string `json:"placeholder"` + + // Name is the variable name (required, can be different from placeholder) + Name string `json:"name"` + + // Value can be any type: string, number, boolean, object, array + Value interface{} `json:"value,omitempty"` + + // Text is the text value (legacy, prefer using Value) + Text *string `json:"text,omitempty"` + + // MimeType is the MIME type of the variable + MimeType *VariableMimeType `json:"mimeType,omitempty"` + + // UsesAdvancedTemplatingEngine enables advanced templating for this variable + UsesAdvancedTemplatingEngine *bool `json:"usesAdvancedTemplatingEngine,omitempty"` + + // NestedInAdvancedTemplatingEngine marks variable as nested within advanced context + NestedInAdvancedTemplatingEngine *bool `json:"nestedInAdvancedTemplatingEngine,omitempty"` + + // AllowRichTextInjection allows rich text injection (HTML formatting) + AllowRichTextInjection *bool `json:"allowRichTextInjection,omitempty"` + + // Description is the variable description + Description *string `json:"description,omitempty"` + + // DefaultValue indicates whether this is a default value + DefaultValue *bool `json:"defaultValue,omitempty"` + + // NestedVariables are nested variables for complex structures + NestedVariables []TemplateVariable `json:"nestedVariables,omitempty"` + + // Subvariables are sub-variables (legacy structure) + Subvariables []TemplateVariable `json:"subvariables,omitempty"` +} + +// GenerateTemplateRequest is the request for generating a document from template +type GenerateTemplateRequest struct { + // TemplateID is the template ID to use for generation + TemplateID string `json:"templateId"` + + // Variables to inject into the template + Variables []TemplateVariable `json:"variables"` + + // Name is the document name + Name *string `json:"name,omitempty"` + + // Description is the document description + Description *string `json:"description,omitempty"` + + // ReplaceFonts replaces fonts in the document + ReplaceFonts *bool `json:"replaceFonts,omitempty"` + + // DefaultFont is the default font to use when replacing + DefaultFont *string `json:"defaultFont,omitempty"` + + // OutputFormat is the output format (default: docx) + OutputFormat *string `json:"outputFormat,omitempty"` + + // Metadata is additional metadata + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// GenerateTemplateResponse is the response from template generation +type GenerateTemplateResponse struct { + // Success indicates whether generation was successful + Success bool `json:"success"` + + // DeliverableID is the deliverable ID + DeliverableID *string `json:"deliverableId,omitempty"` + + // Buffer is the generated document buffer (if returnBuffer is true) + Buffer []byte `json:"buffer,omitempty"` + + // DownloadURL is the document download URL + DownloadURL *string `json:"downloadUrl,omitempty"` + + // Message is the response message + Message *string `json:"message,omitempty"` + + // Error contains error details if generation failed + Error *string `json:"error,omitempty"` +} + +// TurboTemplateClient provides template generation operations +type TurboTemplateClient struct { + httpClient *HTTPClient +} + +// Generate generates a document from a template with variables +// +// Supports advanced templating features: +// - Simple variable substitution: {customer_name} +// - Nested objects: {user.firstName} +// - Loops: {#products}...{/products} +// - Conditionals: {#if condition}...{/if} +// - Expressions: {price + tax} +// - Filters: {name | uppercase} +// +// Example: +// +// // Simple variable substitution +// result, err := client.TurboTemplate.Generate(ctx, &GenerateTemplateRequest{ +// TemplateID: "template-uuid", +// Variables: []TemplateVariable{ +// {Placeholder: "{customer_name}", Value: "John Doe"}, +// {Placeholder: "{order_total}", Value: 1500}, +// }, +// }) +// +// // Advanced: nested objects with dot notation +// mimeTypeJSON := MimeTypeJSON +// result, err := client.TurboTemplate.Generate(ctx, &GenerateTemplateRequest{ +// TemplateID: "template-uuid", +// Variables: []TemplateVariable{ +// { +// Placeholder: "{user}", +// MimeType: &mimeTypeJSON, +// Value: map[string]interface{}{ +// "firstName": "John", +// "email": "john@example.com", +// }, +// }, +// }, +// }) +// // Template can use: {user.firstName}, {user.email} +// +// // Advanced: loops with arrays +// result, err := client.TurboTemplate.Generate(ctx, &GenerateTemplateRequest{ +// TemplateID: "template-uuid", +// Variables: []TemplateVariable{ +// { +// Placeholder: "{products}", +// MimeType: &mimeTypeJSON, +// Value: []map[string]interface{}{ +// {"name": "Laptop", "price": 999}, +// {"name": "Mouse", "price": 29}, +// }, +// }, +// }, +// }) +// // Template can use: {#products}{name}: ${price}{/products} +func (c *TurboTemplateClient) Generate(ctx context.Context, req *GenerateTemplateRequest) (*GenerateTemplateResponse, error) { + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + if req.TemplateID == "" { + return nil, fmt.Errorf("templateId is required") + } + + if len(req.Variables) == 0 { + return nil, fmt.Errorf("variables are required") + } + + // Validate variables + for i, v := range req.Variables { + if v.Placeholder == "" { + return nil, fmt.Errorf("variable %d must have Placeholder", i) + } + if v.Name == "" { + return nil, fmt.Errorf("variable %d must have Name", i) + } + if v.Value == nil && (v.Text == nil || *v.Text == "") { + return nil, fmt.Errorf("variable %d (%s) must have either Value or Text", i, v.Placeholder) + } + } + + // Marshal request to JSON + body, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + // Make request + resp, err := c.httpClient.Post(ctx, "/v1/deliverable", "application/json", bytes.NewReader(body)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Parse response + var result GenerateTemplateResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} + +// Helper functions for creating common variable types + +// NewSimpleVariable creates a simple text variable +// name: variable name +// value: variable value +// placeholder: optional custom placeholder (pass empty string to use default {name}) +func NewSimpleVariable(name string, value interface{}, placeholder ...string) TemplateVariable { + p := "" + if len(placeholder) > 0 && placeholder[0] != "" { + p = placeholder[0] + } else if len(name) > 0 && name[0] == '{' { + p = name + } else { + p = "{" + name + "}" + } + return TemplateVariable{ + Placeholder: p, + Name: name, + Value: value, + } +} + +// NewNestedVariable creates a nested object variable +// name: variable name +// value: nested object/map value +// placeholder: optional custom placeholder (pass empty string to use default {name}) +func NewNestedVariable(name string, value map[string]interface{}, placeholder ...string) TemplateVariable { + p := "" + if len(placeholder) > 0 && placeholder[0] != "" { + p = placeholder[0] + } else if len(name) > 0 && name[0] == '{' { + p = name + } else { + p = "{" + name + "}" + } + mimeType := MimeTypeJSON + usesAdvanced := true + return TemplateVariable{ + Placeholder: p, + Name: name, + Value: value, + MimeType: &mimeType, + UsesAdvancedTemplatingEngine: &usesAdvanced, + } +} + +// NewLoopVariable creates a loop/array variable +// name: variable name +// value: array/slice value for iteration +// placeholder: optional custom placeholder (pass empty string to use default {name}) +func NewLoopVariable(name string, value []interface{}, placeholder ...string) TemplateVariable { + p := "" + if len(placeholder) > 0 && placeholder[0] != "" { + p = placeholder[0] + } else if len(name) > 0 && name[0] == '{' { + p = name + } else { + p = "{" + name + "}" + } + mimeType := MimeTypeJSON + usesAdvanced := true + return TemplateVariable{ + Placeholder: p, + Name: name, + Value: value, + MimeType: &mimeType, + UsesAdvancedTemplatingEngine: &usesAdvanced, + } +} + +// NewConditionalVariable creates a conditional variable +// name: variable name +// value: conditional value (typically boolean) +// placeholder: optional custom placeholder (pass empty string to use default {name}) +func NewConditionalVariable(name string, value interface{}, placeholder ...string) TemplateVariable { + p := "" + if len(placeholder) > 0 && placeholder[0] != "" { + p = placeholder[0] + } else if len(name) > 0 && name[0] == '{' { + p = name + } else { + p = "{" + name + "}" + } + usesAdvanced := true + return TemplateVariable{ + Placeholder: p, + Name: name, + Value: value, + UsesAdvancedTemplatingEngine: &usesAdvanced, + } +} + +// NewImageVariable creates an image variable +// name: variable name +// imageURL: image URL or base64 data +// placeholder: optional custom placeholder (pass empty string to use default {name}) +func NewImageVariable(name string, imageURL string, placeholder ...string) TemplateVariable { + p := "" + if len(placeholder) > 0 && placeholder[0] != "" { + p = placeholder[0] + } else if len(name) > 0 && name[0] == '{' { + p = name + } else { + p = "{" + name + "}" + } + mimeType := MimeTypeImage + return TemplateVariable{ + Placeholder: p, + Name: name, + Value: imageURL, + MimeType: &mimeType, + } +} From 0c700651ef73a321847dc2884cd2b624cf3f1a1d Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 22 Jan 2026 05:43:50 +0000 Subject: [PATCH 04/39] feat: add java sdk for advanced templating Signed-off-by: Kushal --- .../java-sdk/examples/AdvancedTemplating.java | 316 ++++++++++++++++ .../java/com/turbodocx/TurboDocxClient.java | 9 + .../java/com/turbodocx/TurboTemplate.java | 85 +++++ .../models/GenerateTemplateRequest.java | 167 +++++++++ .../models/GenerateTemplateResponse.java | 79 ++++ .../turbodocx/models/TemplateVariable.java | 339 ++++++++++++++++++ .../turbodocx/models/VariableMimeType.java | 36 ++ 7 files changed, 1031 insertions(+) create mode 100644 packages/java-sdk/examples/AdvancedTemplating.java create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java create mode 100644 packages/java-sdk/src/main/java/com/turbodocx/models/VariableMimeType.java diff --git a/packages/java-sdk/examples/AdvancedTemplating.java b/packages/java-sdk/examples/AdvancedTemplating.java new file mode 100644 index 0000000..c67c491 --- /dev/null +++ b/packages/java-sdk/examples/AdvancedTemplating.java @@ -0,0 +1,316 @@ +package examples; + +import com.turbodocx.TurboDocxClient; +import com.turbodocx.TurboDocxException; +import com.turbodocx.models.*; + +import java.util.*; + +/** + * TurboTemplate Advanced Templating Examples + *

+ * This file demonstrates the advanced templating features introduced + * in the RapidDocxBackend PR #1057. + */ +public class AdvancedTemplating { + + private static TurboDocxClient client; + + public static void main(String[] args) { + // Configure the client + client = new TurboDocxClient.Builder() + .apiKey(System.getenv("TURBODOCX_API_KEY")) + .orgId(System.getenv("TURBODOCX_ORG_ID")) + .senderEmail("api@example.com") + .build(); + + // Uncomment the examples you want to run: + // simpleSubstitution(); + // nestedObjects(); + // loopsAndArrays(); + // conditionals(); + // expressionsAndCalculations(); + // complexInvoice(); + // usingHelpers(); + + System.out.println("Examples ready to run!"); + } + + /** + * Example 1: Simple Variable Substitution + *

+ * Template: "Dear {customer_name}, your order total is ${order_total}." + */ + public static void simpleSubstitution() { + try { + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-id") + .variables(Arrays.asList( + TemplateVariable.simple("customer_name", "Person A"), + TemplateVariable.simple("order_total", 1500), + TemplateVariable.simple("order_date", "2024-01-15") + )) + .build() + ); + + System.out.println("Document generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } + + /** + * Example 2: Nested Objects with Dot Notation + *

+ * Template: "Name: {user.name}, Email: {user.email}, Company: {user.profile.company}" + */ + public static void nestedObjects() { + try { + Map profile = new HashMap<>(); + profile.put("company", "Company ABC"); + profile.put("title", "Title A"); + profile.put("location", "Test City, TS"); + + Map user = new HashMap<>(); + user.put("name", "Person A"); + user.put("email", "persona@example.com"); + user.put("profile", profile); + + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-id") + .variables(Collections.singletonList( + TemplateVariable.nested("user", user) + )) + .build() + ); + + System.out.println("Document with nested data generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } + + /** + * Example 3: Loops/Arrays + *

+ * Template: + * {#items} + * - {name}: {quantity} x ${price} = ${quantity * price} + * {/items} + */ + public static void loopsAndArrays() { + try { + List> items = Arrays.asList( + Map.of("name", "Item A", "quantity", 5, "price", 100, "sku", "ITM-001"), + Map.of("name", "Item B", "quantity", 3, "price", 200, "sku", "ITM-002"), + Map.of("name", "Item C", "quantity", 10, "price", 50, "sku", "ITM-003") + ); + + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-id") + .variables(Collections.singletonList( + TemplateVariable.loop("items", items) + )) + .build() + ); + + System.out.println("Document with loop generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } + + /** + * Example 4: Conditionals + *

+ * Template: + * {#if is_premium} + * Premium Member Discount: {discount * 100}% + * {/if} + * {#if !is_premium} + * Become a premium member for exclusive discounts! + * {/if} + */ + public static void conditionals() { + try { + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-id") + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{is_premium}") + .name("is_premium") + .mimeType(VariableMimeType.JSON) + .value(true) + .build(), + TemplateVariable.builder() + .placeholder("{discount}") + .name("discount") + .mimeType(VariableMimeType.JSON) + .value(0.2) + .build() + )) + .build() + ); + + System.out.println("Document with conditionals generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } + + /** + * Example 5: Expressions and Calculations + *

+ * Template: "Subtotal: ${subtotal}, Tax: ${subtotal * tax_rate}, Total: ${subtotal * (1 + tax_rate)}" + */ + public static void expressionsAndCalculations() { + try { + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-id") + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{subtotal}") + .name("subtotal") + .mimeType(VariableMimeType.TEXT) + .value("1000") + .usesAdvancedTemplatingEngine(true) + .build(), + TemplateVariable.builder() + .placeholder("{tax_rate}") + .name("tax_rate") + .mimeType(VariableMimeType.TEXT) + .value("0.08") + .usesAdvancedTemplatingEngine(true) + .build() + )) + .build() + ); + + System.out.println("Document with expressions generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } + + /** + * Example 6: Complex Invoice Example + *

+ * Combines multiple features: nested objects, loops, conditionals, expressions + */ + public static void complexInvoice() { + try { + // Customer info (nested object) + Map address = new HashMap<>(); + address.put("street", "123 Test Street"); + address.put("city", "Test City"); + address.put("state", "TS"); + address.put("zip", "00000"); + + Map customer = new HashMap<>(); + customer.put("name", "Company XYZ"); + customer.put("email", "billing@companyxyz.example.com"); + customer.put("address", address); + + // Line items (array for loops) + List> items = Arrays.asList( + Map.of( + "description", "Service A - Type 1", + "quantity", 40, + "rate", 150 + ), + Map.of( + "description", "Service B - Type 2", + "quantity", 1, + "rate", 5000 + ), + Map.of( + "description", "Service C - Type 3", + "quantity", 12, + "rate", 500 + ) + ); + + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("invoice-template-id") + .name("Invoice - Company XYZ") + .description("Monthly invoice for Company XYZ") + .variables(Arrays.asList( + TemplateVariable.nested("customer", customer), + TemplateVariable.simple("invoice_number", "INV-2024-001"), + TemplateVariable.simple("invoice_date", "2024-01-15"), + TemplateVariable.simple("due_date", "2024-02-14"), + TemplateVariable.loop("items", items), + TemplateVariable.builder() + .placeholder("{tax_rate}") + .name("tax_rate") + .mimeType(VariableMimeType.TEXT) + .value("0.08") + .usesAdvancedTemplatingEngine(true) + .build(), + TemplateVariable.builder() + .placeholder("{is_premium}") + .name("is_premium") + .mimeType(VariableMimeType.JSON) + .value(true) + .build(), + TemplateVariable.builder() + .placeholder("{premium_discount}") + .name("premium_discount") + .mimeType(VariableMimeType.TEXT) + .value("0.05") + .usesAdvancedTemplatingEngine(true) + .build(), + TemplateVariable.simple("payment_terms", "Net 30"), + TemplateVariable.simple("notes", "Thank you for your business!") + )) + .build() + ); + + System.out.println("Complex invoice generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } + + /** + * Example 7: Using Helper Functions + */ + public static void usingHelpers() { + try { + Map company = Map.of( + "name", "Company ABC", + "headquarters", "Test City", + "employees", 500 + ); + + List> departments = Arrays.asList( + Map.of("name", "Dept A", "headcount", 200), + Map.of("name", "Dept B", "headcount", 150), + Map.of("name", "Dept C", "headcount", 100) + ); + + GenerateTemplateResponse response = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-id") + .variables(Arrays.asList( + TemplateVariable.simple("title", "Quarterly Report"), + TemplateVariable.nested("company", company), + TemplateVariable.loop("departments", departments), + TemplateVariable.conditional("show_financials", true), + TemplateVariable.image("company_logo", "https://example.com/logo.png") + )) + .build() + ); + + System.out.println("Document with helpers generated: " + response.getDeliverableId()); + } catch (TurboDocxException e) { + System.err.println("Error: " + e.getMessage()); + } + } +} diff --git a/packages/java-sdk/src/main/java/com/turbodocx/TurboDocxClient.java b/packages/java-sdk/src/main/java/com/turbodocx/TurboDocxClient.java index cb0724e..77d98b9 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/TurboDocxClient.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/TurboDocxClient.java @@ -5,10 +5,12 @@ */ public class TurboDocxClient { private final TurboSign turboSign; + private final TurboTemplate turboTemplate; private TurboDocxClient(Builder builder) { HttpClient httpClient = new HttpClient(builder.baseUrl, builder.apiKey, builder.accessToken, builder.orgId, builder.senderEmail, builder.senderName); this.turboSign = new TurboSign(httpClient); + this.turboTemplate = new TurboTemplate(httpClient); } /** @@ -18,6 +20,13 @@ public TurboSign turboSign() { return turboSign; } + /** + * Get the TurboTemplate client for document templating operations + */ + public TurboTemplate turboTemplate() { + return turboTemplate; + } + /** * Builder for TurboDocxClient */ diff --git a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java new file mode 100644 index 0000000..0437a60 --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java @@ -0,0 +1,85 @@ +package com.turbodocx; + +import com.turbodocx.models.GenerateTemplateRequest; +import com.turbodocx.models.GenerateTemplateResponse; + +/** + * TurboTemplate provides document templating operations + *

+ * Supports advanced templating features: + *

    + *
  • Simple variable substitution: {customer_name}
  • + *
  • Nested objects: {user.firstName}
  • + *
  • Loops: {#products}...{/products}
  • + *
  • Conditionals: {#if condition}...{/if}
  • + *
  • Expressions: {price + tax}
  • + *
  • Filters: {name | uppercase}
  • + *
+ * + *

Example usage:

+ *
{@code
+ * // Simple variable substitution
+ * GenerateTemplateResponse response = client.turboTemplate().generate(
+ *     GenerateTemplateRequest.builder()
+ *         .templateId("template-uuid")
+ *         .variables(Arrays.asList(
+ *             TemplateVariable.simple("customer_name", "John Doe"),
+ *             TemplateVariable.simple("order_total", 1500)
+ *         ))
+ *         .build()
+ * );
+ *
+ * // Advanced: nested objects with dot notation
+ * Map user = new HashMap<>();
+ * user.put("firstName", "John");
+ * user.put("email", "john@example.com");
+ *
+ * GenerateTemplateResponse response = client.turboTemplate().generate(
+ *     GenerateTemplateRequest.builder()
+ *         .templateId("template-uuid")
+ *         .variables(Arrays.asList(
+ *             TemplateVariable.nested("user", user)
+ *         ))
+ *         .build()
+ * );
+ * // Template can use: {user.firstName}, {user.email}
+ *
+ * // Advanced: loops with arrays
+ * List> products = Arrays.asList(
+ *     Map.of("name", "Laptop", "price", 999),
+ *     Map.of("name", "Mouse", "price", 29)
+ * );
+ *
+ * GenerateTemplateResponse response = client.turboTemplate().generate(
+ *     GenerateTemplateRequest.builder()
+ *         .templateId("template-uuid")
+ *         .variables(Arrays.asList(
+ *             TemplateVariable.loop("products", products)
+ *         ))
+ *         .build()
+ * );
+ * // Template can use: {#products}{name}: ${price}{/products}
+ * }
+ */ +public class TurboTemplate { + private final HttpClient httpClient; + + public TurboTemplate(HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * Generate a document from a template with variables + * + * @param request Template ID and variables + * @return Generated document response + * @throws TurboDocxException if the request fails + */ + public GenerateTemplateResponse generate(GenerateTemplateRequest request) throws TurboDocxException { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + + return httpClient.post("/v1/deliverable", request, GenerateTemplateResponse.class); + } +} diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java new file mode 100644 index 0000000..4f421aa --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java @@ -0,0 +1,167 @@ +package com.turbodocx.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +/** + * Request for generating a document from template + */ +public class GenerateTemplateRequest { + @JsonProperty("templateId") + private String templateId; + + @JsonProperty("variables") + private List variables; + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("replaceFonts") + private Boolean replaceFonts; + + @JsonProperty("defaultFont") + private String defaultFont; + + @JsonProperty("outputFormat") + private String outputFormat; + + @JsonProperty("metadata") + private Map metadata; + + // Constructors + public GenerateTemplateRequest() { + } + + public GenerateTemplateRequest(String templateId, List variables) { + this.templateId = templateId; + this.variables = variables; + } + + // Getters and Setters + public String getTemplateId() { + return templateId; + } + + public void setTemplateId(String templateId) { + this.templateId = templateId; + } + + public List getVariables() { + return variables; + } + + public void setVariables(List variables) { + this.variables = variables; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getReplaceFonts() { + return replaceFonts; + } + + public void setReplaceFonts(Boolean replaceFonts) { + this.replaceFonts = replaceFonts; + } + + public String getDefaultFont() { + return defaultFont; + } + + public void setDefaultFont(String defaultFont) { + this.defaultFont = defaultFont; + } + + public String getOutputFormat() { + return outputFormat; + } + + public void setOutputFormat(String outputFormat) { + this.outputFormat = outputFormat; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + // Builder pattern + public static class Builder { + private final GenerateTemplateRequest request = new GenerateTemplateRequest(); + + public Builder templateId(String templateId) { + request.templateId = templateId; + return this; + } + + public Builder variables(List variables) { + request.variables = variables; + return this; + } + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder replaceFonts(Boolean replaceFonts) { + request.replaceFonts = replaceFonts; + return this; + } + + public Builder defaultFont(String defaultFont) { + request.defaultFont = defaultFont; + return this; + } + + public Builder outputFormat(String outputFormat) { + request.outputFormat = outputFormat; + return this; + } + + public Builder metadata(Map metadata) { + request.metadata = metadata; + return this; + } + + public GenerateTemplateRequest build() { + if (request.templateId == null || request.templateId.isEmpty()) { + throw new IllegalStateException("templateId is required"); + } + if (request.variables == null || request.variables.isEmpty()) { + throw new IllegalStateException("variables are required"); + } + return request; + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java new file mode 100644 index 0000000..66ed09d --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java @@ -0,0 +1,79 @@ +package com.turbodocx.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from template generation + */ +public class GenerateTemplateResponse { + @JsonProperty("success") + private boolean success; + + @JsonProperty("deliverableId") + private String deliverableId; + + @JsonProperty("buffer") + private byte[] buffer; + + @JsonProperty("downloadUrl") + private String downloadUrl; + + @JsonProperty("message") + private String message; + + @JsonProperty("error") + private String error; + + // Constructors + public GenerateTemplateResponse() { + } + + // Getters and Setters + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getDeliverableId() { + return deliverableId; + } + + public void setDeliverableId(String deliverableId) { + this.deliverableId = deliverableId; + } + + public byte[] getBuffer() { + return buffer; + } + + public void setBuffer(byte[] buffer) { + this.buffer = buffer; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setDownloadUrl(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } +} diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java new file mode 100644 index 0000000..b6b91c3 --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java @@ -0,0 +1,339 @@ +package com.turbodocx.models; + +import com.fasterxml.jackson.annotation.json.JsonProperty; +import java.util.List; +import java.util.Map; + +/** + * Variable configuration for template generation + *

+ * Supports both simple text replacement and advanced templating with Angular-like expressions + */ +public class TemplateVariable { + @JsonProperty("placeholder") + private String placeholder; + + @JsonProperty("name") + private String name; + + @JsonProperty("value") + private Object value; + + @JsonProperty("text") + private String text; + + @JsonProperty("mimeType") + private String mimeType; + + @JsonProperty("usesAdvancedTemplatingEngine") + private Boolean usesAdvancedTemplatingEngine; + + @JsonProperty("nestedInAdvancedTemplatingEngine") + private Boolean nestedInAdvancedTemplatingEngine; + + @JsonProperty("allowRichTextInjection") + private Boolean allowRichTextInjection; + + @JsonProperty("description") + private String description; + + @JsonProperty("defaultValue") + private Boolean defaultValue; + + @JsonProperty("nestedVariables") + private List nestedVariables; + + @JsonProperty("subvariables") + private List subvariables; + + // Constructors + public TemplateVariable() { + } + + public TemplateVariable(String placeholder, Object value) { + this.placeholder = placeholder; + this.value = value; + } + + // Getters and Setters + public String getPlaceholder() { + return placeholder; + } + + public void setPlaceholder(String placeholder) { + this.placeholder = placeholder; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public void setMimeType(VariableMimeType mimeType) { + this.mimeType = mimeType.getValue(); + } + + public Boolean getUsesAdvancedTemplatingEngine() { + return usesAdvancedTemplatingEngine; + } + + public void setUsesAdvancedTemplatingEngine(Boolean usesAdvancedTemplatingEngine) { + this.usesAdvancedTemplatingEngine = usesAdvancedTemplatingEngine; + } + + public Boolean getNestedInAdvancedTemplatingEngine() { + return nestedInAdvancedTemplatingEngine; + } + + public void setNestedInAdvancedTemplatingEngine(Boolean nestedInAdvancedTemplatingEngine) { + this.nestedInAdvancedTemplatingEngine = nestedInAdvancedTemplatingEngine; + } + + public Boolean getAllowRichTextInjection() { + return allowRichTextInjection; + } + + public void setAllowRichTextInjection(Boolean allowRichTextInjection) { + this.allowRichTextInjection = allowRichTextInjection; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(Boolean defaultValue) { + this.defaultValue = defaultValue; + } + + public List getNestedVariables() { + return nestedVariables; + } + + public void setNestedVariables(List nestedVariables) { + this.nestedVariables = nestedVariables; + } + + public List getSubvariables() { + return subvariables; + } + + public void setSubvariables(List subvariables) { + this.subvariables = subvariables; + } + + // Builder pattern + public static class Builder { + private final TemplateVariable variable = new TemplateVariable(); + + public Builder placeholder(String placeholder) { + variable.placeholder = placeholder; + return this; + } + + public Builder name(String name) { + variable.name = name; + return this; + } + + public Builder value(Object value) { + variable.value = value; + return this; + } + + public Builder text(String text) { + variable.text = text; + return this; + } + + public Builder mimeType(String mimeType) { + variable.mimeType = mimeType; + return this; + } + + public Builder mimeType(VariableMimeType mimeType) { + variable.mimeType = mimeType.getValue(); + return this; + } + + public Builder usesAdvancedTemplatingEngine(Boolean usesAdvancedTemplatingEngine) { + variable.usesAdvancedTemplatingEngine = usesAdvancedTemplatingEngine; + return this; + } + + public Builder nestedInAdvancedTemplatingEngine(Boolean nestedInAdvancedTemplatingEngine) { + variable.nestedInAdvancedTemplatingEngine = nestedInAdvancedTemplatingEngine; + return this; + } + + public Builder allowRichTextInjection(Boolean allowRichTextInjection) { + variable.allowRichTextInjection = allowRichTextInjection; + return this; + } + + public Builder description(String description) { + variable.description = description; + return this; + } + + public Builder defaultValue(Boolean defaultValue) { + variable.defaultValue = defaultValue; + return this; + } + + public Builder nestedVariables(List nestedVariables) { + variable.nestedVariables = nestedVariables; + return this; + } + + public Builder subvariables(List subvariables) { + variable.subvariables = subvariables; + return this; + } + + public TemplateVariable build() { + if (variable.placeholder == null || variable.placeholder.isEmpty()) { + throw new IllegalStateException("placeholder must be set"); + } + if (variable.name == null || variable.name.isEmpty()) { + throw new IllegalStateException("name must be set"); + } + if (variable.value == null && variable.text == null) { + throw new IllegalStateException("Either value or text must be set"); + } + return variable; + } + } + + public static Builder builder() { + return new Builder(); + } + + // Helper factory methods + + /** + * Creates a simple text variable + * @param name The variable name (used as placeholder if not provided) + * @param value The value to substitute + * @param placeholder Optional placeholder override (defaults to {name}) + */ + public static TemplateVariable simple(String name, Object value, String... placeholder) { + String p = getPlaceholder(name, placeholder); + TemplateVariable var = new TemplateVariable(p, value); + var.setName(name); + return var; + } + + /** + * Creates a variable for nested objects (dot notation access) + * @param name The variable name + * @param value The nested object value + * @param placeholder Optional placeholder override (defaults to {name}) + */ + public static TemplateVariable nested(String name, Map value, String... placeholder) { + String p = getPlaceholder(name, placeholder); + return builder() + .placeholder(p) + .name(name) + .value(value) + .mimeType(VariableMimeType.JSON) + .usesAdvancedTemplatingEngine(true) + .build(); + } + + /** + * Creates a variable for array loops + * @param name The variable name + * @param value The array/list value + * @param placeholder Optional placeholder override (defaults to {name}) + */ + public static TemplateVariable loop(String name, List value, String... placeholder) { + String p = getPlaceholder(name, placeholder); + return builder() + .placeholder(p) + .name(name) + .value(value) + .mimeType(VariableMimeType.JSON) + .usesAdvancedTemplatingEngine(true) + .build(); + } + + /** + * Creates a variable for conditionals + * @param name The variable name + * @param value The boolean or truthy value + * @param placeholder Optional placeholder override (defaults to {name}) + */ + public static TemplateVariable conditional(String name, Object value, String... placeholder) { + String p = getPlaceholder(name, placeholder); + return builder() + .placeholder(p) + .name(name) + .value(value) + .usesAdvancedTemplatingEngine(true) + .build(); + } + + /** + * Creates a variable for images + * @param name The variable name + * @param imageUrl The image URL + * @param placeholder Optional placeholder override (defaults to {name}) + */ + public static TemplateVariable image(String name, String imageUrl, String... placeholder) { + String p = getPlaceholder(name, placeholder); + return builder() + .placeholder(p) + .name(name) + .value(imageUrl) + .mimeType(VariableMimeType.IMAGE) + .build(); + } + + /** + * Helper to determine placeholder from name and optional override + */ + private static String getPlaceholder(String name, String... placeholder) { + if (placeholder.length > 0 && placeholder[0] != null && !placeholder[0].isEmpty()) { + return placeholder[0]; + } + if (name.startsWith("{")) { + return name; + } + return "{" + name + "}"; + } +} diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/VariableMimeType.java b/packages/java-sdk/src/main/java/com/turbodocx/models/VariableMimeType.java new file mode 100644 index 0000000..0b13ea9 --- /dev/null +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/VariableMimeType.java @@ -0,0 +1,36 @@ +package com.turbodocx.models; + +/** + * Variable MIME types supported by TurboDocx + */ +public enum VariableMimeType { + TEXT("text"), + HTML("html"), + IMAGE("image"), + MARKDOWN("markdown"), + JSON("json"); + + private final String value; + + VariableMimeType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + public static VariableMimeType fromString(String value) { + for (VariableMimeType type : VariableMimeType.values()) { + if (type.value.equalsIgnoreCase(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown VariableMimeType: " + value); + } +} From 25e079007f3e3239c58de9fe441a34277ab49964 Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 22 Jan 2026 18:34:12 +0000 Subject: [PATCH 05/39] tests: add tests for each sdk Signed-off-by: Kushal --- packages/go-sdk/turbotemplate_test.go | 668 ++++++++++++++++ .../java/com/turbodocx/TurboTemplateTest.java | 642 ++++++++++++++++ packages/js-sdk/tests/turbotemplate.test.ts | 710 ++++++++++++++++++ packages/py-sdk/tests/test_turbotemplate.py | 679 +++++++++++++++++ 4 files changed, 2699 insertions(+) create mode 100644 packages/go-sdk/turbotemplate_test.go create mode 100644 packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java create mode 100644 packages/js-sdk/tests/turbotemplate.test.ts create mode 100644 packages/py-sdk/tests/test_turbotemplate.py diff --git a/packages/go-sdk/turbotemplate_test.go b/packages/go-sdk/turbotemplate_test.go new file mode 100644 index 0000000..fa1001a --- /dev/null +++ b/packages/go-sdk/turbotemplate_test.go @@ -0,0 +1,668 @@ +package turbodocx + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewSimpleVariable(t *testing.T) { + t.Run("creates simple variable with name and value", func(t *testing.T) { + variable := NewSimpleVariable("customer_name", "Person A") + + assert.Equal(t, "{customer_name}", variable.Placeholder) + assert.Equal(t, "customer_name", variable.Name) + assert.Equal(t, "Person A", variable.Value) + }) + + t.Run("creates simple variable with number value", func(t *testing.T) { + variable := NewSimpleVariable("order_total", 1500) + + assert.Equal(t, "{order_total}", variable.Placeholder) + assert.Equal(t, "order_total", variable.Name) + assert.Equal(t, 1500, variable.Value) + }) + + t.Run("creates simple variable with boolean value", func(t *testing.T) { + variable := NewSimpleVariable("is_active", true) + + assert.Equal(t, "{is_active}", variable.Placeholder) + assert.Equal(t, "is_active", variable.Name) + assert.Equal(t, true, variable.Value) + }) + + t.Run("uses custom placeholder when provided", func(t *testing.T) { + variable := NewSimpleVariable("customer_name", "Person A", "{custom_placeholder}") + + assert.Equal(t, "{custom_placeholder}", variable.Placeholder) + assert.Equal(t, "customer_name", variable.Name) + }) + + t.Run("handles name with curly braces", func(t *testing.T) { + variable := NewSimpleVariable("{customer_name}", "Person A") + + assert.Equal(t, "{customer_name}", variable.Placeholder) + assert.Equal(t, "{customer_name}", variable.Name) + }) +} + +func TestNewNestedVariable(t *testing.T) { + t.Run("creates nested variable with object value", func(t *testing.T) { + variable := NewNestedVariable("user", map[string]interface{}{ + "firstName": "Foo", + "lastName": "Bar", + "email": "foo@example.com", + }) + + assert.Equal(t, "{user}", variable.Placeholder) + assert.Equal(t, "user", variable.Name) + assert.Equal(t, map[string]interface{}{ + "firstName": "Foo", + "lastName": "Bar", + "email": "foo@example.com", + }, variable.Value) + assert.NotNil(t, variable.MimeType) + assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.NotNil(t, variable.UsesAdvancedTemplatingEngine) + assert.True(t, *variable.UsesAdvancedTemplatingEngine) + }) + + t.Run("creates nested variable with deeply nested object", func(t *testing.T) { + variable := NewNestedVariable("company", map[string]interface{}{ + "name": "Company ABC", + "address": map[string]interface{}{ + "street": "123 Test Street", + "city": "Test City", + "state": "TS", + }, + }) + + assert.Equal(t, "{company}", variable.Placeholder) + assert.NotNil(t, variable.MimeType) + assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.True(t, *variable.UsesAdvancedTemplatingEngine) + }) + + t.Run("uses custom placeholder when provided", func(t *testing.T) { + variable := NewNestedVariable("user", map[string]interface{}{"name": "Test"}, "{custom_user}") + + assert.Equal(t, "{custom_user}", variable.Placeholder) + assert.Equal(t, "user", variable.Name) + }) +} + +func TestNewLoopVariable(t *testing.T) { + t.Run("creates loop variable with array value", func(t *testing.T) { + variable := NewLoopVariable("items", []interface{}{ + map[string]interface{}{"name": "Item A", "price": 100}, + map[string]interface{}{"name": "Item B", "price": 200}, + }) + + assert.Equal(t, "{items}", variable.Placeholder) + assert.Equal(t, "items", variable.Name) + assert.NotNil(t, variable.MimeType) + assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.True(t, *variable.UsesAdvancedTemplatingEngine) + }) + + t.Run("creates loop variable with empty array", func(t *testing.T) { + variable := NewLoopVariable("products", []interface{}{}) + + assert.Equal(t, "{products}", variable.Placeholder) + assert.Equal(t, []interface{}{}, variable.Value) + assert.Equal(t, MimeTypeJSON, *variable.MimeType) + }) + + t.Run("creates loop variable with primitive array", func(t *testing.T) { + variable := NewLoopVariable("tags", []interface{}{"tag1", "tag2", "tag3"}) + + assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, variable.Value) + }) + + t.Run("uses custom placeholder when provided", func(t *testing.T) { + variable := NewLoopVariable("items", []interface{}{}, "{line_items}") + + assert.Equal(t, "{line_items}", variable.Placeholder) + assert.Equal(t, "items", variable.Name) + }) +} + +func TestNewConditionalVariable(t *testing.T) { + t.Run("creates conditional variable with boolean true", func(t *testing.T) { + variable := NewConditionalVariable("is_premium", true) + + assert.Equal(t, "{is_premium}", variable.Placeholder) + assert.Equal(t, "is_premium", variable.Name) + assert.Equal(t, true, variable.Value) + assert.True(t, *variable.UsesAdvancedTemplatingEngine) + }) + + t.Run("creates conditional variable with boolean false", func(t *testing.T) { + variable := NewConditionalVariable("show_discount", false) + + assert.Equal(t, false, variable.Value) + assert.True(t, *variable.UsesAdvancedTemplatingEngine) + }) + + t.Run("creates conditional variable with truthy value", func(t *testing.T) { + variable := NewConditionalVariable("count", 5) + + assert.Equal(t, 5, variable.Value) + }) + + t.Run("uses custom placeholder when provided", func(t *testing.T) { + variable := NewConditionalVariable("is_active", true, "{active_flag}") + + assert.Equal(t, "{active_flag}", variable.Placeholder) + assert.Equal(t, "is_active", variable.Name) + }) +} + +func TestNewImageVariable(t *testing.T) { + t.Run("creates image variable with URL", func(t *testing.T) { + variable := NewImageVariable("logo", "https://example.com/logo.png") + + assert.Equal(t, "{logo}", variable.Placeholder) + assert.Equal(t, "logo", variable.Name) + assert.Equal(t, "https://example.com/logo.png", variable.Value) + assert.NotNil(t, variable.MimeType) + assert.Equal(t, MimeTypeImage, *variable.MimeType) + }) + + t.Run("creates image variable with base64", func(t *testing.T) { + base64Image := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." + variable := NewImageVariable("signature", base64Image) + + assert.Equal(t, base64Image, variable.Value) + assert.Equal(t, MimeTypeImage, *variable.MimeType) + }) + + t.Run("uses custom placeholder when provided", func(t *testing.T) { + variable := NewImageVariable("logo", "https://example.com/logo.png", "{company_logo}") + + assert.Equal(t, "{company_logo}", variable.Placeholder) + assert.Equal(t, "logo", variable.Name) + }) +} + +func TestTurboTemplateClient_Generate(t *testing.T) { + t.Run("generates document with simple variables", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/v1/deliverable", r.URL.Path) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization")) + assert.Equal(t, "test-org-id", r.Header.Get("x-rapiddocx-org-id")) + + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.Equal(t, "template-123", reqBody.TemplateID) + assert.Len(t, reqBody.Variables, 2) + assert.Equal(t, "{customer_name}", reqBody.Variables[0].Placeholder) + assert.Equal(t, "customer_name", reqBody.Variables[0].Name) + assert.Equal(t, "Person A", reqBody.Variables[0].Value) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-123", + "message": "Document generated successfully", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Test Document" + desc := "Test description" + result, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{ + {Placeholder: "{customer_name}", Name: "customer_name", Value: "Person A"}, + {Placeholder: "{order_total}", Name: "order_total", Value: 1500}, + }, + }) + + require.NoError(t, err) + assert.True(t, result.Success) + assert.Equal(t, "doc-123", *result.DeliverableID) + }) + + t.Run("generates document with nested object variables", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.Equal(t, "json", string(*reqBody.Variables[0].MimeType)) + assert.True(t, *reqBody.Variables[0].UsesAdvancedTemplatingEngine) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-456", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + mimeTypeJSON := MimeTypeJSON + usesAdvanced := true + name := "Nested Document" + desc := "Document with nested objects" + result, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{ + { + Placeholder: "{user}", + Name: "user", + MimeType: &mimeTypeJSON, + Value: map[string]interface{}{"firstName": "Foo", "lastName": "Bar"}, + UsesAdvancedTemplatingEngine: &usesAdvanced, + }, + }, + }) + + require.NoError(t, err) + assert.True(t, result.Success) + }) + + t.Run("generates document with loop/array variables", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.Equal(t, "{items}", reqBody.Variables[0].Placeholder) + assert.Equal(t, "items", reqBody.Variables[0].Name) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-789", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + mimeTypeJSON := MimeTypeJSON + usesAdvanced := true + name := "Loop Document" + desc := "Document with loops" + result, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{ + { + Placeholder: "{items}", + Name: "items", + MimeType: &mimeTypeJSON, + Value: []map[string]interface{}{{"name": "Item A"}, {"name": "Item B"}}, + UsesAdvancedTemplatingEngine: &usesAdvanced, + }, + }, + }) + + require.NoError(t, err) + assert.True(t, result.Success) + }) + + t.Run("generates document with helper-created variables", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.Len(t, reqBody.Variables, 5) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-helper", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Helper Document" + desc := "Document using helper functions" + result, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{ + NewSimpleVariable("title", "Quarterly Report"), + NewNestedVariable("company", map[string]interface{}{"name": "Company XYZ", "employees": 500}), + NewLoopVariable("departments", []interface{}{map[string]interface{}{"name": "Dept A"}, map[string]interface{}{"name": "Dept B"}}), + NewConditionalVariable("show_financials", true), + NewImageVariable("logo", "https://example.com/logo.png"), + }, + }) + + require.NoError(t, err) + assert.True(t, result.Success) + }) + + t.Run("includes optional request parameters", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.True(t, *reqBody.ReplaceFonts) + assert.Equal(t, "Arial", *reqBody.DefaultFont) + assert.Equal(t, "pdf", *reqBody.OutputFormat) + assert.Equal(t, "value", reqBody.Metadata["customField"]) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-options", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Options Document" + desc := "Document with all options" + replaceFonts := true + defaultFont := "Arial" + outputFormat := "pdf" + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + ReplaceFonts: &replaceFonts, + DefaultFont: &defaultFont, + OutputFormat: &outputFormat, + Metadata: map[string]interface{}{"customField": "value"}, + }) + + require.NoError(t, err) + }) + + t.Run("returns error when templateId is missing", func(t *testing.T) { + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + SenderEmail: "test@example.com", + }) + + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "templateId is required") + }) + + t.Run("returns error when variables are empty", func(t *testing.T) { + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + SenderEmail: "test@example.com", + }) + + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Variables: []TemplateVariable{}, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "variables are required") + }) + + t.Run("returns error when variable has no value or text", func(t *testing.T) { + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + SenderEmail: "test@example.com", + }) + + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Variables: []TemplateVariable{{Placeholder: "{test}", Name: "test"}}, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "must have either Value or Text") + }) + + t.Run("returns error when placeholder is missing", func(t *testing.T) { + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + SenderEmail: "test@example.com", + }) + + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Variables: []TemplateVariable{{Name: "test", Value: "value"}}, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "must have Placeholder") + }) + + t.Run("returns error when name is missing", func(t *testing.T) { + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + SenderEmail: "test@example.com", + }) + + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Variables: []TemplateVariable{{Placeholder: "{test}", Value: "value"}}, + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "must have Name") + }) +} + +func TestTurboTemplateClient_PlaceholderAndNameHandling(t *testing.T) { + t.Run("requires both placeholder and name in generated request", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.Equal(t, "{customer}", reqBody.Variables[0].Placeholder) + assert.Equal(t, "customer", reqBody.Variables[0].Name) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-both", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Both Fields Document" + desc := "Document with both placeholder and name" + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{ + {Placeholder: "{customer}", Name: "customer", Value: "Person A"}, + }, + }) + + require.NoError(t, err) + }) + + t.Run("allows distinct placeholder and name values", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var reqBody GenerateTemplateRequest + json.NewDecoder(r.Body).Decode(&reqBody) + assert.Equal(t, "{cust_name}", reqBody.Variables[0].Placeholder) + assert.Equal(t, "customerFullName", reqBody.Variables[0].Name) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-distinct", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Distinct Fields Document" + desc := "Document with distinct placeholder and name" + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{ + {Placeholder: "{cust_name}", Name: "customerFullName", Value: "Person A"}, + }, + }) + + require.NoError(t, err) + }) +} + +func TestTurboTemplateClient_ErrorHandling(t *testing.T) { + t.Run("handles API errors gracefully", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Template not found", + "code": "TEMPLATE_NOT_FOUND", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Error Document" + desc := "Document that should fail" + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "invalid-template", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + }) + + require.Error(t, err) + notFoundErr, ok := err.(*NotFoundError) + require.True(t, ok, "expected NotFoundError") + assert.Equal(t, 404, notFoundErr.StatusCode) + }) + + t.Run("handles validation errors", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Validation failed: Invalid variable configuration", + "code": "VALIDATION_ERROR", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Validation Error Document" + desc := "Document that should fail validation" + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + }) + + require.Error(t, err) + validationErr, ok := err.(*ValidationError) + require.True(t, ok, "expected ValidationError") + assert.Equal(t, 400, validationErr.StatusCode) + }) + + t.Run("handles rate limit errors", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTooManyRequests) + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Rate limit exceeded", + "code": "RATE_LIMIT_EXCEEDED", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + name := "Rate Limit Document" + desc := "Document that should hit rate limit" + _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Name: &name, + Description: &desc, + Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + }) + + require.Error(t, err) + rateLimitErr, ok := err.(*RateLimitError) + require.True(t, ok, "expected RateLimitError") + assert.Equal(t, 429, rateLimitErr.StatusCode) + }) +} diff --git a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java new file mode 100644 index 0000000..576b1c5 --- /dev/null +++ b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java @@ -0,0 +1,642 @@ +package com.turbodocx; + +import com.google.gson.Gson; +import com.turbodocx.models.*; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.*; + +import java.io.IOException; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * TurboTemplate Module Tests + * + * Tests for advanced templating features: + * - Helper functions (simple, nested, loop, conditional, image) + * - Builder validation + * - Generate template functionality + * - Placeholder and name handling + */ +class TurboTemplateTest { + + private MockWebServer server; + private TurboDocxClient client; + private final Gson gson = new Gson(); + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + + client = new TurboDocxClient.Builder() + .apiKey("test-api-key") + .orgId("test-org-id") + .baseUrl(server.url("/").toString()) + .senderEmail("test@example.com") + .build(); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + // ============================================ + // Helper Function Tests - simple() + // ============================================ + + @Test + @DisplayName("simple() should create variable with name and value") + void simpleVariableWithNameAndValue() { + TemplateVariable variable = TemplateVariable.simple("customer_name", "Person A"); + + assertEquals("{customer_name}", variable.getPlaceholder()); + assertEquals("customer_name", variable.getName()); + assertEquals("Person A", variable.getValue()); + } + + @Test + @DisplayName("simple() should create variable with number value") + void simpleVariableWithNumberValue() { + TemplateVariable variable = TemplateVariable.simple("order_total", 1500); + + assertEquals("{order_total}", variable.getPlaceholder()); + assertEquals("order_total", variable.getName()); + assertEquals(1500, variable.getValue()); + } + + @Test + @DisplayName("simple() should create variable with boolean value") + void simpleVariableWithBooleanValue() { + TemplateVariable variable = TemplateVariable.simple("is_active", true); + + assertEquals("{is_active}", variable.getPlaceholder()); + assertEquals("is_active", variable.getName()); + assertEquals(true, variable.getValue()); + } + + @Test + @DisplayName("simple() should use custom placeholder when provided") + void simpleVariableWithCustomPlaceholder() { + TemplateVariable variable = TemplateVariable.simple("customer_name", "Person A", "{custom_placeholder}"); + + assertEquals("{custom_placeholder}", variable.getPlaceholder()); + assertEquals("customer_name", variable.getName()); + } + + @Test + @DisplayName("simple() should handle name with curly braces") + void simpleVariableWithCurlyBracesInName() { + TemplateVariable variable = TemplateVariable.simple("{customer_name}", "Person A"); + + assertEquals("{customer_name}", variable.getPlaceholder()); + assertEquals("{customer_name}", variable.getName()); + } + + // ============================================ + // Helper Function Tests - nested() + // ============================================ + + @Test + @DisplayName("nested() should create variable with object value") + void nestedVariableWithObjectValue() { + Map user = new HashMap<>(); + user.put("firstName", "Foo"); + user.put("lastName", "Bar"); + user.put("email", "foo@example.com"); + + TemplateVariable variable = TemplateVariable.nested("user", user); + + assertEquals("{user}", variable.getPlaceholder()); + assertEquals("user", variable.getName()); + assertEquals(user, variable.getValue()); + assertEquals("json", variable.getMimeType()); + assertTrue(variable.getUsesAdvancedTemplatingEngine()); + } + + @Test + @DisplayName("nested() should create variable with deeply nested object") + void nestedVariableWithDeeplyNestedObject() { + Map address = new HashMap<>(); + address.put("street", "123 Test Street"); + address.put("city", "Test City"); + address.put("state", "TS"); + + Map company = new HashMap<>(); + company.put("name", "Company ABC"); + company.put("address", address); + + TemplateVariable variable = TemplateVariable.nested("company", company); + + assertEquals("{company}", variable.getPlaceholder()); + assertEquals("json", variable.getMimeType()); + assertTrue(variable.getUsesAdvancedTemplatingEngine()); + } + + @Test + @DisplayName("nested() should use custom placeholder when provided") + void nestedVariableWithCustomPlaceholder() { + Map user = new HashMap<>(); + user.put("name", "Test"); + + TemplateVariable variable = TemplateVariable.nested("user", user, "{custom_user}"); + + assertEquals("{custom_user}", variable.getPlaceholder()); + assertEquals("user", variable.getName()); + } + + // ============================================ + // Helper Function Tests - loop() + // ============================================ + + @Test + @DisplayName("loop() should create variable with array value") + void loopVariableWithArrayValue() { + List> items = Arrays.asList( + Map.of("name", "Item A", "price", 100), + Map.of("name", "Item B", "price", 200) + ); + + TemplateVariable variable = TemplateVariable.loop("items", items); + + assertEquals("{items}", variable.getPlaceholder()); + assertEquals("items", variable.getName()); + assertEquals(items, variable.getValue()); + assertEquals("json", variable.getMimeType()); + assertTrue(variable.getUsesAdvancedTemplatingEngine()); + } + + @Test + @DisplayName("loop() should create variable with empty array") + void loopVariableWithEmptyArray() { + List> products = Collections.emptyList(); + + TemplateVariable variable = TemplateVariable.loop("products", products); + + assertEquals("{products}", variable.getPlaceholder()); + assertEquals(products, variable.getValue()); + assertEquals("json", variable.getMimeType()); + } + + @Test + @DisplayName("loop() should create variable with primitive array") + void loopVariableWithPrimitiveArray() { + List tags = Arrays.asList("tag1", "tag2", "tag3"); + + TemplateVariable variable = TemplateVariable.loop("tags", tags); + + assertEquals(tags, variable.getValue()); + } + + @Test + @DisplayName("loop() should use custom placeholder when provided") + void loopVariableWithCustomPlaceholder() { + List> items = Collections.emptyList(); + + TemplateVariable variable = TemplateVariable.loop("items", items, "{line_items}"); + + assertEquals("{line_items}", variable.getPlaceholder()); + assertEquals("items", variable.getName()); + } + + // ============================================ + // Helper Function Tests - conditional() + // ============================================ + + @Test + @DisplayName("conditional() should create variable with boolean true") + void conditionalVariableWithBooleanTrue() { + TemplateVariable variable = TemplateVariable.conditional("is_premium", true); + + assertEquals("{is_premium}", variable.getPlaceholder()); + assertEquals("is_premium", variable.getName()); + assertEquals(true, variable.getValue()); + assertTrue(variable.getUsesAdvancedTemplatingEngine()); + } + + @Test + @DisplayName("conditional() should create variable with boolean false") + void conditionalVariableWithBooleanFalse() { + TemplateVariable variable = TemplateVariable.conditional("show_discount", false); + + assertEquals(false, variable.getValue()); + assertTrue(variable.getUsesAdvancedTemplatingEngine()); + } + + @Test + @DisplayName("conditional() should create variable with truthy value") + void conditionalVariableWithTruthyValue() { + TemplateVariable variable = TemplateVariable.conditional("count", 5); + + assertEquals(5, variable.getValue()); + } + + @Test + @DisplayName("conditional() should use custom placeholder when provided") + void conditionalVariableWithCustomPlaceholder() { + TemplateVariable variable = TemplateVariable.conditional("is_active", true, "{active_flag}"); + + assertEquals("{active_flag}", variable.getPlaceholder()); + assertEquals("is_active", variable.getName()); + } + + // ============================================ + // Helper Function Tests - image() + // ============================================ + + @Test + @DisplayName("image() should create variable with URL") + void imageVariableWithUrl() { + TemplateVariable variable = TemplateVariable.image("logo", "https://example.com/logo.png"); + + assertEquals("{logo}", variable.getPlaceholder()); + assertEquals("logo", variable.getName()); + assertEquals("https://example.com/logo.png", variable.getValue()); + assertEquals("image", variable.getMimeType()); + } + + @Test + @DisplayName("image() should create variable with base64") + void imageVariableWithBase64() { + String base64Image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."; + TemplateVariable variable = TemplateVariable.image("signature", base64Image); + + assertEquals(base64Image, variable.getValue()); + assertEquals("image", variable.getMimeType()); + } + + @Test + @DisplayName("image() should use custom placeholder when provided") + void imageVariableWithCustomPlaceholder() { + TemplateVariable variable = TemplateVariable.image("logo", "https://example.com/logo.png", "{company_logo}"); + + assertEquals("{company_logo}", variable.getPlaceholder()); + assertEquals("logo", variable.getName()); + } + + // ============================================ + // Builder Validation Tests + // ============================================ + + @Test + @DisplayName("builder should throw error when placeholder is missing") + void builderThrowsErrorWhenPlaceholderMissing() { + assertThrows(IllegalStateException.class, () -> { + TemplateVariable.builder() + .name("test") + .value("value") + .build(); + }); + } + + @Test + @DisplayName("builder should throw error when name is missing") + void builderThrowsErrorWhenNameMissing() { + assertThrows(IllegalStateException.class, () -> { + TemplateVariable.builder() + .placeholder("{test}") + .value("value") + .build(); + }); + } + + @Test + @DisplayName("builder should throw error when value and text are both missing") + void builderThrowsErrorWhenValueAndTextMissing() { + assertThrows(IllegalStateException.class, () -> { + TemplateVariable.builder() + .placeholder("{test}") + .name("test") + .build(); + }); + } + + @Test + @DisplayName("builder should accept text as alternative to value") + void builderAcceptsTextAsAlternative() { + TemplateVariable variable = TemplateVariable.builder() + .placeholder("{test}") + .name("test") + .text("text value") + .build(); + + assertEquals("text value", variable.getText()); + } + + // ============================================ + // Generate Tests + // ============================================ + + @Test + @DisplayName("should generate document with simple variables") + void generateDocumentWithSimpleVariables() throws Exception { + Map responseData = new HashMap<>(); + responseData.put("success", true); + responseData.put("deliverableId", "doc-123"); + responseData.put("message", "Document generated successfully"); + + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(responseData))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Test Document") + .description("Test description") + .variables(Arrays.asList( + TemplateVariable.simple("customer_name", "Person A"), + TemplateVariable.simple("order_total", 1500) + )) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + assertEquals("doc-123", result.getDeliverableId()); + + RecordedRequest recorded = server.takeRequest(); + assertEquals("POST", recorded.getMethod()); + assertEquals("/v1/deliverable", recorded.getPath()); + assertEquals("Bearer test-api-key", recorded.getHeader("Authorization")); + assertEquals("test-org-id", recorded.getHeader("x-rapiddocx-org-id")); + } + + @Test + @DisplayName("should generate document with nested object variables") + void generateDocumentWithNestedObjectVariables() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "deliverableId", "doc-456" + )))); + + Map user = new HashMap<>(); + user.put("firstName", "Foo"); + user.put("lastName", "Bar"); + user.put("profile", Map.of("company", "Company ABC")); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Nested Document") + .description("Document with nested objects") + .variables(Collections.singletonList( + TemplateVariable.nested("user", user) + )) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + } + + @Test + @DisplayName("should generate document with loop/array variables") + void generateDocumentWithLoopArrayVariables() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "deliverableId", "doc-789" + )))); + + List> items = Arrays.asList( + Map.of("name", "Item A", "quantity", 5, "price", 100), + Map.of("name", "Item B", "quantity", 3, "price", 200) + ); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Loop Document") + .description("Document with loops") + .variables(Collections.singletonList( + TemplateVariable.loop("items", items) + )) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + } + + @Test + @DisplayName("should generate document with helper-created variables") + void generateDocumentWithHelperCreatedVariables() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "deliverableId", "doc-helper" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Helper Document") + .description("Document using helper functions") + .variables(Arrays.asList( + TemplateVariable.simple("title", "Quarterly Report"), + TemplateVariable.nested("company", Map.of("name", "Company XYZ", "employees", 500)), + TemplateVariable.loop("departments", Arrays.asList(Map.of("name", "Dept A"), Map.of("name", "Dept B"))), + TemplateVariable.conditional("show_financials", true), + TemplateVariable.image("logo", "https://example.com/logo.png") + )) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + } + + @Test + @DisplayName("should include optional request parameters") + void generateIncludesOptionalRequestParameters() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "deliverableId", "doc-options" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Options Document") + .description("Document with all options") + .variables(Collections.singletonList( + TemplateVariable.simple("test", "value") + )) + .replaceFonts(true) + .defaultFont("Arial") + .outputFormat("pdf") + .metadata(Map.of("customField", "value")) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + } + + @Test + @DisplayName("should throw error when request is null") + void generateThrowsErrorWhenRequestIsNull() { + assertThrows(IllegalArgumentException.class, () -> { + client.turboTemplate().generate(null); + }); + } + + // ============================================ + // Placeholder and Name Handling Tests + // ============================================ + + @Test + @DisplayName("should require both placeholder and name in generated request") + void requireBothPlaceholderAndName() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "deliverableId", "doc-both" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Both Fields Document") + .description("Document with both placeholder and name") + .variables(Collections.singletonList( + TemplateVariable.builder() + .placeholder("{customer}") + .name("customer") + .value("Person A") + .build() + )) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + } + + @Test + @DisplayName("should allow distinct placeholder and name values") + void allowDistinctPlaceholderAndName() throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "success", true, + "deliverableId", "doc-distinct" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Distinct Fields Document") + .description("Document with distinct placeholder and name") + .variables(Collections.singletonList( + TemplateVariable.builder() + .placeholder("{cust_name}") + .name("customerFullName") + .value("Person A") + .build() + )) + .build(); + + GenerateTemplateResponse result = client.turboTemplate().generate(request); + + assertTrue(result.isSuccess()); + } + + // ============================================ + // Error Handling Tests + // ============================================ + + @Test + @DisplayName("should handle API errors gracefully") + void handleApiErrors() { + server.enqueue(new MockResponse() + .setResponseCode(404) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "message", "Template not found", + "code", "TEMPLATE_NOT_FOUND" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("invalid-template") + .name("Error Document") + .description("Document that should fail") + .variables(Collections.singletonList( + TemplateVariable.simple("test", "value") + )) + .build(); + + TurboDocxException.NotFoundException exception = assertThrows(TurboDocxException.NotFoundException.class, () -> { + client.turboTemplate().generate(request); + }); + + assertEquals(404, exception.getStatusCode()); + assertEquals("Template not found", exception.getMessage()); + } + + @Test + @DisplayName("should handle validation errors") + void handleValidationErrors() { + server.enqueue(new MockResponse() + .setResponseCode(400) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "message", "Validation failed: Invalid variable configuration", + "code", "VALIDATION_ERROR" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Validation Error Document") + .description("Document that should fail validation") + .variables(Collections.singletonList( + TemplateVariable.simple("test", "value") + )) + .build(); + + TurboDocxException.ValidationException exception = assertThrows(TurboDocxException.ValidationException.class, () -> { + client.turboTemplate().generate(request); + }); + + assertEquals(400, exception.getStatusCode()); + } + + @Test + @DisplayName("should handle rate limit errors") + void handleRateLimitErrors() { + server.enqueue(new MockResponse() + .setResponseCode(429) + .setHeader("Content-Type", "application/json") + .setBody(gson.toJson(Map.of( + "message", "Rate limit exceeded", + "code", "RATE_LIMIT_EXCEEDED" + )))); + + GenerateTemplateRequest request = GenerateTemplateRequest.builder() + .templateId("template-123") + .name("Rate Limit Document") + .description("Document that should hit rate limit") + .variables(Collections.singletonList( + TemplateVariable.simple("test", "value") + )) + .build(); + + TurboDocxException.RateLimitException exception = assertThrows(TurboDocxException.RateLimitException.class, () -> { + client.turboTemplate().generate(request); + }); + + assertEquals(429, exception.getStatusCode()); + } +} diff --git a/packages/js-sdk/tests/turbotemplate.test.ts b/packages/js-sdk/tests/turbotemplate.test.ts new file mode 100644 index 0000000..2695ecd --- /dev/null +++ b/packages/js-sdk/tests/turbotemplate.test.ts @@ -0,0 +1,710 @@ +/** + * TurboTemplate Module Tests + * + * Tests for advanced templating features: + * - Helper functions (createSimpleVariable, createNestedVariable, etc.) + * - Variable validation + * - Generate template functionality + * - Placeholder and name handling + */ + +import { TurboTemplate } from '../src/modules/template'; +import { HttpClient } from '../src/http'; + +// Mock the HttpClient +jest.mock('../src/http'); + +const MockedHttpClient = HttpClient as jest.MockedClass; + +describe('TurboTemplate Module', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset static client + (TurboTemplate as any).client = undefined; + + // Mock getSenderConfig to return default values + MockedHttpClient.prototype.getSenderConfig = jest.fn().mockReturnValue({ + senderEmail: 'test@company.com', + senderName: 'Test Company', + }); + }); + + describe('configure', () => { + it('should configure the client with API key', () => { + TurboTemplate.configure({ + apiKey: 'test-api-key', + orgId: 'test-org-id', + senderEmail: 'test@company.com', + }); + expect(MockedHttpClient).toHaveBeenCalledWith({ + apiKey: 'test-api-key', + orgId: 'test-org-id', + senderEmail: 'test@company.com', + }); + }); + + it('should configure with custom base URL', () => { + TurboTemplate.configure({ + apiKey: 'test-api-key', + orgId: 'test-org-id', + senderEmail: 'test@company.com', + baseUrl: 'https://custom-api.example.com', + }); + expect(MockedHttpClient).toHaveBeenCalledWith({ + apiKey: 'test-api-key', + orgId: 'test-org-id', + senderEmail: 'test@company.com', + baseUrl: 'https://custom-api.example.com', + }); + }); + }); + + describe('Helper Functions', () => { + describe('createSimpleVariable', () => { + it('should create a simple variable with name and value', () => { + const variable = TurboTemplate.createSimpleVariable('customer_name', 'Person A'); + + expect(variable).toEqual({ + placeholder: '{customer_name}', + name: 'customer_name', + value: 'Person A', + }); + }); + + it('should create a simple variable with number value', () => { + const variable = TurboTemplate.createSimpleVariable('order_total', 1500); + + expect(variable).toEqual({ + placeholder: '{order_total}', + name: 'order_total', + value: 1500, + }); + }); + + it('should create a simple variable with boolean value', () => { + const variable = TurboTemplate.createSimpleVariable('is_active', true); + + expect(variable).toEqual({ + placeholder: '{is_active}', + name: 'is_active', + value: true, + }); + }); + + it('should use custom placeholder when provided', () => { + const variable = TurboTemplate.createSimpleVariable('customer_name', 'Person A', '{custom_placeholder}'); + + expect(variable).toEqual({ + placeholder: '{custom_placeholder}', + name: 'customer_name', + value: 'Person A', + }); + }); + + it('should handle name that already has curly braces', () => { + const variable = TurboTemplate.createSimpleVariable('{customer_name}', 'Person A'); + + expect(variable).toEqual({ + placeholder: '{customer_name}', + name: '{customer_name}', + value: 'Person A', + }); + }); + }); + + describe('createNestedVariable', () => { + it('should create a nested variable with object value', () => { + const variable = TurboTemplate.createNestedVariable('user', { + firstName: 'Foo', + lastName: 'Bar', + email: 'foo@example.com', + }); + + expect(variable).toEqual({ + placeholder: '{user}', + name: 'user', + value: { + firstName: 'Foo', + lastName: 'Bar', + email: 'foo@example.com', + }, + mimeType: 'json', + usesAdvancedTemplatingEngine: true, + }); + }); + + it('should create a nested variable with deeply nested object', () => { + const variable = TurboTemplate.createNestedVariable('company', { + name: 'Company ABC', + address: { + street: '123 Test Street', + city: 'Test City', + state: 'TS', + }, + }); + + expect(variable.value).toEqual({ + name: 'Company ABC', + address: { + street: '123 Test Street', + city: 'Test City', + state: 'TS', + }, + }); + expect(variable.mimeType).toBe('json'); + expect(variable.usesAdvancedTemplatingEngine).toBe(true); + }); + + it('should use custom placeholder when provided', () => { + const variable = TurboTemplate.createNestedVariable('user', { name: 'Test' }, '{custom_user}'); + + expect(variable.placeholder).toBe('{custom_user}'); + expect(variable.name).toBe('user'); + }); + }); + + describe('createLoopVariable', () => { + it('should create a loop variable with array value', () => { + const variable = TurboTemplate.createLoopVariable('items', [ + { name: 'Item A', price: 100 }, + { name: 'Item B', price: 200 }, + ]); + + expect(variable).toEqual({ + placeholder: '{items}', + name: 'items', + value: [ + { name: 'Item A', price: 100 }, + { name: 'Item B', price: 200 }, + ], + mimeType: 'json', + usesAdvancedTemplatingEngine: true, + }); + }); + + it('should create a loop variable with empty array', () => { + const variable = TurboTemplate.createLoopVariable('products', []); + + expect(variable.value).toEqual([]); + expect(variable.mimeType).toBe('json'); + }); + + it('should create a loop variable with primitive array', () => { + const variable = TurboTemplate.createLoopVariable('tags', ['tag1', 'tag2', 'tag3']); + + expect(variable.value).toEqual(['tag1', 'tag2', 'tag3']); + }); + + it('should use custom placeholder when provided', () => { + const variable = TurboTemplate.createLoopVariable('items', [], '{line_items}'); + + expect(variable.placeholder).toBe('{line_items}'); + expect(variable.name).toBe('items'); + }); + }); + + describe('createConditionalVariable', () => { + it('should create a conditional variable with boolean true', () => { + const variable = TurboTemplate.createConditionalVariable('is_premium', true); + + expect(variable).toEqual({ + placeholder: '{is_premium}', + name: 'is_premium', + value: true, + usesAdvancedTemplatingEngine: true, + }); + }); + + it('should create a conditional variable with boolean false', () => { + const variable = TurboTemplate.createConditionalVariable('show_discount', false); + + expect(variable.value).toBe(false); + expect(variable.usesAdvancedTemplatingEngine).toBe(true); + }); + + it('should create a conditional variable with truthy value', () => { + const variable = TurboTemplate.createConditionalVariable('count', 5); + + expect(variable.value).toBe(5); + }); + + it('should use custom placeholder when provided', () => { + const variable = TurboTemplate.createConditionalVariable('is_active', true, '{active_flag}'); + + expect(variable.placeholder).toBe('{active_flag}'); + expect(variable.name).toBe('is_active'); + }); + }); + + describe('createImageVariable', () => { + it('should create an image variable with URL', () => { + const variable = TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png'); + + expect(variable).toEqual({ + placeholder: '{logo}', + name: 'logo', + value: 'https://example.com/logo.png', + mimeType: 'image', + }); + }); + + it('should create an image variable with base64', () => { + const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...'; + const variable = TurboTemplate.createImageVariable('signature', base64Image); + + expect(variable.value).toBe(base64Image); + expect(variable.mimeType).toBe('image'); + }); + + it('should use custom placeholder when provided', () => { + const variable = TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png', '{company_logo}'); + + expect(variable.placeholder).toBe('{company_logo}'); + expect(variable.name).toBe('logo'); + }); + }); + }); + + describe('validateVariable', () => { + it('should validate a correct simple variable', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{name}', + name: 'name', + value: 'Test', + }); + + expect(result.isValid).toBe(true); + expect(result.errors).toBeUndefined(); + }); + + it('should return error when placeholder and name are both missing', () => { + const result = TurboTemplate.validateVariable({ + value: 'Test', + } as any); + + expect(result.isValid).toBe(false); + expect(result.errors).toContain('Variable must have either "placeholder" or "name" property'); + }); + + it('should return error when value and text are both missing', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{name}', + name: 'name', + } as any); + + expect(result.isValid).toBe(false); + expect(result.errors).toContain('Variable must have either "value" or "text" property'); + }); + + it('should warn about array without json mimeType', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{items}', + name: 'items', + value: [1, 2, 3], + }); + + expect(result.isValid).toBe(true); + expect(result.warnings).toContain('Array values should use mimeType: "json"'); + }); + + it('should not warn about array with json mimeType', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{items}', + name: 'items', + value: [1, 2, 3], + mimeType: 'json', + }); + + expect(result.isValid).toBe(true); + expect(result.warnings).toBeUndefined(); + }); + + it('should validate image variable with string value', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{logo}', + name: 'logo', + value: 'https://example.com/logo.png', + mimeType: 'image', + }); + + expect(result.isValid).toBe(true); + }); + + it('should return error for image variable with non-string value', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{logo}', + name: 'logo', + value: 123, + mimeType: 'image', + }); + + expect(result.isValid).toBe(false); + expect(result.errors).toContain('Image variables must have a string value (URL or base64)'); + }); + + it('should warn about object without explicit mimeType', () => { + const result = TurboTemplate.validateVariable({ + placeholder: '{user}', + name: 'user', + value: { name: 'Test' }, + }); + + expect(result.isValid).toBe(true); + expect(result.warnings).toContain('Complex objects should explicitly set mimeType to "json"'); + }); + }); + + describe('generate', () => { + it('should generate document with simple variables', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-123', + message: 'Document generated successfully', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Test Document', + description: 'Test description', + variables: [ + { placeholder: '{customer_name}', name: 'customer_name', value: 'Person A' }, + { placeholder: '{order_total}', name: 'order_total', value: 1500 }, + ], + }); + + expect(result.success).toBe(true); + expect(result.deliverableId).toBe('doc-123'); + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + templateId: 'template-123', + name: 'Test Document', + description: 'Test description', + variables: expect.arrayContaining([ + expect.objectContaining({ + placeholder: '{customer_name}', + name: 'customer_name', + value: 'Person A', + mimeType: 'text', + }), + expect.objectContaining({ + placeholder: '{order_total}', + name: 'order_total', + value: 1500, + mimeType: 'text', + }), + ]), + }) + ); + }); + + it('should generate document with nested object variables', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-456', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Nested Document', + description: 'Document with nested objects', + variables: [ + { + placeholder: '{user}', + name: 'user', + mimeType: 'json', + value: { + firstName: 'Foo', + lastName: 'Bar', + profile: { + company: 'Company ABC', + }, + }, + usesAdvancedTemplatingEngine: true, + }, + ], + }); + + expect(result.success).toBe(true); + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + variables: expect.arrayContaining([ + expect.objectContaining({ + placeholder: '{user}', + name: 'user', + mimeType: 'json', + usesAdvancedTemplatingEngine: true, + value: { + firstName: 'Foo', + lastName: 'Bar', + profile: { + company: 'Company ABC', + }, + }, + }), + ]), + }) + ); + }); + + it('should generate document with loop/array variables', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-789', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Loop Document', + description: 'Document with loops', + variables: [ + { + placeholder: '{items}', + name: 'items', + mimeType: 'json', + value: [ + { name: 'Item A', quantity: 5, price: 100 }, + { name: 'Item B', quantity: 3, price: 200 }, + ], + usesAdvancedTemplatingEngine: true, + }, + ], + }); + + expect(result.success).toBe(true); + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + variables: expect.arrayContaining([ + expect.objectContaining({ + placeholder: '{items}', + name: 'items', + mimeType: 'json', + }), + ]), + }) + ); + }); + + it('should generate document with helper-created variables', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-helper', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Helper Document', + description: 'Document using helper functions', + variables: [ + TurboTemplate.createSimpleVariable('title', 'Quarterly Report'), + TurboTemplate.createNestedVariable('company', { name: 'Company XYZ', employees: 500 }), + TurboTemplate.createLoopVariable('departments', [{ name: 'Dept A' }, { name: 'Dept B' }]), + TurboTemplate.createConditionalVariable('show_financials', true), + TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png'), + ], + }); + + expect(result.success).toBe(true); + expect(MockedHttpClient.prototype.post).toHaveBeenCalled(); + }); + + it('should include optional request parameters', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-options', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Options Document', + description: 'Document with all options', + variables: [{ placeholder: '{test}', name: 'test', value: 'value' }], + replaceFonts: true, + defaultFont: 'Arial', + outputFormat: 'pdf', + metadata: { customField: 'value' }, + }); + + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + replaceFonts: true, + defaultFont: 'Arial', + outputFormat: 'pdf', + metadata: { customField: 'value' }, + }) + ); + }); + + it('should throw error when variable has no value or text', async () => { + MockedHttpClient.prototype.post = jest.fn(); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await expect( + TurboTemplate.generate({ + templateId: 'template-123', + name: 'Error Document', + description: 'Document that should fail', + variables: [{ placeholder: '{test}', name: 'test' } as any], + }) + ).rejects.toThrow('Variable "{test}" must have either \'value\' or \'text\' property'); + }); + + it('should handle text property as fallback', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-text', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Text Document', + description: 'Document using text property', + variables: [{ placeholder: '{legacy}', name: 'legacy', text: 'Legacy value' }], + }); + + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + variables: expect.arrayContaining([ + expect.objectContaining({ + placeholder: '{legacy}', + name: 'legacy', + text: 'Legacy value', + }), + ]), + }) + ); + }); + }); + + describe('Placeholder and Name Handling', () => { + it('should require both placeholder and name in generated request', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-both', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Both Fields Document', + description: 'Document with both placeholder and name', + variables: [ + { placeholder: '{customer}', name: 'customer', value: 'Person A' }, + ], + }); + + const callArgs = (MockedHttpClient.prototype.post as jest.Mock).mock.calls[0][1]; + expect(callArgs.variables[0].placeholder).toBe('{customer}'); + expect(callArgs.variables[0].name).toBe('customer'); + }); + + it('should allow distinct placeholder and name values', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-distinct', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Distinct Fields Document', + description: 'Document with distinct placeholder and name', + variables: [ + { placeholder: '{cust_name}', name: 'customerFullName', value: 'Person A' }, + ], + }); + + const callArgs = (MockedHttpClient.prototype.post as jest.Mock).mock.calls[0][1]; + expect(callArgs.variables[0].placeholder).toBe('{cust_name}'); + expect(callArgs.variables[0].name).toBe('customerFullName'); + }); + }); + + describe('Error Handling', () => { + it('should handle API errors gracefully', async () => { + const apiError = { + statusCode: 404, + message: 'Template not found', + code: 'TEMPLATE_NOT_FOUND', + }; + + MockedHttpClient.prototype.post = jest.fn().mockRejectedValue(apiError); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await expect( + TurboTemplate.generate({ + templateId: 'invalid-template', + name: 'Error Document', + description: 'Document that should fail', + variables: [{ placeholder: '{test}', name: 'test', value: 'value' }], + }) + ).rejects.toEqual(apiError); + }); + + it('should handle validation errors', async () => { + const validationError = { + statusCode: 400, + message: 'Validation failed', + errors: [{ path: ['variables', 0, 'value'], message: 'Value is required' }], + }; + + MockedHttpClient.prototype.post = jest.fn().mockRejectedValue(validationError); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await expect( + TurboTemplate.generate({ + templateId: 'template-123', + name: 'Validation Error Document', + description: 'Document that should fail validation', + variables: [{ placeholder: '{test}', name: 'test', value: '' }], + }) + ).rejects.toEqual(validationError); + }); + + it('should handle rate limit errors', async () => { + const rateLimitError = { + statusCode: 429, + message: 'Rate limit exceeded', + code: 'RATE_LIMIT_EXCEEDED', + }; + + MockedHttpClient.prototype.post = jest.fn().mockRejectedValue(rateLimitError); + TurboTemplate.configure({ apiKey: 'test-key' }); + + await expect( + TurboTemplate.generate({ + templateId: 'template-123', + name: 'Rate Limit Document', + description: 'Document that should hit rate limit', + variables: [{ placeholder: '{test}', name: 'test', value: 'value' }], + }) + ).rejects.toEqual(rateLimitError); + }); + }); +}); diff --git a/packages/py-sdk/tests/test_turbotemplate.py b/packages/py-sdk/tests/test_turbotemplate.py new file mode 100644 index 0000000..97207c2 --- /dev/null +++ b/packages/py-sdk/tests/test_turbotemplate.py @@ -0,0 +1,679 @@ +""" +TurboTemplate Module Tests + +Tests for advanced templating features: +- Helper functions (create_simple_variable, create_nested_variable, etc.) +- Variable validation +- Generate template functionality +- Placeholder and name handling +""" + +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from turbodocx_sdk import TurboTemplate + + +class TestTurboTemplateConfigure: + """Test TurboTemplate configuration""" + + @pytest.fixture(autouse=True) + def setup(self): + """Reset client before each test""" + TurboTemplate._client = None + + def test_configure_with_api_key_and_org_id(self): + """Should configure the client with API key and org ID""" + TurboTemplate.configure(api_key="test-api-key", org_id="test-org-id") + assert TurboTemplate._client is not None + assert TurboTemplate._client.api_key == "test-api-key" + assert TurboTemplate._client.org_id == "test-org-id" + + def test_configure_with_custom_base_url(self): + """Should configure with custom base URL""" + TurboTemplate.configure( + api_key="test-api-key", + org_id="test-org-id", + base_url="https://custom-api.example.com", + ) + assert TurboTemplate._client.base_url == "https://custom-api.example.com" + + +class TestHelperFunctions: + """Test helper functions for creating variables""" + + class TestCreateSimpleVariable: + """Test create_simple_variable helper""" + + def test_create_simple_variable_with_string_value(self): + """Should create a simple variable with string value""" + variable = TurboTemplate.create_simple_variable("customer_name", "Person A") + + assert variable == { + "placeholder": "{customer_name}", + "name": "customer_name", + "value": "Person A", + } + + def test_create_simple_variable_with_number_value(self): + """Should create a simple variable with number value""" + variable = TurboTemplate.create_simple_variable("order_total", 1500) + + assert variable == { + "placeholder": "{order_total}", + "name": "order_total", + "value": 1500, + } + + def test_create_simple_variable_with_boolean_value(self): + """Should create a simple variable with boolean value""" + variable = TurboTemplate.create_simple_variable("is_active", True) + + assert variable == { + "placeholder": "{is_active}", + "name": "is_active", + "value": True, + } + + def test_create_simple_variable_with_custom_placeholder(self): + """Should use custom placeholder when provided""" + variable = TurboTemplate.create_simple_variable( + "customer_name", "Person A", "{custom_placeholder}" + ) + + assert variable == { + "placeholder": "{custom_placeholder}", + "name": "customer_name", + "value": "Person A", + } + + def test_create_simple_variable_with_curly_braces_in_name(self): + """Should handle name that already has curly braces""" + variable = TurboTemplate.create_simple_variable("{customer_name}", "Person A") + + assert variable == { + "placeholder": "{customer_name}", + "name": "{customer_name}", + "value": "Person A", + } + + class TestCreateNestedVariable: + """Test create_nested_variable helper""" + + def test_create_nested_variable_with_object_value(self): + """Should create a nested variable with object value""" + variable = TurboTemplate.create_nested_variable( + "user", + { + "firstName": "Foo", + "lastName": "Bar", + "email": "foo@example.com", + }, + ) + + assert variable["placeholder"] == "{user}" + assert variable["name"] == "user" + assert variable["value"] == { + "firstName": "Foo", + "lastName": "Bar", + "email": "foo@example.com", + } + assert variable["mimeType"] == "json" + assert variable["usesAdvancedTemplatingEngine"] is True + + def test_create_nested_variable_with_deeply_nested_object(self): + """Should create a nested variable with deeply nested object""" + variable = TurboTemplate.create_nested_variable( + "company", + { + "name": "Company ABC", + "address": { + "street": "123 Test Street", + "city": "Test City", + "state": "TS", + }, + }, + ) + + assert variable["value"] == { + "name": "Company ABC", + "address": { + "street": "123 Test Street", + "city": "Test City", + "state": "TS", + }, + } + assert variable["mimeType"] == "json" + assert variable["usesAdvancedTemplatingEngine"] is True + + def test_create_nested_variable_with_custom_placeholder(self): + """Should use custom placeholder when provided""" + variable = TurboTemplate.create_nested_variable( + "user", {"name": "Test"}, "{custom_user}" + ) + + assert variable["placeholder"] == "{custom_user}" + assert variable["name"] == "user" + + class TestCreateLoopVariable: + """Test create_loop_variable helper""" + + def test_create_loop_variable_with_array_value(self): + """Should create a loop variable with array value""" + variable = TurboTemplate.create_loop_variable( + "items", + [ + {"name": "Item A", "price": 100}, + {"name": "Item B", "price": 200}, + ], + ) + + assert variable["placeholder"] == "{items}" + assert variable["name"] == "items" + assert variable["value"] == [ + {"name": "Item A", "price": 100}, + {"name": "Item B", "price": 200}, + ] + assert variable["mimeType"] == "json" + assert variable["usesAdvancedTemplatingEngine"] is True + + def test_create_loop_variable_with_empty_array(self): + """Should create a loop variable with empty array""" + variable = TurboTemplate.create_loop_variable("products", []) + + assert variable["value"] == [] + assert variable["mimeType"] == "json" + + def test_create_loop_variable_with_primitive_array(self): + """Should create a loop variable with primitive array""" + variable = TurboTemplate.create_loop_variable("tags", ["tag1", "tag2", "tag3"]) + + assert variable["value"] == ["tag1", "tag2", "tag3"] + + def test_create_loop_variable_with_custom_placeholder(self): + """Should use custom placeholder when provided""" + variable = TurboTemplate.create_loop_variable("items", [], "{line_items}") + + assert variable["placeholder"] == "{line_items}" + assert variable["name"] == "items" + + class TestCreateConditionalVariable: + """Test create_conditional_variable helper""" + + def test_create_conditional_variable_with_boolean_true(self): + """Should create a conditional variable with boolean true""" + variable = TurboTemplate.create_conditional_variable("is_premium", True) + + assert variable == { + "placeholder": "{is_premium}", + "name": "is_premium", + "value": True, + "usesAdvancedTemplatingEngine": True, + } + + def test_create_conditional_variable_with_boolean_false(self): + """Should create a conditional variable with boolean false""" + variable = TurboTemplate.create_conditional_variable("show_discount", False) + + assert variable["value"] is False + assert variable["usesAdvancedTemplatingEngine"] is True + + def test_create_conditional_variable_with_truthy_value(self): + """Should create a conditional variable with truthy value""" + variable = TurboTemplate.create_conditional_variable("count", 5) + + assert variable["value"] == 5 + + def test_create_conditional_variable_with_custom_placeholder(self): + """Should use custom placeholder when provided""" + variable = TurboTemplate.create_conditional_variable( + "is_active", True, "{active_flag}" + ) + + assert variable["placeholder"] == "{active_flag}" + assert variable["name"] == "is_active" + + class TestCreateImageVariable: + """Test create_image_variable helper""" + + def test_create_image_variable_with_url(self): + """Should create an image variable with URL""" + variable = TurboTemplate.create_image_variable( + "logo", "https://example.com/logo.png" + ) + + assert variable == { + "placeholder": "{logo}", + "name": "logo", + "value": "https://example.com/logo.png", + "mimeType": "image", + } + + def test_create_image_variable_with_base64(self): + """Should create an image variable with base64""" + base64_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." + variable = TurboTemplate.create_image_variable("signature", base64_image) + + assert variable["value"] == base64_image + assert variable["mimeType"] == "image" + + def test_create_image_variable_with_custom_placeholder(self): + """Should use custom placeholder when provided""" + variable = TurboTemplate.create_image_variable( + "logo", "https://example.com/logo.png", "{company_logo}" + ) + + assert variable["placeholder"] == "{company_logo}" + assert variable["name"] == "logo" + + +class TestValidateVariable: + """Test validate_variable function""" + + def test_validate_correct_simple_variable(self): + """Should validate a correct simple variable""" + result = TurboTemplate.validate_variable( + {"placeholder": "{name}", "name": "name", "value": "Test"} + ) + + assert result["isValid"] is True + assert result["errors"] is None + + def test_error_when_placeholder_and_name_missing(self): + """Should return error when placeholder and name are both missing""" + result = TurboTemplate.validate_variable({"value": "Test"}) + + assert result["isValid"] is False + assert 'Variable must have either "placeholder" or "name" property' in result["errors"] + + def test_error_when_value_and_text_missing(self): + """Should return error when value and text are both missing""" + result = TurboTemplate.validate_variable({"placeholder": "{name}", "name": "name"}) + + assert result["isValid"] is False + assert 'Variable must have either "value" or "text" property' in result["errors"] + + def test_warn_about_array_without_json_mimetype(self): + """Should warn about array without json mimeType""" + result = TurboTemplate.validate_variable( + {"placeholder": "{items}", "name": "items", "value": [1, 2, 3]} + ) + + assert result["isValid"] is True + assert 'Array values should use mimeType: "json"' in result["warnings"] + + def test_no_warn_about_array_with_json_mimetype(self): + """Should not warn about array with json mimeType""" + result = TurboTemplate.validate_variable( + {"placeholder": "{items}", "name": "items", "value": [1, 2, 3], "mimeType": "json"} + ) + + assert result["isValid"] is True + assert result["warnings"] is None + + def test_validate_image_variable_with_string_value(self): + """Should validate image variable with string value""" + result = TurboTemplate.validate_variable( + { + "placeholder": "{logo}", + "name": "logo", + "value": "https://example.com/logo.png", + "mimeType": "image", + } + ) + + assert result["isValid"] is True + + def test_error_for_image_variable_with_non_string_value(self): + """Should return error for image variable with non-string value""" + result = TurboTemplate.validate_variable( + {"placeholder": "{logo}", "name": "logo", "value": 123, "mimeType": "image"} + ) + + assert result["isValid"] is False + assert "Image variables must have a string value (URL or base64)" in result["errors"] + + def test_warn_about_object_without_explicit_mimetype(self): + """Should warn about object without explicit mimeType""" + result = TurboTemplate.validate_variable( + {"placeholder": "{user}", "name": "user", "value": {"name": "Test"}} + ) + + assert result["isValid"] is True + assert 'Complex objects should explicitly set mimeType to "json"' in result["warnings"] + + +class TestGenerate: + """Test generate function""" + + @pytest.fixture(autouse=True) + def setup(self): + """Reset client before each test""" + TurboTemplate._client = None + + @pytest.mark.asyncio + async def test_generate_document_with_simple_variables(self): + """Should generate document with simple variables""" + mock_response = { + "success": True, + "deliverableId": "doc-123", + "message": "Document generated successfully", + } + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + result = await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Test Document", + "description": "Test description", + "variables": [ + {"placeholder": "{customer_name}", "name": "customer_name", "value": "Person A"}, + {"placeholder": "{order_total}", "name": "order_total", "value": 1500}, + ], + } + ) + + assert result["success"] is True + assert result["deliverableId"] == "doc-123" + mock_client.post.assert_called_once() + call_args = mock_client.post.call_args + assert call_args[0][0] == "/v1/deliverable" + body = call_args[1]["json"] + assert body["templateId"] == "template-123" + assert body["name"] == "Test Document" + assert len(body["variables"]) == 2 + assert body["variables"][0]["placeholder"] == "{customer_name}" + assert body["variables"][0]["name"] == "customer_name" + assert body["variables"][0]["value"] == "Person A" + assert body["variables"][0]["mimeType"] == "text" + + @pytest.mark.asyncio + async def test_generate_document_with_nested_object_variables(self): + """Should generate document with nested object variables""" + mock_response = {"success": True, "deliverableId": "doc-456"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + result = await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Nested Document", + "description": "Document with nested objects", + "variables": [ + { + "placeholder": "{user}", + "name": "user", + "mimeType": "json", + "value": { + "firstName": "Foo", + "lastName": "Bar", + "profile": {"company": "Company ABC"}, + }, + "usesAdvancedTemplatingEngine": True, + } + ], + } + ) + + assert result["success"] is True + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["variables"][0]["mimeType"] == "json" + assert body["variables"][0]["usesAdvancedTemplatingEngine"] is True + + @pytest.mark.asyncio + async def test_generate_document_with_loop_array_variables(self): + """Should generate document with loop/array variables""" + mock_response = {"success": True, "deliverableId": "doc-789"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + result = await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Loop Document", + "description": "Document with loops", + "variables": [ + { + "placeholder": "{items}", + "name": "items", + "mimeType": "json", + "value": [ + {"name": "Item A", "quantity": 5, "price": 100}, + {"name": "Item B", "quantity": 3, "price": 200}, + ], + "usesAdvancedTemplatingEngine": True, + } + ], + } + ) + + assert result["success"] is True + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["variables"][0]["placeholder"] == "{items}" + assert body["variables"][0]["mimeType"] == "json" + + @pytest.mark.asyncio + async def test_generate_document_with_helper_created_variables(self): + """Should generate document with helper-created variables""" + mock_response = {"success": True, "deliverableId": "doc-helper"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + result = await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Helper Document", + "description": "Document using helper functions", + "variables": [ + TurboTemplate.create_simple_variable("title", "Quarterly Report"), + TurboTemplate.create_nested_variable( + "company", {"name": "Company XYZ", "employees": 500} + ), + TurboTemplate.create_loop_variable( + "departments", [{"name": "Dept A"}, {"name": "Dept B"}] + ), + TurboTemplate.create_conditional_variable("show_financials", True), + TurboTemplate.create_image_variable("logo", "https://example.com/logo.png"), + ], + } + ) + + assert result["success"] is True + mock_client.post.assert_called_once() + + @pytest.mark.asyncio + async def test_generate_includes_optional_request_parameters(self): + """Should include optional request parameters""" + mock_response = {"success": True, "deliverableId": "doc-options"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Options Document", + "description": "Document with all options", + "variables": [{"placeholder": "{test}", "name": "test", "value": "value"}], + "replaceFonts": True, + "defaultFont": "Arial", + "outputFormat": "pdf", + "metadata": {"customField": "value"}, + } + ) + + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["replaceFonts"] is True + assert body["defaultFont"] == "Arial" + assert body["outputFormat"] == "pdf" + assert body["metadata"] == {"customField": "value"} + + @pytest.mark.asyncio + async def test_generate_throws_error_when_variable_has_no_value_or_text(self): + """Should throw error when variable has no value or text""" + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + with pytest.raises(ValueError, match='must have either "value" or "text" property'): + await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Error Document", + "description": "Document that should fail", + "variables": [{"placeholder": "{test}", "name": "test"}], + } + ) + + @pytest.mark.asyncio + async def test_generate_handles_text_property_as_fallback(self): + """Should handle text property as fallback""" + mock_response = {"success": True, "deliverableId": "doc-text"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Text Document", + "description": "Document using text property", + "variables": [{"placeholder": "{legacy}", "name": "legacy", "text": "Legacy value"}], + } + ) + + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["variables"][0]["text"] == "Legacy value" + + +class TestPlaceholderAndNameHandling: + """Test placeholder and name handling""" + + @pytest.fixture(autouse=True) + def setup(self): + """Reset client before each test""" + TurboTemplate._client = None + + @pytest.mark.asyncio + async def test_require_both_placeholder_and_name_in_generated_request(self): + """Should require both placeholder and name in generated request""" + mock_response = {"success": True, "deliverableId": "doc-both"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Both Fields Document", + "description": "Document with both placeholder and name", + "variables": [ + {"placeholder": "{customer}", "name": "customer", "value": "Person A"} + ], + } + ) + + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["variables"][0]["placeholder"] == "{customer}" + assert body["variables"][0]["name"] == "customer" + + @pytest.mark.asyncio + async def test_allow_distinct_placeholder_and_name_values(self): + """Should allow distinct placeholder and name values""" + mock_response = {"success": True, "deliverableId": "doc-distinct"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Distinct Fields Document", + "description": "Document with distinct placeholder and name", + "variables": [ + {"placeholder": "{cust_name}", "name": "customerFullName", "value": "Person A"} + ], + } + ) + + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["variables"][0]["placeholder"] == "{cust_name}" + assert body["variables"][0]["name"] == "customerFullName" + + +class TestErrorHandling: + """Test error handling""" + + @pytest.fixture(autouse=True) + def setup(self): + """Reset client before each test""" + TurboTemplate._client = None + + @pytest.mark.asyncio + async def test_throw_error_when_not_configured(self): + """Should throw error when not configured""" + with pytest.raises(RuntimeError, match="not configured"): + await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Test", + "description": "Test", + "variables": [{"placeholder": "{test}", "name": "test", "value": "value"}], + } + ) + + @pytest.mark.asyncio + async def test_handle_api_errors_gracefully(self): + """Should handle API errors gracefully""" + api_error = Exception("Template not found") + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(side_effect=api_error) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org") + with pytest.raises(Exception, match="Template not found"): + await TurboTemplate.generate( + { + "templateId": "invalid-template", + "name": "Error Document", + "description": "Document that should fail", + "variables": [{"placeholder": "{test}", "name": "test", "value": "value"}], + } + ) From 41513b524ceef99b537a7a3b0134f469f989fc6f Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 22 Jan 2026 19:02:01 +0000 Subject: [PATCH 06/39] docs: add documentation for deliverable generation Signed-off-by: Kushal --- packages/go-sdk/README.md | 89 ++++++++++++++++++++++++++++++ packages/java-sdk/README.md | 93 ++++++++++++++++++++++++++++++++ packages/js-sdk/README.md | 102 +++++++++++++++++++++++++++++++++++ packages/py-sdk/README.md | 105 ++++++++++++++++++++++++++++++++++++ 4 files changed, 389 insertions(+) diff --git a/packages/go-sdk/README.md b/packages/go-sdk/README.md index 2342fc8..3dd8e9c 100644 --- a/packages/go-sdk/README.md +++ b/packages/go-sdk/README.md @@ -232,6 +232,95 @@ err := client.TurboSign.Resend(ctx, "doc-uuid-here", []string{"recipient-uuid-1" --- +### TurboTemplate + +Generate documents from templates with advanced variable substitution. + +#### `TurboTemplate.Generate` + +Generate a document from a template with variables. + +```go +result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "your-template-uuid", + Name: stringPtr("Generated Contract"), + Description: stringPtr("Contract for Q4 2024"), + Variables: []turbodocx.TemplateVariable{ + {Placeholder: "{customer_name}", Name: "customer_name", Value: "Acme Corp"}, + {Placeholder: "{contract_date}", Name: "contract_date", Value: "2024-01-15"}, + {Placeholder: "{total_amount}", Name: "total_amount", Value: 50000}, + }, +}) + +fmt.Printf("Document ID: %s\n", *result.DeliverableID) +``` + +#### Helper Functions + +Use helper functions for cleaner variable creation: + +```go +result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: "invoice-template-uuid", + Name: stringPtr("Invoice #1234"), + Description: stringPtr("Monthly invoice"), + Variables: []turbodocx.TemplateVariable{ + // Simple text/number variables + turbodocx.NewSimpleVariable("invoice_number", "INV-2024-001"), + turbodocx.NewSimpleVariable("total", 1500), + + // Nested objects (access with dot notation: {customer.name}, {customer.address.city}) + turbodocx.NewNestedVariable("customer", map[string]interface{}{ + "name": "Acme Corp", + "email": "billing@acme.com", + "address": map[string]interface{}{ + "street": "123 Main St", + "city": "New York", + "state": "NY", + }, + }), + + // Arrays for loops ({#items}...{/items}) + turbodocx.NewLoopVariable("items", []interface{}{ + map[string]interface{}{"name": "Widget A", "quantity": 5, "price": 100}, + map[string]interface{}{"name": "Widget B", "quantity": 3, "price": 200}, + }), + + // Conditionals ({#is_premium}...{/is_premium}) + turbodocx.NewConditionalVariable("is_premium", true), + + // Images + turbodocx.NewImageVariable("logo", "https://example.com/logo.png"), + }, +}) +``` + +#### Advanced Templating Features + +TurboTemplate supports Angular-like expressions: + +| Feature | Template Syntax | Example | +|:--------|:----------------|:--------| +| Simple substitution | `{variable}` | `{customer_name}` | +| Nested objects | `{object.property}` | `{user.address.city}` | +| Loops | `{#array}...{/array}` | `{#items}{name}: ${price}{/items}` | +| Conditionals | `{#condition}...{/condition}` | `{#is_premium}Premium Member{/is_premium}` | +| Expressions | `{expression}` | `{price * quantity}` | + +#### Variable Configuration + +| Field | Type | Required | Description | +|:------|:-----|:---------|:------------| +| `Placeholder` | string | Yes | The placeholder in template (e.g., `{name}`) | +| `Name` | string | Yes | Variable name for the templating engine | +| `Value` | interface{} | Yes* | The value to substitute | +| `MimeType` | VariableMimeType | Yes | `MimeTypeText`, `MimeTypeJSON`, `MimeTypeHTML`, `MimeTypeImage`, `MimeTypeMarkdown` | +| `UsesAdvancedTemplatingEngine` | *bool | No | Enable for loops, conditionals, expressions | + +*Either `Value` or `Text` must be provided. + +--- + ## Field Types | Type | Description | Required | Auto-filled | diff --git a/packages/java-sdk/README.md b/packages/java-sdk/README.md index 53b1831..ed406d7 100644 --- a/packages/java-sdk/README.md +++ b/packages/java-sdk/README.md @@ -265,6 +265,99 @@ client.turboSign().resendEmail("doc-uuid-here", Arrays.asList("recipient-uuid-1" --- +### TurboTemplate + +Generate documents from templates with advanced variable substitution. + +#### `turboTemplate().generate()` + +Generate a document from a template with variables. + +```java +GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("your-template-uuid") + .name("Generated Contract") + .description("Contract for Q4 2024") + .variables(Arrays.asList( + TemplateVariable.simple("customer_name", "Acme Corp"), + TemplateVariable.simple("contract_date", "2024-01-15"), + TemplateVariable.simple("total_amount", 50000) + )) + .build() +); + +System.out.println("Document ID: " + result.getDeliverableId()); +``` + +#### Helper Functions + +Use helper functions for cleaner variable creation: + +```java +GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId("invoice-template-uuid") + .name("Invoice #1234") + .description("Monthly invoice") + .variables(Arrays.asList( + // Simple text/number variables + TemplateVariable.simple("invoice_number", "INV-2024-001"), + TemplateVariable.simple("total", 1500), + + // Nested objects (access with dot notation: {customer.name}, {customer.address.city}) + TemplateVariable.nested("customer", Map.of( + "name", "Acme Corp", + "email", "billing@acme.com", + "address", Map.of( + "street", "123 Main St", + "city", "New York", + "state", "NY" + ) + )), + + // Arrays for loops ({#items}...{/items}) + TemplateVariable.loop("items", Arrays.asList( + Map.of("name", "Widget A", "quantity", 5, "price", 100), + Map.of("name", "Widget B", "quantity", 3, "price", 200) + )), + + // Conditionals ({#is_premium}...{/is_premium}) + TemplateVariable.conditional("is_premium", true), + + // Images + TemplateVariable.image("logo", "https://example.com/logo.png") + )) + .build() +); +``` + +#### Advanced Templating Features + +TurboTemplate supports Angular-like expressions: + +| Feature | Template Syntax | Example | +|:--------|:----------------|:--------| +| Simple substitution | `{variable}` | `{customer_name}` | +| Nested objects | `{object.property}` | `{user.address.city}` | +| Loops | `{#array}...{/array}` | `{#items}{name}: ${price}{/items}` | +| Conditionals | `{#condition}...{/condition}` | `{#is_premium}Premium Member{/is_premium}` | +| Expressions | `{expression}` | `{price * quantity}` | + +#### Variable Configuration + +| Property | Type | Required | Description | +|:---------|:-----|:---------|:------------| +| `placeholder` | String | Yes | The placeholder in template (e.g., `{name}`) | +| `name` | String | Yes | Variable name for the templating engine | +| `value` | Object | Yes* | The value to substitute | +| `mimeType` | VariableMimeType | Yes | `TEXT`, `JSON`, `HTML`, `IMAGE`, `MARKDOWN` | +| `usesAdvancedTemplatingEngine` | Boolean | No | Enable for loops, conditionals, expressions | + +*Either `value` or `text` must be provided. + +--- + ## Field Types | Type | Description | Required | Auto-filled | diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index eeec8a3..518e548 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -237,6 +237,108 @@ await TurboSign.resend('doc-uuid-here', ['recipient-uuid-1', 'recipient-uuid-2'] --- +### TurboTemplate + +Generate documents from templates with advanced variable substitution. + +#### `TurboTemplate.configure(options)` + +Configure the TurboTemplate module (same configuration as TurboSign). + +```typescript +import { TurboTemplate } from '@turbodocx/sdk'; + +TurboTemplate.configure({ + apiKey: process.env.TURBODOCX_API_KEY, + orgId: process.env.TURBODOCX_ORG_ID, +}); +``` + +#### `TurboTemplate.generate(options)` + +Generate a document from a template with variables. + +```typescript +const result = await TurboTemplate.generate({ + templateId: 'your-template-uuid', + name: 'Generated Contract', + description: 'Contract for Q4 2024', + variables: [ + { placeholder: '{customer_name}', name: 'customer_name', value: 'Acme Corp' }, + { placeholder: '{contract_date}', name: 'contract_date', value: '2024-01-15' }, + { placeholder: '{total_amount}', name: 'total_amount', value: 50000 }, + ], +}); + +console.log('Document ID:', result.deliverableId); +``` + +#### Helper Functions + +Use helper functions for cleaner variable creation: + +```typescript +const result = await TurboTemplate.generate({ + templateId: 'invoice-template-uuid', + name: 'Invoice #1234', + description: 'Monthly invoice', + variables: [ + // Simple text/number variables + TurboTemplate.createSimpleVariable('invoice_number', 'INV-2024-001'), + TurboTemplate.createSimpleVariable('total', 1500), + + // Nested objects (access with dot notation: {customer.name}, {customer.address.city}) + TurboTemplate.createNestedVariable('customer', { + name: 'Acme Corp', + email: 'billing@acme.com', + address: { + street: '123 Main St', + city: 'New York', + state: 'NY', + }, + }), + + // Arrays for loops ({#items}...{/items}) + TurboTemplate.createLoopVariable('items', [ + { name: 'Widget A', quantity: 5, price: 100 }, + { name: 'Widget B', quantity: 3, price: 200 }, + ]), + + // Conditionals ({#is_premium}...{/is_premium}) + TurboTemplate.createConditionalVariable('is_premium', true), + + // Images + TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png'), + ], +}); +``` + +#### Advanced Templating Features + +TurboTemplate supports Angular-like expressions: + +| Feature | Template Syntax | Example | +|:--------|:----------------|:--------| +| Simple substitution | `{variable}` | `{customer_name}` | +| Nested objects | `{object.property}` | `{user.address.city}` | +| Loops | `{#array}...{/array}` | `{#items}{name}: ${price}{/items}` | +| Conditionals | `{#condition}...{/condition}` | `{#is_premium}Premium Member{/is_premium}` | +| Expressions | `{expression}` | `{price * quantity}` | + +#### Variable Configuration + +| Property | Type | Required | Description | +|:---------|:-----|:---------|:------------| +| `placeholder` | string | Yes | The placeholder in template (e.g., `{name}`) | +| `name` | string | Yes | Variable name for the templating engine | +| `value` | any | Yes* | The value to substitute | +| `mimeType` | string | Yes | `text`, `json`, `html`, `image`, `markdown` | +| `usesAdvancedTemplatingEngine` | boolean | No | Enable for loops, conditionals, expressions | + +*Either `value` or `text` must be provided. + +--- + ## Field Types | Type | Description | Required | Auto-filled | diff --git a/packages/py-sdk/README.md b/packages/py-sdk/README.md index 986180d..704ca28 100644 --- a/packages/py-sdk/README.md +++ b/packages/py-sdk/README.md @@ -261,6 +261,111 @@ await TurboSign.resend_email("doc-uuid-here", recipient_ids=["recipient-uuid-1"] --- +### TurboTemplate + +Generate documents from templates with advanced variable substitution. + +#### `TurboTemplate.configure()` + +Configure the TurboTemplate module (same configuration as TurboSign). + +```python +from turbodocx_sdk import TurboTemplate +import os + +TurboTemplate.configure( + api_key=os.environ["TURBODOCX_API_KEY"], + org_id=os.environ["TURBODOCX_ORG_ID"], +) +``` + +#### `TurboTemplate.generate()` + +Generate a document from a template with variables. + +```python +result = await TurboTemplate.generate({ + "templateId": "your-template-uuid", + "name": "Generated Contract", + "description": "Contract for Q4 2024", + "variables": [ + {"placeholder": "{customer_name}", "name": "customer_name", "value": "Acme Corp"}, + {"placeholder": "{contract_date}", "name": "contract_date", "value": "2024-01-15"}, + {"placeholder": "{total_amount}", "name": "total_amount", "value": 50000}, + ], +}) + +print(f"Document ID: {result['deliverableId']}") +``` + +#### Helper Functions + +Use helper functions for cleaner variable creation: + +```python +from turbodocx_sdk import TurboTemplate + +result = await TurboTemplate.generate({ + "templateId": "invoice-template-uuid", + "name": "Invoice #1234", + "description": "Monthly invoice", + "variables": [ + # Simple text/number variables + TurboTemplate.create_simple_variable("invoice_number", "INV-2024-001"), + TurboTemplate.create_simple_variable("total", 1500), + + # Nested objects (access with dot notation: {customer.name}, {customer.address.city}) + TurboTemplate.create_nested_variable("customer", { + "name": "Acme Corp", + "email": "billing@acme.com", + "address": { + "street": "123 Main St", + "city": "New York", + "state": "NY", + }, + }), + + # Arrays for loops ({#items}...{/items}) + TurboTemplate.create_loop_variable("items", [ + {"name": "Widget A", "quantity": 5, "price": 100}, + {"name": "Widget B", "quantity": 3, "price": 200}, + ]), + + # Conditionals ({#is_premium}...{/is_premium}) + TurboTemplate.create_conditional_variable("is_premium", True), + + # Images + TurboTemplate.create_image_variable("logo", "https://example.com/logo.png"), + ], +}) +``` + +#### Advanced Templating Features + +TurboTemplate supports Angular-like expressions: + +| Feature | Template Syntax | Example | +|:--------|:----------------|:--------| +| Simple substitution | `{variable}` | `{customer_name}` | +| Nested objects | `{object.property}` | `{user.address.city}` | +| Loops | `{#array}...{/array}` | `{#items}{name}: ${price}{/items}` | +| Conditionals | `{#condition}...{/condition}` | `{#is_premium}Premium Member{/is_premium}` | +| Expressions | `{expression}` | `{price * quantity}` | + +#### Variable Configuration + +| Property | Type | Required | Description | +|:---------|:-----|:---------|:------------| +| `placeholder` | str | Yes | The placeholder in template (e.g., `{name}`) | +| `name` | str | Yes | Variable name for the templating engine | +| `value` | any | Yes* | The value to substitute | +| `mimeType` | str | Yes | `text`, `json`, `html`, `image`, `markdown` | +| `usesAdvancedTemplatingEngine` | bool | No | Enable for loops, conditionals, expressions | + +*Either `value` or `text` must be provided. + +--- + ## Field Types | Type | Description | Required | Auto-filled | From 3d427f4cfbb23e6deb908afd2b1eb21e1e7fd709 Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 22 Jan 2026 19:06:19 +0000 Subject: [PATCH 07/39] feat: make mimetype non optional Signed-off-by: Kushal --- packages/go-sdk/turbotemplate.go | 18 ++++++++++-------- .../com/turbodocx/models/TemplateVariable.java | 13 ++++++++++--- packages/js-sdk/src/modules/template.ts | 9 +++++++-- packages/js-sdk/src/types/template.ts | 6 ++++-- .../src/turbodocx_sdk/modules/template.py | 11 ++++++++--- .../py-sdk/src/turbodocx_sdk/types/template.py | 2 +- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index 1fd9c36..e8ceff3 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -37,8 +37,8 @@ type TemplateVariable struct { // Text is the text value (legacy, prefer using Value) Text *string `json:"text,omitempty"` - // MimeType is the MIME type of the variable - MimeType *VariableMimeType `json:"mimeType,omitempty"` + // MimeType is the MIME type of the variable (required) + MimeType VariableMimeType `json:"mimeType"` // UsesAdvancedTemplatingEngine enables advanced templating for this variable UsesAdvancedTemplatingEngine *bool `json:"usesAdvancedTemplatingEngine,omitempty"` @@ -189,6 +189,9 @@ func (c *TurboTemplateClient) Generate(ctx context.Context, req *GenerateTemplat if v.Name == "" { return nil, fmt.Errorf("variable %d must have Name", i) } + if v.MimeType == "" { + return nil, fmt.Errorf("variable %d (%s) must have MimeType", i, v.Placeholder) + } if v.Value == nil && (v.Text == nil || *v.Text == "") { return nil, fmt.Errorf("variable %d (%s) must have either Value or Text", i, v.Placeholder) } @@ -235,6 +238,7 @@ func NewSimpleVariable(name string, value interface{}, placeholder ...string) Te Placeholder: p, Name: name, Value: value, + MimeType: MimeTypeText, } } @@ -251,13 +255,12 @@ func NewNestedVariable(name string, value map[string]interface{}, placeholder .. } else { p = "{" + name + "}" } - mimeType := MimeTypeJSON usesAdvanced := true return TemplateVariable{ Placeholder: p, Name: name, Value: value, - MimeType: &mimeType, + MimeType: MimeTypeJSON, UsesAdvancedTemplatingEngine: &usesAdvanced, } } @@ -275,13 +278,12 @@ func NewLoopVariable(name string, value []interface{}, placeholder ...string) Te } else { p = "{" + name + "}" } - mimeType := MimeTypeJSON usesAdvanced := true return TemplateVariable{ Placeholder: p, Name: name, Value: value, - MimeType: &mimeType, + MimeType: MimeTypeJSON, UsesAdvancedTemplatingEngine: &usesAdvanced, } } @@ -304,6 +306,7 @@ func NewConditionalVariable(name string, value interface{}, placeholder ...strin Placeholder: p, Name: name, Value: value, + MimeType: MimeTypeJSON, UsesAdvancedTemplatingEngine: &usesAdvanced, } } @@ -321,11 +324,10 @@ func NewImageVariable(name string, imageURL string, placeholder ...string) Templ } else { p = "{" + name + "}" } - mimeType := MimeTypeImage return TemplateVariable{ Placeholder: p, Name: name, Value: imageURL, - MimeType: &mimeType, + MimeType: MimeTypeImage, } } diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java index b6b91c3..2920bd3 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java @@ -232,6 +232,9 @@ public TemplateVariable build() { if (variable.name == null || variable.name.isEmpty()) { throw new IllegalStateException("name must be set"); } + if (variable.mimeType == null || variable.mimeType.isEmpty()) { + throw new IllegalStateException("mimeType must be set"); + } if (variable.value == null && variable.text == null) { throw new IllegalStateException("Either value or text must be set"); } @@ -253,9 +256,12 @@ public static Builder builder() { */ public static TemplateVariable simple(String name, Object value, String... placeholder) { String p = getPlaceholder(name, placeholder); - TemplateVariable var = new TemplateVariable(p, value); - var.setName(name); - return var; + return builder() + .placeholder(p) + .name(name) + .value(value) + .mimeType(VariableMimeType.TEXT) + .build(); } /** @@ -304,6 +310,7 @@ public static TemplateVariable conditional(String name, Object value, String... .placeholder(p) .name(name) .value(value) + .mimeType(VariableMimeType.JSON) .usesAdvancedTemplatingEngine(true) .build(); } diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts index c31ee8b..24945a8 100644 --- a/packages/js-sdk/src/modules/template.ts +++ b/packages/js-sdk/src/modules/template.ts @@ -129,8 +129,11 @@ export class TurboTemplate { name: v.name, }; - // Add mimeType if specified (default to 'text' if not provided) - variable.mimeType = v.mimeType || 'text'; + // mimeType is required + if (!v.mimeType) { + throw new Error(`Variable "${variable.placeholder}" must have a 'mimeType' property`); + } + variable.mimeType = v.mimeType; // Handle value - keep objects/arrays as-is for JSON serialization if (v.value !== undefined && v.value !== null) { @@ -239,6 +242,7 @@ export class TurboTemplate { placeholder: p, name, value, + mimeType: 'text', }; } @@ -288,6 +292,7 @@ export class TurboTemplate { placeholder: p, name, value, + mimeType: 'json', usesAdvancedTemplatingEngine: true, }; } diff --git a/packages/js-sdk/src/types/template.ts b/packages/js-sdk/src/types/template.ts index f12e6e6..77ef193 100644 --- a/packages/js-sdk/src/types/template.ts +++ b/packages/js-sdk/src/types/template.ts @@ -35,8 +35,8 @@ export interface TemplateVariable { */ text?: string | number | boolean | object | any[] | null; - /** MIME type of the variable */ - mimeType?: VariableMimeType; + /** MIME type of the variable (required) */ + mimeType: VariableMimeType; /** * Enable advanced templating engine for this variable @@ -127,6 +127,7 @@ export interface SimpleVariable { placeholder: string; name: string; value: string | number | boolean; + mimeType: 'text'; } /** Variable with nested structure (e.g., user.name, user.email) */ @@ -152,6 +153,7 @@ export interface ConditionalVariable { placeholder: string; name: string; value: any; + mimeType: 'json'; usesAdvancedTemplatingEngine: true; } diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py index 737fb01..9b05a15 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -149,8 +149,12 @@ async def generate(cls, request: GenerateTemplateRequest) -> GenerateTemplateRes "name": v.get("name"), } - # Add mimeType (default to 'text' if not provided) - variable["mimeType"] = v.get("mimeType", "text") + # mimeType is required + if "mimeType" not in v or not v["mimeType"]: + raise ValueError( + f'Variable "{variable["placeholder"]}" must have a "mimeType" property' + ) + variable["mimeType"] = v["mimeType"] # Handle value - keep objects/arrays as-is for JSON serialization if "value" in v and v["value"] is not None: @@ -266,7 +270,7 @@ def create_simple_variable( TemplateVariable configured for simple substitution """ p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") - return {"placeholder": p, "name": name, "value": value} + return {"placeholder": p, "name": name, "value": value, "mimeType": VariableMimeType.TEXT} @staticmethod def create_nested_variable( @@ -336,6 +340,7 @@ def create_conditional_variable( "placeholder": p, "name": name, "value": value, + "mimeType": VariableMimeType.JSON, "usesAdvancedTemplatingEngine": True, } diff --git a/packages/py-sdk/src/turbodocx_sdk/types/template.py b/packages/py-sdk/src/turbodocx_sdk/types/template.py index 573199a..5952b92 100644 --- a/packages/py-sdk/src/turbodocx_sdk/types/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/types/template.py @@ -21,6 +21,7 @@ class _TemplateVariableRequired(TypedDict): placeholder: str name: str + mimeType: str # Required: 'text', 'json', 'html', 'image', 'markdown' class TemplateVariable(_TemplateVariableRequired, total=False): @@ -46,7 +47,6 @@ class TemplateVariable(_TemplateVariableRequired, total=False): value: Union[str, int, float, bool, Dict[str, Any], List[Any], None] text: Optional[str] - mimeType: Optional[VariableMimeType] usesAdvancedTemplatingEngine: Optional[bool] nestedInAdvancedTemplatingEngine: Optional[bool] allowRichTextInjection: Optional[bool] From ec2752ea3b5aea36b9281a93946f274f0d6fc734 Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 22 Jan 2026 19:13:56 +0000 Subject: [PATCH 08/39] fix: remove nested variable property Signed-off-by: Kushal --- packages/go-sdk/turbotemplate.go | 3 --- .../com/turbodocx/models/TemplateVariable.java | 16 ---------------- packages/js-sdk/src/modules/template.ts | 1 - packages/js-sdk/src/types/template.ts | 3 --- .../py-sdk/src/turbodocx_sdk/modules/template.py | 2 -- .../py-sdk/src/turbodocx_sdk/types/template.py | 2 -- 6 files changed, 27 deletions(-) diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index e8ceff3..6f3c621 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -55,9 +55,6 @@ type TemplateVariable struct { // DefaultValue indicates whether this is a default value DefaultValue *bool `json:"defaultValue,omitempty"` - // NestedVariables are nested variables for complex structures - NestedVariables []TemplateVariable `json:"nestedVariables,omitempty"` - // Subvariables are sub-variables (legacy structure) Subvariables []TemplateVariable `json:"subvariables,omitempty"` } diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java index 2920bd3..5c7ee53 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java @@ -40,9 +40,6 @@ public class TemplateVariable { @JsonProperty("defaultValue") private Boolean defaultValue; - @JsonProperty("nestedVariables") - private List nestedVariables; - @JsonProperty("subvariables") private List subvariables; @@ -140,14 +137,6 @@ public void setDefaultValue(Boolean defaultValue) { this.defaultValue = defaultValue; } - public List getNestedVariables() { - return nestedVariables; - } - - public void setNestedVariables(List nestedVariables) { - this.nestedVariables = nestedVariables; - } - public List getSubvariables() { return subvariables; } @@ -215,11 +204,6 @@ public Builder defaultValue(Boolean defaultValue) { return this; } - public Builder nestedVariables(List nestedVariables) { - variable.nestedVariables = nestedVariables; - return this; - } - public Builder subvariables(List subvariables) { variable.subvariables = subvariables; return this; diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts index 24945a8..d6a3331 100644 --- a/packages/js-sdk/src/modules/template.ts +++ b/packages/js-sdk/src/modules/template.ts @@ -158,7 +158,6 @@ export class TurboTemplate { // Add optional fields if (v.description) variable.description = v.description; if (v.defaultValue !== undefined) variable.defaultValue = v.defaultValue; - if (v.nestedVariables) variable.nestedVariables = v.nestedVariables; if (v.subvariables) variable.subvariables = v.subvariables; return variable; diff --git a/packages/js-sdk/src/types/template.ts b/packages/js-sdk/src/types/template.ts index 77ef193..3e8179a 100644 --- a/packages/js-sdk/src/types/template.ts +++ b/packages/js-sdk/src/types/template.ts @@ -59,9 +59,6 @@ export interface TemplateVariable { /** Whether this is a default value */ defaultValue?: boolean; - /** Nested variables for complex object structures */ - nestedVariables?: TemplateVariable[]; - /** Sub-variables (legacy structure) */ subvariables?: TemplateVariable[]; } diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py index 9b05a15..ef1e622 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -179,8 +179,6 @@ async def generate(cls, request: GenerateTemplateRequest) -> GenerateTemplateRes variable["description"] = v["description"] if "defaultValue" in v: variable["defaultValue"] = v["defaultValue"] - if "nestedVariables" in v: - variable["nestedVariables"] = v["nestedVariables"] if "subvariables" in v: variable["subvariables"] = v["subvariables"] diff --git a/packages/py-sdk/src/turbodocx_sdk/types/template.py b/packages/py-sdk/src/turbodocx_sdk/types/template.py index 5952b92..fb6a381 100644 --- a/packages/py-sdk/src/turbodocx_sdk/types/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/types/template.py @@ -41,7 +41,6 @@ class TemplateVariable(_TemplateVariableRequired, total=False): allowRichTextInjection: Allow rich text injection (HTML formatting) description: Variable description defaultValue: Whether this is a default value - nestedVariables: Nested variables for complex structures subvariables: Sub-variables (legacy structure) """ @@ -52,7 +51,6 @@ class TemplateVariable(_TemplateVariableRequired, total=False): allowRichTextInjection: Optional[bool] description: Optional[str] defaultValue: Optional[bool] - nestedVariables: Optional[List["TemplateVariable"]] subvariables: Optional[List["TemplateVariable"]] From 0680a27dd830edf51820ba170b83a946ec5ed30e Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 22 Jan 2026 19:16:53 +0000 Subject: [PATCH 09/39] chore: rename functions to generate advanced engine variable Signed-off-by: Kushal --- packages/go-sdk/turbotemplate.go | 4 ++-- .../main/java/com/turbodocx/models/TemplateVariable.java | 4 ++-- packages/js-sdk/src/modules/template.ts | 4 ++-- packages/py-sdk/src/turbodocx_sdk/modules/template.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index 6f3c621..dde99e1 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -239,11 +239,11 @@ func NewSimpleVariable(name string, value interface{}, placeholder ...string) Te } } -// NewNestedVariable creates a nested object variable +// NewAdvancedEngineVariable creates an advanced engine variable (for nested objects, complex data) // name: variable name // value: nested object/map value // placeholder: optional custom placeholder (pass empty string to use default {name}) -func NewNestedVariable(name string, value map[string]interface{}, placeholder ...string) TemplateVariable { +func NewAdvancedEngineVariable(name string, value map[string]interface{}, placeholder ...string) TemplateVariable { p := "" if len(placeholder) > 0 && placeholder[0] != "" { p = placeholder[0] diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java index 5c7ee53..ddbc5cd 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java @@ -249,12 +249,12 @@ public static TemplateVariable simple(String name, Object value, String... place } /** - * Creates a variable for nested objects (dot notation access) + * Creates an advanced engine variable (for nested objects, complex data) * @param name The variable name * @param value The nested object value * @param placeholder Optional placeholder override (defaults to {name}) */ - public static TemplateVariable nested(String name, Map value, String... placeholder) { + public static TemplateVariable advancedEngine(String name, Map value, String... placeholder) { String p = getPlaceholder(name, placeholder); return builder() .placeholder(p) diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts index d6a3331..2c225bd 100644 --- a/packages/js-sdk/src/modules/template.ts +++ b/packages/js-sdk/src/modules/template.ts @@ -246,12 +246,12 @@ export class TurboTemplate { } /** - * Helper: Create a nested object variable + * Helper: Create an advanced engine variable (for nested objects, complex data) * @param name - Variable name * @param value - Object value * @param placeholder - Optional custom placeholder (defaults to {name}) */ - static createNestedVariable(name: string, value: Record, placeholder?: string): NestedVariable { + static createAdvancedEngineVariable(name: string, value: Record, placeholder?: string): NestedVariable { const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); return { placeholder: p, diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py index ef1e622..46a8e1f 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -271,11 +271,11 @@ def create_simple_variable( return {"placeholder": p, "name": name, "value": value, "mimeType": VariableMimeType.TEXT} @staticmethod - def create_nested_variable( + def create_advanced_engine_variable( name: str, value: Dict[str, Any], placeholder: Optional[str] = None ) -> TemplateVariable: """ - Helper: Create a nested object variable + Helper: Create an advanced engine variable (for nested objects, complex data) Args: name: Variable name @@ -283,7 +283,7 @@ def create_nested_variable( placeholder: Optional custom placeholder (defaults to {name}) Returns: - TemplateVariable configured for nested object access + TemplateVariable configured for advanced templating engine """ p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") return { From 3604a313f6dba5f0bd0a264d30357e8eef1c5847 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:27:53 +0000 Subject: [PATCH 10/39] feat: make placeholder non optional field js and go-sdk Signed-off-by: Kushal --- .../go-sdk/examples/advanced_templating.go | 24 ++- packages/go-sdk/turbotemplate.go | 117 ++++++------ packages/go-sdk/turbotemplate_test.go | 177 ++++++++++++------ .../js-sdk/examples/advanced-templating.ts | 13 +- packages/js-sdk/src/modules/template.ts | 87 ++++++--- packages/js-sdk/src/types/template.ts | 2 +- 6 files changed, 270 insertions(+), 150 deletions(-) diff --git a/packages/go-sdk/examples/advanced_templating.go b/packages/go-sdk/examples/advanced_templating.go index 0738e5f..53fb51a 100644 --- a/packages/go-sdk/examples/advanced_templating.go +++ b/packages/go-sdk/examples/advanced_templating.go @@ -295,27 +295,27 @@ func usingHelpers(ctx context.Context, client *turbodocx.Client) { Description: &description, Variables: []turbodocx.TemplateVariable{ // Simple variable - turbodocx.NewSimpleVariable("title", "Quarterly Report"), + must(turbodocx.NewSimpleVariable("{title}", "title", "Quarterly Report", turbodocx.MimeTypeText)), - // Nested object - turbodocx.NewNestedVariable("company", map[string]interface{}{ + // Advanced engine variable + must(turbodocx.NewAdvancedEngineVariable("{company}", "company", map[string]interface{}{ "name": "Company XYZ", "headquarters": "Test Location", "employees": 500, - }), + })), // Loop variable - turbodocx.NewLoopVariable("departments", []interface{}{ + must(turbodocx.NewLoopVariable("{departments}", "departments", []interface{}{ map[string]interface{}{"name": "Dept A", "headcount": 200}, map[string]interface{}{"name": "Dept B", "headcount": 150}, map[string]interface{}{"name": "Dept C", "headcount": 100}, - }), + })), // Conditional - turbodocx.NewConditionalVariable("show_financials", true), + must(turbodocx.NewConditionalVariable("{show_financials}", "show_financials", true)), // Image - turbodocx.NewImageVariable("company_logo", "https://example.com/logo.png"), + must(turbodocx.NewImageVariable("{company_logo}", "company_logo", "https://example.com/logo.png")), }, }) if err != nil { @@ -324,3 +324,11 @@ func usingHelpers(ctx context.Context, client *turbodocx.Client) { fmt.Println("Document with helpers generated:", *result.DeliverableID) } + +// must is a helper function to handle errors in variable creation +func must(v turbodocx.TemplateVariable, err error) turbodocx.TemplateVariable { + if err != nil { + log.Fatal("Error creating variable:", err) + } + return v +} diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index dde99e1..219374f 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -219,112 +219,117 @@ func (c *TurboTemplateClient) Generate(ctx context.Context, req *GenerateTemplat // Helper functions for creating common variable types // NewSimpleVariable creates a simple text variable +// placeholder: variable placeholder (e.g., "{customer_name}") // name: variable name // value: variable value -// placeholder: optional custom placeholder (pass empty string to use default {name}) -func NewSimpleVariable(name string, value interface{}, placeholder ...string) TemplateVariable { - p := "" - if len(placeholder) > 0 && placeholder[0] != "" { - p = placeholder[0] - } else if len(name) > 0 && name[0] == '{' { - p = name - } else { - p = "{" + name + "}" +// mimeType: variable mime type (MimeTypeText or MimeTypeHTML) +// Returns error if any required parameter is missing or invalid +func NewSimpleVariable(placeholder string, name string, value interface{}, mimeType VariableMimeType) (TemplateVariable, error) { + if placeholder == "" { + return TemplateVariable{}, fmt.Errorf("placeholder is required") + } + if name == "" { + return TemplateVariable{}, fmt.Errorf("name is required") + } + if mimeType == "" { + return TemplateVariable{}, fmt.Errorf("mimeType is required") + } + if mimeType != MimeTypeText && mimeType != MimeTypeHTML { + return TemplateVariable{}, fmt.Errorf("mimeType must be 'text' or 'html'") } return TemplateVariable{ - Placeholder: p, + Placeholder: placeholder, Name: name, Value: value, - MimeType: MimeTypeText, - } + MimeType: mimeType, + }, nil } // NewAdvancedEngineVariable creates an advanced engine variable (for nested objects, complex data) +// placeholder: variable placeholder (e.g., "{user}") // name: variable name // value: nested object/map value -// placeholder: optional custom placeholder (pass empty string to use default {name}) -func NewAdvancedEngineVariable(name string, value map[string]interface{}, placeholder ...string) TemplateVariable { - p := "" - if len(placeholder) > 0 && placeholder[0] != "" { - p = placeholder[0] - } else if len(name) > 0 && name[0] == '{' { - p = name - } else { - p = "{" + name + "}" +// Returns error if any required parameter is missing +func NewAdvancedEngineVariable(placeholder string, name string, value map[string]interface{}) (TemplateVariable, error) { + if placeholder == "" { + return TemplateVariable{}, fmt.Errorf("placeholder is required") + } + if name == "" { + return TemplateVariable{}, fmt.Errorf("name is required") } usesAdvanced := true return TemplateVariable{ - Placeholder: p, + Placeholder: placeholder, Name: name, Value: value, MimeType: MimeTypeJSON, UsesAdvancedTemplatingEngine: &usesAdvanced, - } + }, nil } // NewLoopVariable creates a loop/array variable +// placeholder: variable placeholder (e.g., "{products}") // name: variable name // value: array/slice value for iteration -// placeholder: optional custom placeholder (pass empty string to use default {name}) -func NewLoopVariable(name string, value []interface{}, placeholder ...string) TemplateVariable { - p := "" - if len(placeholder) > 0 && placeholder[0] != "" { - p = placeholder[0] - } else if len(name) > 0 && name[0] == '{' { - p = name - } else { - p = "{" + name + "}" +// Returns error if any required parameter is missing +func NewLoopVariable(placeholder string, name string, value []interface{}) (TemplateVariable, error) { + if placeholder == "" { + return TemplateVariable{}, fmt.Errorf("placeholder is required") + } + if name == "" { + return TemplateVariable{}, fmt.Errorf("name is required") } usesAdvanced := true return TemplateVariable{ - Placeholder: p, + Placeholder: placeholder, Name: name, Value: value, MimeType: MimeTypeJSON, UsesAdvancedTemplatingEngine: &usesAdvanced, - } + }, nil } // NewConditionalVariable creates a conditional variable +// placeholder: variable placeholder (e.g., "{showDetails}") // name: variable name // value: conditional value (typically boolean) -// placeholder: optional custom placeholder (pass empty string to use default {name}) -func NewConditionalVariable(name string, value interface{}, placeholder ...string) TemplateVariable { - p := "" - if len(placeholder) > 0 && placeholder[0] != "" { - p = placeholder[0] - } else if len(name) > 0 && name[0] == '{' { - p = name - } else { - p = "{" + name + "}" +// Returns error if any required parameter is missing +func NewConditionalVariable(placeholder string, name string, value interface{}) (TemplateVariable, error) { + if placeholder == "" { + return TemplateVariable{}, fmt.Errorf("placeholder is required") + } + if name == "" { + return TemplateVariable{}, fmt.Errorf("name is required") } usesAdvanced := true return TemplateVariable{ - Placeholder: p, + Placeholder: placeholder, Name: name, Value: value, MimeType: MimeTypeJSON, UsesAdvancedTemplatingEngine: &usesAdvanced, - } + }, nil } // NewImageVariable creates an image variable +// placeholder: variable placeholder (e.g., "{logo}") // name: variable name // imageURL: image URL or base64 data -// placeholder: optional custom placeholder (pass empty string to use default {name}) -func NewImageVariable(name string, imageURL string, placeholder ...string) TemplateVariable { - p := "" - if len(placeholder) > 0 && placeholder[0] != "" { - p = placeholder[0] - } else if len(name) > 0 && name[0] == '{' { - p = name - } else { - p = "{" + name + "}" +// Returns error if any required parameter is missing +func NewImageVariable(placeholder string, name string, imageURL string) (TemplateVariable, error) { + if placeholder == "" { + return TemplateVariable{}, fmt.Errorf("placeholder is required") + } + if name == "" { + return TemplateVariable{}, fmt.Errorf("name is required") + } + if imageURL == "" { + return TemplateVariable{}, fmt.Errorf("imageURL is required") } return TemplateVariable{ - Placeholder: p, + Placeholder: placeholder, Name: name, Value: imageURL, MimeType: MimeTypeImage, - } + }, nil } diff --git a/packages/go-sdk/turbotemplate_test.go b/packages/go-sdk/turbotemplate_test.go index fa1001a..37cefaf 100644 --- a/packages/go-sdk/turbotemplate_test.go +++ b/packages/go-sdk/turbotemplate_test.go @@ -12,53 +12,74 @@ import ( ) func TestNewSimpleVariable(t *testing.T) { - t.Run("creates simple variable with name and value", func(t *testing.T) { - variable := NewSimpleVariable("customer_name", "Person A") + t.Run("creates simple variable with placeholder, name, value and mimeType", func(t *testing.T) { + variable, err := NewSimpleVariable("{customer_name}", "customer_name", "Person A", MimeTypeText) + require.NoError(t, err) assert.Equal(t, "{customer_name}", variable.Placeholder) assert.Equal(t, "customer_name", variable.Name) assert.Equal(t, "Person A", variable.Value) + assert.Equal(t, MimeTypeText, variable.MimeType) }) t.Run("creates simple variable with number value", func(t *testing.T) { - variable := NewSimpleVariable("order_total", 1500) + variable, err := NewSimpleVariable("{order_total}", "order_total", 1500, MimeTypeText) + require.NoError(t, err) assert.Equal(t, "{order_total}", variable.Placeholder) assert.Equal(t, "order_total", variable.Name) assert.Equal(t, 1500, variable.Value) + assert.Equal(t, MimeTypeText, variable.MimeType) }) - t.Run("creates simple variable with boolean value", func(t *testing.T) { - variable := NewSimpleVariable("is_active", true) + t.Run("creates simple variable with html mimeType", func(t *testing.T) { + variable, err := NewSimpleVariable("{content}", "content", "Bold", MimeTypeHTML) - assert.Equal(t, "{is_active}", variable.Placeholder) - assert.Equal(t, "is_active", variable.Name) - assert.Equal(t, true, variable.Value) + require.NoError(t, err) + assert.Equal(t, "{content}", variable.Placeholder) + assert.Equal(t, "content", variable.Name) + assert.Equal(t, "Bold", variable.Value) + assert.Equal(t, MimeTypeHTML, variable.MimeType) }) - t.Run("uses custom placeholder when provided", func(t *testing.T) { - variable := NewSimpleVariable("customer_name", "Person A", "{custom_placeholder}") + t.Run("returns error when placeholder is missing", func(t *testing.T) { + _, err := NewSimpleVariable("", "name", "value", MimeTypeText) - assert.Equal(t, "{custom_placeholder}", variable.Placeholder) - assert.Equal(t, "customer_name", variable.Name) + assert.Error(t, err) + assert.Contains(t, err.Error(), "placeholder is required") }) - t.Run("handles name with curly braces", func(t *testing.T) { - variable := NewSimpleVariable("{customer_name}", "Person A") + t.Run("returns error when name is missing", func(t *testing.T) { + _, err := NewSimpleVariable("{test}", "", "value", MimeTypeText) - assert.Equal(t, "{customer_name}", variable.Placeholder) - assert.Equal(t, "{customer_name}", variable.Name) + assert.Error(t, err) + assert.Contains(t, err.Error(), "name is required") + }) + + t.Run("returns error when mimeType is missing", func(t *testing.T) { + _, err := NewSimpleVariable("{test}", "test", "value", "") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "mimeType is required") + }) + + t.Run("returns error when mimeType is invalid", func(t *testing.T) { + _, err := NewSimpleVariable("{test}", "test", "value", MimeTypeJSON) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "mimeType must be 'text' or 'html'") }) } -func TestNewNestedVariable(t *testing.T) { - t.Run("creates nested variable with object value", func(t *testing.T) { - variable := NewNestedVariable("user", map[string]interface{}{ +func TestNewAdvancedEngineVariable(t *testing.T) { + t.Run("creates advanced engine variable with object value", func(t *testing.T) { + variable, err := NewAdvancedEngineVariable("{user}", "user", map[string]interface{}{ "firstName": "Foo", "lastName": "Bar", "email": "foo@example.com", }) + require.NoError(t, err) assert.Equal(t, "{user}", variable.Placeholder) assert.Equal(t, "user", variable.Name) assert.Equal(t, map[string]interface{}{ @@ -66,14 +87,13 @@ func TestNewNestedVariable(t *testing.T) { "lastName": "Bar", "email": "foo@example.com", }, variable.Value) - assert.NotNil(t, variable.MimeType) - assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.Equal(t, MimeTypeJSON, variable.MimeType) assert.NotNil(t, variable.UsesAdvancedTemplatingEngine) assert.True(t, *variable.UsesAdvancedTemplatingEngine) }) - t.Run("creates nested variable with deeply nested object", func(t *testing.T) { - variable := NewNestedVariable("company", map[string]interface{}{ + t.Run("creates advanced engine variable with deeply nested object", func(t *testing.T) { + variable, err := NewAdvancedEngineVariable("{company}", "company", map[string]interface{}{ "name": "Company ABC", "address": map[string]interface{}{ "street": "123 Test Street", @@ -82,111 +102,156 @@ func TestNewNestedVariable(t *testing.T) { }, }) + require.NoError(t, err) assert.Equal(t, "{company}", variable.Placeholder) - assert.NotNil(t, variable.MimeType) - assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.Equal(t, MimeTypeJSON, variable.MimeType) assert.True(t, *variable.UsesAdvancedTemplatingEngine) }) - t.Run("uses custom placeholder when provided", func(t *testing.T) { - variable := NewNestedVariable("user", map[string]interface{}{"name": "Test"}, "{custom_user}") + t.Run("returns error when placeholder is missing", func(t *testing.T) { + _, err := NewAdvancedEngineVariable("", "user", map[string]interface{}{"name": "Test"}) - assert.Equal(t, "{custom_user}", variable.Placeholder) - assert.Equal(t, "user", variable.Name) + assert.Error(t, err) + assert.Contains(t, err.Error(), "placeholder is required") + }) + + t.Run("returns error when name is missing", func(t *testing.T) { + _, err := NewAdvancedEngineVariable("{user}", "", map[string]interface{}{"name": "Test"}) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "name is required") }) } func TestNewLoopVariable(t *testing.T) { t.Run("creates loop variable with array value", func(t *testing.T) { - variable := NewLoopVariable("items", []interface{}{ + variable, err := NewLoopVariable("{items}", "items", []interface{}{ map[string]interface{}{"name": "Item A", "price": 100}, map[string]interface{}{"name": "Item B", "price": 200}, }) + require.NoError(t, err) assert.Equal(t, "{items}", variable.Placeholder) assert.Equal(t, "items", variable.Name) - assert.NotNil(t, variable.MimeType) - assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.Equal(t, MimeTypeJSON, variable.MimeType) assert.True(t, *variable.UsesAdvancedTemplatingEngine) }) t.Run("creates loop variable with empty array", func(t *testing.T) { - variable := NewLoopVariable("products", []interface{}{}) + variable, err := NewLoopVariable("{products}", "products", []interface{}{}) + require.NoError(t, err) assert.Equal(t, "{products}", variable.Placeholder) assert.Equal(t, []interface{}{}, variable.Value) - assert.Equal(t, MimeTypeJSON, *variable.MimeType) + assert.Equal(t, MimeTypeJSON, variable.MimeType) }) t.Run("creates loop variable with primitive array", func(t *testing.T) { - variable := NewLoopVariable("tags", []interface{}{"tag1", "tag2", "tag3"}) + variable, err := NewLoopVariable("{tags}", "tags", []interface{}{"tag1", "tag2", "tag3"}) + require.NoError(t, err) assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, variable.Value) + assert.Equal(t, MimeTypeJSON, variable.MimeType) }) - t.Run("uses custom placeholder when provided", func(t *testing.T) { - variable := NewLoopVariable("items", []interface{}{}, "{line_items}") + t.Run("returns error when placeholder is missing", func(t *testing.T) { + _, err := NewLoopVariable("", "items", []interface{}{}) - assert.Equal(t, "{line_items}", variable.Placeholder) - assert.Equal(t, "items", variable.Name) + assert.Error(t, err) + assert.Contains(t, err.Error(), "placeholder is required") + }) + + t.Run("returns error when name is missing", func(t *testing.T) { + _, err := NewLoopVariable("{items}", "", []interface{}{}) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "name is required") }) } func TestNewConditionalVariable(t *testing.T) { t.Run("creates conditional variable with boolean true", func(t *testing.T) { - variable := NewConditionalVariable("is_premium", true) + variable, err := NewConditionalVariable("{is_premium}", "is_premium", true) + require.NoError(t, err) assert.Equal(t, "{is_premium}", variable.Placeholder) assert.Equal(t, "is_premium", variable.Name) assert.Equal(t, true, variable.Value) + assert.Equal(t, MimeTypeJSON, variable.MimeType) assert.True(t, *variable.UsesAdvancedTemplatingEngine) }) t.Run("creates conditional variable with boolean false", func(t *testing.T) { - variable := NewConditionalVariable("show_discount", false) + variable, err := NewConditionalVariable("{show_discount}", "show_discount", false) + require.NoError(t, err) assert.Equal(t, false, variable.Value) + assert.Equal(t, MimeTypeJSON, variable.MimeType) assert.True(t, *variable.UsesAdvancedTemplatingEngine) }) t.Run("creates conditional variable with truthy value", func(t *testing.T) { - variable := NewConditionalVariable("count", 5) + variable, err := NewConditionalVariable("{count}", "count", 5) + require.NoError(t, err) assert.Equal(t, 5, variable.Value) + assert.Equal(t, MimeTypeJSON, variable.MimeType) }) - t.Run("uses custom placeholder when provided", func(t *testing.T) { - variable := NewConditionalVariable("is_active", true, "{active_flag}") + t.Run("returns error when placeholder is missing", func(t *testing.T) { + _, err := NewConditionalVariable("", "is_active", true) - assert.Equal(t, "{active_flag}", variable.Placeholder) - assert.Equal(t, "is_active", variable.Name) + assert.Error(t, err) + assert.Contains(t, err.Error(), "placeholder is required") + }) + + t.Run("returns error when name is missing", func(t *testing.T) { + _, err := NewConditionalVariable("{is_active}", "", true) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "name is required") }) } func TestNewImageVariable(t *testing.T) { t.Run("creates image variable with URL", func(t *testing.T) { - variable := NewImageVariable("logo", "https://example.com/logo.png") + variable, err := NewImageVariable("{logo}", "logo", "https://example.com/logo.png") + require.NoError(t, err) assert.Equal(t, "{logo}", variable.Placeholder) assert.Equal(t, "logo", variable.Name) assert.Equal(t, "https://example.com/logo.png", variable.Value) - assert.NotNil(t, variable.MimeType) - assert.Equal(t, MimeTypeImage, *variable.MimeType) + assert.Equal(t, MimeTypeImage, variable.MimeType) }) t.Run("creates image variable with base64", func(t *testing.T) { base64Image := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." - variable := NewImageVariable("signature", base64Image) + variable, err := NewImageVariable("{signature}", "signature", base64Image) + require.NoError(t, err) assert.Equal(t, base64Image, variable.Value) - assert.Equal(t, MimeTypeImage, *variable.MimeType) + assert.Equal(t, MimeTypeImage, variable.MimeType) }) - t.Run("uses custom placeholder when provided", func(t *testing.T) { - variable := NewImageVariable("logo", "https://example.com/logo.png", "{company_logo}") + t.Run("returns error when placeholder is missing", func(t *testing.T) { + _, err := NewImageVariable("", "logo", "https://example.com/logo.png") - assert.Equal(t, "{company_logo}", variable.Placeholder) - assert.Equal(t, "logo", variable.Name) + assert.Error(t, err) + assert.Contains(t, err.Error(), "placeholder is required") + }) + + t.Run("returns error when name is missing", func(t *testing.T) { + _, err := NewImageVariable("{logo}", "", "https://example.com/logo.png") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "name is required") + }) + + t.Run("returns error when imageURL is missing", func(t *testing.T) { + _, err := NewImageVariable("{logo}", "logo", "") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "imageURL is required") }) } @@ -358,7 +423,7 @@ func TestTurboTemplateClient_Generate(t *testing.T) { Description: &desc, Variables: []TemplateVariable{ NewSimpleVariable("title", "Quarterly Report"), - NewNestedVariable("company", map[string]interface{}{"name": "Company XYZ", "employees": 500}), + NewAdvancedEngineVariable("company", map[string]interface{}{"name": "Company XYZ", "employees": 500}), NewLoopVariable("departments", []interface{}{map[string]interface{}{"name": "Dept A"}, map[string]interface{}{"name": "Dept B"}}), NewConditionalVariable("show_financials", true), NewImageVariable("logo", "https://example.com/logo.png"), diff --git a/packages/js-sdk/examples/advanced-templating.ts b/packages/js-sdk/examples/advanced-templating.ts index 9faeaa7..f574bac 100644 --- a/packages/js-sdk/examples/advanced-templating.ts +++ b/packages/js-sdk/examples/advanced-templating.ts @@ -420,27 +420,27 @@ async function usingHelpers() { description: 'Using helper functions example', variables: [ // Simple variable - helper adds {} and sets mimeType - TurboTemplate.createSimpleVariable('title', 'Quarterly Report'), + TurboTemplate.createSimpleVariable('{title}', 'title', 'Quarterly Report', 'text'), - // Nested object - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: true - TurboTemplate.createNestedVariable('company', { + // Advanced engine variable - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: true + TurboTemplate.createAdvancedEngineVariable('{company}', 'company', { name: 'Company XYZ', headquarters: 'Test Location', employees: 500, }), // Loop variable - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: true - TurboTemplate.createLoopVariable('departments', [ + TurboTemplate.createLoopVariable('{departments}', 'departments', [ { name: 'Dept A', headcount: 200 }, { name: 'Dept B', headcount: 150 }, { name: 'Dept C', headcount: 100 }, ]), // Conditional - helper sets usesAdvancedTemplatingEngine: true - TurboTemplate.createConditionalVariable('show_financials', true), + TurboTemplate.createConditionalVariable('{show_financials}', 'show_financials', true), // Image - helper sets mimeType: 'image' - TurboTemplate.createImageVariable('company_logo', 'https://example.com/logo.png'), + TurboTemplate.createImageVariable('{company_logo}', 'company_logo', 'https://example.com/logo.png'), ], }); @@ -475,6 +475,7 @@ function variableValidation() { const warningVariable = { placeholder: '{items}', name: 'items', + mimeType: 'text' as const, value: [1, 2, 3], }; diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts index 2c225bd..7504c17 100644 --- a/packages/js-sdk/src/modules/template.ts +++ b/packages/js-sdk/src/modules/template.ts @@ -189,8 +189,8 @@ export class TurboTemplate { const warnings: string[] = []; // Check placeholder/name - if (!variable.placeholder && !variable.name) { - errors.push('Variable must have either "placeholder" or "name" property'); + if (!variable.placeholder || !variable.name) { + errors.push('Variable must have both "placeholder" and "name" properties'); } // Check value/text @@ -231,30 +231,53 @@ export class TurboTemplate { /** * Helper: Create a simple text variable + * @param placeholder - Variable placeholder (e.g., '{customer_name}') * @param name - Variable name * @param value - Variable value - * @param placeholder - Optional custom placeholder (defaults to {name}) + * @param mimeType - Variable mime type ('text' or 'html') */ - static createSimpleVariable(name: string, value: string | number | boolean, placeholder?: string): SimpleVariable { - const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + static createSimpleVariable( + placeholder: string, + name: string, + value: string | number | boolean, + mimeType: 'text' | 'html' + ): SimpleVariable { + if (!placeholder) { + throw new Error('placeholder is required'); + } + if (!name) { + throw new Error('name is required'); + } + if (!mimeType) { + throw new Error('mimeType is required'); + } return { - placeholder: p, + placeholder, name, value, - mimeType: 'text', + mimeType, }; } /** * Helper: Create an advanced engine variable (for nested objects, complex data) + * @param placeholder - Variable placeholder (e.g., '{user}') * @param name - Variable name * @param value - Object value - * @param placeholder - Optional custom placeholder (defaults to {name}) */ - static createAdvancedEngineVariable(name: string, value: Record, placeholder?: string): NestedVariable { - const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + static createAdvancedEngineVariable( + placeholder: string, + name: string, + value: Record + ): NestedVariable { + if (!placeholder) { + throw new Error('placeholder is required'); + } + if (!name) { + throw new Error('name is required'); + } return { - placeholder: p, + placeholder, name, value, usesAdvancedTemplatingEngine: true, @@ -264,14 +287,19 @@ export class TurboTemplate { /** * Helper: Create a loop/array variable + * @param placeholder - Variable placeholder (e.g., '{products}') * @param name - Variable name * @param value - Array value - * @param placeholder - Optional custom placeholder (defaults to {name}) */ - static createLoopVariable(name: string, value: any[], placeholder?: string): LoopVariable { - const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + static createLoopVariable(placeholder: string, name: string, value: any[]): LoopVariable { + if (!placeholder) { + throw new Error('placeholder is required'); + } + if (!name) { + throw new Error('name is required'); + } return { - placeholder: p, + placeholder, name, value, usesAdvancedTemplatingEngine: true, @@ -281,14 +309,19 @@ export class TurboTemplate { /** * Helper: Create a conditional variable + * @param placeholder - Variable placeholder (e.g., '{showDetails}') * @param name - Variable name * @param value - Conditional value - * @param placeholder - Optional custom placeholder (defaults to {name}) */ - static createConditionalVariable(name: string, value: any, placeholder?: string): ConditionalVariable { - const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + static createConditionalVariable(placeholder: string, name: string, value: any): ConditionalVariable { + if (!placeholder) { + throw new Error('placeholder is required'); + } + if (!name) { + throw new Error('name is required'); + } return { - placeholder: p, + placeholder, name, value, mimeType: 'json', @@ -298,14 +331,22 @@ export class TurboTemplate { /** * Helper: Create an image variable + * @param placeholder - Variable placeholder (e.g., '{logo}') * @param name - Variable name * @param imageUrl - Image URL or base64 string - * @param placeholder - Optional custom placeholder (defaults to {name}) */ - static createImageVariable(name: string, imageUrl: string, placeholder?: string): ImageVariable { - const p = placeholder ?? (name.startsWith('{') ? name : `{${name}}`); + static createImageVariable(placeholder: string, name: string, imageUrl: string): ImageVariable { + if (!placeholder) { + throw new Error('placeholder is required'); + } + if (!name) { + throw new Error('name is required'); + } + if (!imageUrl) { + throw new Error('imageUrl is required'); + } return { - placeholder: p, + placeholder, name, value: imageUrl, mimeType: 'image', diff --git a/packages/js-sdk/src/types/template.ts b/packages/js-sdk/src/types/template.ts index 3e8179a..44a0cfc 100644 --- a/packages/js-sdk/src/types/template.ts +++ b/packages/js-sdk/src/types/template.ts @@ -124,7 +124,7 @@ export interface SimpleVariable { placeholder: string; name: string; value: string | number | boolean; - mimeType: 'text'; + mimeType: 'text' | 'html'; } /** Variable with nested structure (e.g., user.name, user.email) */ From 5589a288c977762d968af909314748ada3c163cd Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:28:50 +0000 Subject: [PATCH 11/39] feat: make placeholder non optional field py-sdk Signed-off-by: Kushal --- .../py-sdk/examples/advanced_templating.py | 12 ++- .../src/turbodocx_sdk/modules/template.py | 92 ++++++++++++++----- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/packages/py-sdk/examples/advanced_templating.py b/packages/py-sdk/examples/advanced_templating.py index 56ac664..8388f75 100644 --- a/packages/py-sdk/examples/advanced_templating.py +++ b/packages/py-sdk/examples/advanced_templating.py @@ -267,9 +267,10 @@ async def using_helpers(): "description": "Using helper functions example", "variables": [ # Simple variable - helper adds {} and sets mimeType - TurboTemplate.create_simple_variable("title", "Quarterly Report"), - # Nested object - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: True - TurboTemplate.create_nested_variable( + TurboTemplate.create_simple_variable("{title}", "title", "Quarterly Report", "text"), + # Advanced engine variable - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: True + TurboTemplate.create_advanced_engine_variable( + "{company}", "company", { "name": "Company XYZ", @@ -279,6 +280,7 @@ async def using_helpers(): ), # Loop variable - helper sets mimeType: 'json' and usesAdvancedTemplatingEngine: True TurboTemplate.create_loop_variable( + "{departments}", "departments", [ {"name": "Dept A", "headcount": 200}, @@ -287,10 +289,10 @@ async def using_helpers(): ], ), # Conditional - helper sets usesAdvancedTemplatingEngine: True - TurboTemplate.create_conditional_variable("show_financials", True), + TurboTemplate.create_conditional_variable("{show_financials}", "show_financials", True), # Image - helper sets mimeType: 'image' TurboTemplate.create_image_variable( - "company_logo", "https://example.com/logo.png" + "{company_logo}", "company_logo", "https://example.com/logo.png" ), ], }) diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py index 46a8e1f..52f1a33 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -218,8 +218,8 @@ def validate_variable(cls, variable: TemplateVariable) -> VariableValidation: warnings: List[str] = [] # Check placeholder/name - if not variable.get("placeholder") and not variable.get("name"): - errors.append('Variable must have either "placeholder" or "name" property') + if not variable.get("placeholder") or not variable.get("name"): + errors.append('Variable must have both "placeholder" and "name" properties') # Check value/text has_value = "value" in variable and variable["value"] is not None @@ -254,40 +254,62 @@ def validate_variable(cls, variable: TemplateVariable) -> VariableValidation: @staticmethod def create_simple_variable( - name: str, value: Union[str, int, float, bool], placeholder: Optional[str] = None + placeholder: str, + name: str, + value: Union[str, int, float, bool], + mime_type: str, ) -> TemplateVariable: """ Helper: Create a simple text variable Args: + placeholder: Variable placeholder (e.g., '{customer_name}') name: Variable name value: Variable value - placeholder: Optional custom placeholder (defaults to {name}) + mime_type: Variable mime type ('text' or 'html') Returns: TemplateVariable configured for simple substitution + + Raises: + ValueError: If any required parameter is missing or invalid """ - p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") - return {"placeholder": p, "name": name, "value": value, "mimeType": VariableMimeType.TEXT} + if not placeholder: + raise ValueError("placeholder is required") + if not name: + raise ValueError("name is required") + if not mime_type: + raise ValueError("mime_type is required") + if mime_type not in (VariableMimeType.TEXT, VariableMimeType.HTML): + raise ValueError("mime_type must be 'text' or 'html'") + return {"placeholder": placeholder, "name": name, "value": value, "mimeType": mime_type} @staticmethod def create_advanced_engine_variable( - name: str, value: Dict[str, Any], placeholder: Optional[str] = None + placeholder: str, + name: str, + value: Dict[str, Any], ) -> TemplateVariable: """ Helper: Create an advanced engine variable (for nested objects, complex data) Args: + placeholder: Variable placeholder (e.g., '{user}') name: Variable name value: Nested object/dict value - placeholder: Optional custom placeholder (defaults to {name}) Returns: TemplateVariable configured for advanced templating engine + + Raises: + ValueError: If any required parameter is missing """ - p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + if not placeholder: + raise ValueError("placeholder is required") + if not name: + raise ValueError("name is required") return { - "placeholder": p, + "placeholder": placeholder, "name": name, "value": value, "usesAdvancedTemplatingEngine": True, @@ -296,22 +318,30 @@ def create_advanced_engine_variable( @staticmethod def create_loop_variable( - name: str, value: List[Any], placeholder: Optional[str] = None + placeholder: str, + name: str, + value: List[Any], ) -> TemplateVariable: """ Helper: Create a loop/array variable Args: + placeholder: Variable placeholder (e.g., '{products}') name: Variable name value: Array/list value for iteration - placeholder: Optional custom placeholder (defaults to {name}) Returns: TemplateVariable configured for loop iteration + + Raises: + ValueError: If any required parameter is missing """ - p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + if not placeholder: + raise ValueError("placeholder is required") + if not name: + raise ValueError("name is required") return { - "placeholder": p, + "placeholder": placeholder, "name": name, "value": value, "usesAdvancedTemplatingEngine": True, @@ -320,22 +350,30 @@ def create_loop_variable( @staticmethod def create_conditional_variable( - name: str, value: Any, placeholder: Optional[str] = None + placeholder: str, + name: str, + value: Any, ) -> TemplateVariable: """ Helper: Create a conditional variable Args: + placeholder: Variable placeholder (e.g., '{showDetails}') name: Variable name value: Variable value (typically boolean) - placeholder: Optional custom placeholder (defaults to {name}) Returns: TemplateVariable configured for conditionals + + Raises: + ValueError: If any required parameter is missing """ - p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + if not placeholder: + raise ValueError("placeholder is required") + if not name: + raise ValueError("name is required") return { - "placeholder": p, + "placeholder": placeholder, "name": name, "value": value, "mimeType": VariableMimeType.JSON, @@ -344,22 +382,32 @@ def create_conditional_variable( @staticmethod def create_image_variable( - name: str, image_url: str, placeholder: Optional[str] = None + placeholder: str, + name: str, + image_url: str, ) -> TemplateVariable: """ Helper: Create an image variable Args: + placeholder: Variable placeholder (e.g., '{logo}') name: Variable name image_url: Image URL or base64 data - placeholder: Optional custom placeholder (defaults to {name}) Returns: TemplateVariable configured for image insertion + + Raises: + ValueError: If any required parameter is missing """ - p = placeholder if placeholder else (name if name.startswith("{") else f"{{{name}}}") + if not placeholder: + raise ValueError("placeholder is required") + if not name: + raise ValueError("name is required") + if not image_url: + raise ValueError("image_url is required") return { - "placeholder": p, + "placeholder": placeholder, "name": name, "value": image_url, "mimeType": VariableMimeType.IMAGE, From 61b5b8bfc5943c42f1f252ec72a866958e67a5fc Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:28:59 +0000 Subject: [PATCH 12/39] feat: make placeholder non optional field java-sdk Signed-off-by: Kushal --- .../java-sdk/examples/AdvancedTemplating.java | 34 ++-- .../java/com/turbodocx/TurboTemplate.java | 2 +- .../turbodocx/models/TemplateVariable.java | 100 +++++++---- .../java/com/turbodocx/TurboTemplateTest.java | 170 ++++++++++++------ 4 files changed, 198 insertions(+), 108 deletions(-) diff --git a/packages/java-sdk/examples/AdvancedTemplating.java b/packages/java-sdk/examples/AdvancedTemplating.java index c67c491..0084bc8 100644 --- a/packages/java-sdk/examples/AdvancedTemplating.java +++ b/packages/java-sdk/examples/AdvancedTemplating.java @@ -47,9 +47,9 @@ public static void simpleSubstitution() { GenerateTemplateRequest.builder() .templateId("your-template-id") .variables(Arrays.asList( - TemplateVariable.simple("customer_name", "Person A"), - TemplateVariable.simple("order_total", 1500), - TemplateVariable.simple("order_date", "2024-01-15") + TemplateVariable.simple("{customer_name}", "customer_name", "Person A", VariableMimeType.TEXT), + TemplateVariable.simple("{order_total}", "order_total", 1500, VariableMimeType.TEXT), + TemplateVariable.simple("{order_date}", "order_date", "2024-01-15", VariableMimeType.TEXT) )) .build() ); @@ -81,7 +81,7 @@ public static void nestedObjects() { GenerateTemplateRequest.builder() .templateId("your-template-id") .variables(Collections.singletonList( - TemplateVariable.nested("user", user) + TemplateVariable.advancedEngine("{user}", "user", user) )) .build() ); @@ -112,7 +112,7 @@ public static void loopsAndArrays() { GenerateTemplateRequest.builder() .templateId("your-template-id") .variables(Collections.singletonList( - TemplateVariable.loop("items", items) + TemplateVariable.loop("{items}", "items", items) )) .build() ); @@ -241,11 +241,11 @@ public static void complexInvoice() { .name("Invoice - Company XYZ") .description("Monthly invoice for Company XYZ") .variables(Arrays.asList( - TemplateVariable.nested("customer", customer), - TemplateVariable.simple("invoice_number", "INV-2024-001"), - TemplateVariable.simple("invoice_date", "2024-01-15"), - TemplateVariable.simple("due_date", "2024-02-14"), - TemplateVariable.loop("items", items), + TemplateVariable.advancedEngine("{customer}", "customer", customer), + TemplateVariable.simple("{invoice_number}", "invoice_number", "INV-2024-001", VariableMimeType.TEXT), + TemplateVariable.simple("{invoice_date}", "invoice_date", "2024-01-15", VariableMimeType.TEXT), + TemplateVariable.simple("{due_date}", "due_date", "2024-02-14", VariableMimeType.TEXT), + TemplateVariable.loop("{items}", "items", items), TemplateVariable.builder() .placeholder("{tax_rate}") .name("tax_rate") @@ -266,8 +266,8 @@ public static void complexInvoice() { .value("0.05") .usesAdvancedTemplatingEngine(true) .build(), - TemplateVariable.simple("payment_terms", "Net 30"), - TemplateVariable.simple("notes", "Thank you for your business!") + TemplateVariable.simple("{payment_terms}", "payment_terms", "Net 30", VariableMimeType.TEXT), + TemplateVariable.simple("{notes}", "notes", "Thank you for your business!", VariableMimeType.TEXT) )) .build() ); @@ -299,11 +299,11 @@ public static void usingHelpers() { GenerateTemplateRequest.builder() .templateId("your-template-id") .variables(Arrays.asList( - TemplateVariable.simple("title", "Quarterly Report"), - TemplateVariable.nested("company", company), - TemplateVariable.loop("departments", departments), - TemplateVariable.conditional("show_financials", true), - TemplateVariable.image("company_logo", "https://example.com/logo.png") + TemplateVariable.simple("{title}", "title", "Quarterly Report", VariableMimeType.TEXT), + TemplateVariable.advancedEngine("{company}", "company", company), + TemplateVariable.loop("{departments}", "departments", departments), + TemplateVariable.conditional("{show_financials}", "show_financials", true), + TemplateVariable.image("{company_logo}", "company_logo", "https://example.com/logo.png") )) .build() ); diff --git a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java index 0437a60..6191c93 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java @@ -38,7 +38,7 @@ * GenerateTemplateRequest.builder() * .templateId("template-uuid") * .variables(Arrays.asList( - * TemplateVariable.nested("user", user) + * TemplateVariable.advancedEngine("user", user) * )) * .build() * ); diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java index ddbc5cd..6ba97b5 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java @@ -219,9 +219,6 @@ public TemplateVariable build() { if (variable.mimeType == null || variable.mimeType.isEmpty()) { throw new IllegalStateException("mimeType must be set"); } - if (variable.value == null && variable.text == null) { - throw new IllegalStateException("Either value or text must be set"); - } return variable; } } @@ -234,30 +231,49 @@ public static Builder builder() { /** * Creates a simple text variable - * @param name The variable name (used as placeholder if not provided) + * @param placeholder The variable placeholder (e.g., "{customer_name}") + * @param name The variable name * @param value The value to substitute - * @param placeholder Optional placeholder override (defaults to {name}) + * @param mimeType The mime type (TEXT or HTML) + * @throws IllegalArgumentException if any required parameter is missing or invalid */ - public static TemplateVariable simple(String name, Object value, String... placeholder) { - String p = getPlaceholder(name, placeholder); + public static TemplateVariable simple(String placeholder, String name, Object value, VariableMimeType mimeType) { + if (placeholder == null || placeholder.isEmpty()) { + throw new IllegalArgumentException("placeholder is required"); + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name is required"); + } + if (mimeType == null) { + throw new IllegalArgumentException("mimeType is required"); + } + if (mimeType != VariableMimeType.TEXT && mimeType != VariableMimeType.HTML) { + throw new IllegalArgumentException("mimeType must be TEXT or HTML"); + } return builder() - .placeholder(p) + .placeholder(placeholder) .name(name) .value(value) - .mimeType(VariableMimeType.TEXT) + .mimeType(mimeType) .build(); } /** * Creates an advanced engine variable (for nested objects, complex data) + * @param placeholder The variable placeholder (e.g., "{user}") * @param name The variable name * @param value The nested object value - * @param placeholder Optional placeholder override (defaults to {name}) + * @throws IllegalArgumentException if any required parameter is missing */ - public static TemplateVariable advancedEngine(String name, Map value, String... placeholder) { - String p = getPlaceholder(name, placeholder); + public static TemplateVariable advancedEngine(String placeholder, String name, Map value) { + if (placeholder == null || placeholder.isEmpty()) { + throw new IllegalArgumentException("placeholder is required"); + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name is required"); + } return builder() - .placeholder(p) + .placeholder(placeholder) .name(name) .value(value) .mimeType(VariableMimeType.JSON) @@ -267,14 +283,20 @@ public static TemplateVariable advancedEngine(String name, Map v /** * Creates a variable for array loops + * @param placeholder The variable placeholder (e.g., "{products}") * @param name The variable name * @param value The array/list value - * @param placeholder Optional placeholder override (defaults to {name}) + * @throws IllegalArgumentException if any required parameter is missing */ - public static TemplateVariable loop(String name, List value, String... placeholder) { - String p = getPlaceholder(name, placeholder); + public static TemplateVariable loop(String placeholder, String name, List value) { + if (placeholder == null || placeholder.isEmpty()) { + throw new IllegalArgumentException("placeholder is required"); + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name is required"); + } return builder() - .placeholder(p) + .placeholder(placeholder) .name(name) .value(value) .mimeType(VariableMimeType.JSON) @@ -284,14 +306,20 @@ public static TemplateVariable loop(String name, List value, String... placeh /** * Creates a variable for conditionals + * @param placeholder The variable placeholder (e.g., "{showDetails}") * @param name The variable name * @param value The boolean or truthy value - * @param placeholder Optional placeholder override (defaults to {name}) + * @throws IllegalArgumentException if any required parameter is missing */ - public static TemplateVariable conditional(String name, Object value, String... placeholder) { - String p = getPlaceholder(name, placeholder); + public static TemplateVariable conditional(String placeholder, String name, Object value) { + if (placeholder == null || placeholder.isEmpty()) { + throw new IllegalArgumentException("placeholder is required"); + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name is required"); + } return builder() - .placeholder(p) + .placeholder(placeholder) .name(name) .value(value) .mimeType(VariableMimeType.JSON) @@ -301,30 +329,26 @@ public static TemplateVariable conditional(String name, Object value, String... /** * Creates a variable for images + * @param placeholder The variable placeholder (e.g., "{logo}") * @param name The variable name * @param imageUrl The image URL - * @param placeholder Optional placeholder override (defaults to {name}) + * @throws IllegalArgumentException if any required parameter is missing */ - public static TemplateVariable image(String name, String imageUrl, String... placeholder) { - String p = getPlaceholder(name, placeholder); + public static TemplateVariable image(String placeholder, String name, String imageUrl) { + if (placeholder == null || placeholder.isEmpty()) { + throw new IllegalArgumentException("placeholder is required"); + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name is required"); + } + if (imageUrl == null || imageUrl.isEmpty()) { + throw new IllegalArgumentException("imageUrl is required"); + } return builder() - .placeholder(p) + .placeholder(placeholder) .name(name) .value(imageUrl) .mimeType(VariableMimeType.IMAGE) .build(); } - - /** - * Helper to determine placeholder from name and optional override - */ - private static String getPlaceholder(String name, String... placeholder) { - if (placeholder.length > 0 && placeholder[0] != null && !placeholder[0].isEmpty()) { - return placeholder[0]; - } - if (name.startsWith("{")) { - return name; - } - return "{" + name + "}"; - } } diff --git a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java index 576b1c5..4b6fbfc 100644 --- a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java +++ b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java @@ -50,19 +50,20 @@ void tearDown() throws IOException { // ============================================ @Test - @DisplayName("simple() should create variable with name and value") - void simpleVariableWithNameAndValue() { - TemplateVariable variable = TemplateVariable.simple("customer_name", "Person A"); + @DisplayName("simple() should create variable with placeholder, name, value and mimeType") + void simpleVariableWithAllRequiredParams() { + TemplateVariable variable = TemplateVariable.simple("{customer_name}", "customer_name", "Person A", VariableMimeType.TEXT); assertEquals("{customer_name}", variable.getPlaceholder()); assertEquals("customer_name", variable.getName()); assertEquals("Person A", variable.getValue()); + assertEquals(VariableMimeType.TEXT.getValue(), variable.getMimeType()); } @Test @DisplayName("simple() should create variable with number value") void simpleVariableWithNumberValue() { - TemplateVariable variable = TemplateVariable.simple("order_total", 1500); + TemplateVariable variable = TemplateVariable.simple("{order_total}", "order_total", 1500, VariableMimeType.TEXT); assertEquals("{order_total}", variable.getPlaceholder()); assertEquals("order_total", variable.getName()); @@ -70,46 +71,61 @@ void simpleVariableWithNumberValue() { } @Test - @DisplayName("simple() should create variable with boolean value") - void simpleVariableWithBooleanValue() { - TemplateVariable variable = TemplateVariable.simple("is_active", true); + @DisplayName("simple() should create variable with HTML mimeType") + void simpleVariableWithHtmlMimeType() { + TemplateVariable variable = TemplateVariable.simple("{content}", "content", "Bold", VariableMimeType.HTML); - assertEquals("{is_active}", variable.getPlaceholder()); - assertEquals("is_active", variable.getName()); - assertEquals(true, variable.getValue()); + assertEquals("{content}", variable.getPlaceholder()); + assertEquals("content", variable.getName()); + assertEquals("Bold", variable.getValue()); + assertEquals(VariableMimeType.HTML.getValue(), variable.getMimeType()); } @Test - @DisplayName("simple() should use custom placeholder when provided") - void simpleVariableWithCustomPlaceholder() { - TemplateVariable variable = TemplateVariable.simple("customer_name", "Person A", "{custom_placeholder}"); + @DisplayName("simple() should throw error when placeholder is missing") + void simpleVariableThrowsWhenPlaceholderMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.simple("", "name", "value", VariableMimeType.TEXT) + ); + } - assertEquals("{custom_placeholder}", variable.getPlaceholder()); - assertEquals("customer_name", variable.getName()); + @Test + @DisplayName("simple() should throw error when name is missing") + void simpleVariableThrowsWhenNameMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.simple("{test}", "", "value", VariableMimeType.TEXT) + ); } @Test - @DisplayName("simple() should handle name with curly braces") - void simpleVariableWithCurlyBracesInName() { - TemplateVariable variable = TemplateVariable.simple("{customer_name}", "Person A"); + @DisplayName("simple() should throw error when mimeType is missing") + void simpleVariableThrowsWhenMimeTypeMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.simple("{test}", "test", "value", null) + ); + } - assertEquals("{customer_name}", variable.getPlaceholder()); - assertEquals("{customer_name}", variable.getName()); + @Test + @DisplayName("simple() should throw error when mimeType is invalid") + void simpleVariableThrowsWhenMimeTypeInvalid() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.simple("{test}", "test", "value", VariableMimeType.JSON) + ); } // ============================================ - // Helper Function Tests - nested() + // Helper Function Tests - advancedEngine() // ============================================ @Test - @DisplayName("nested() should create variable with object value") - void nestedVariableWithObjectValue() { + @DisplayName("advancedEngine() should create variable with object value") + void advancedEngineVariableWithObjectValue() { Map user = new HashMap<>(); user.put("firstName", "Foo"); user.put("lastName", "Bar"); user.put("email", "foo@example.com"); - TemplateVariable variable = TemplateVariable.nested("user", user); + TemplateVariable variable = TemplateVariable.advancedEngine("{user}", "user", user); assertEquals("{user}", variable.getPlaceholder()); assertEquals("user", variable.getName()); @@ -119,8 +135,8 @@ void nestedVariableWithObjectValue() { } @Test - @DisplayName("nested() should create variable with deeply nested object") - void nestedVariableWithDeeplyNestedObject() { + @DisplayName("advancedEngine() should create variable with deeply nested object") + void advancedEngineVariableWithDeeplyNestedObject() { Map address = new HashMap<>(); address.put("street", "123 Test Street"); address.put("city", "Test City"); @@ -130,7 +146,7 @@ void nestedVariableWithDeeplyNestedObject() { company.put("name", "Company ABC"); company.put("address", address); - TemplateVariable variable = TemplateVariable.nested("company", company); + TemplateVariable variable = TemplateVariable.advancedEngine("{company}", "company", company); assertEquals("{company}", variable.getPlaceholder()); assertEquals("json", variable.getMimeType()); @@ -138,15 +154,25 @@ void nestedVariableWithDeeplyNestedObject() { } @Test - @DisplayName("nested() should use custom placeholder when provided") - void nestedVariableWithCustomPlaceholder() { + @DisplayName("advancedEngine() should throw error when placeholder is missing") + void advancedEngineVariableThrowsWhenPlaceholderMissing() { Map user = new HashMap<>(); user.put("name", "Test"); - TemplateVariable variable = TemplateVariable.nested("user", user, "{custom_user}"); + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.advancedEngine("", "user", user) + ); + } - assertEquals("{custom_user}", variable.getPlaceholder()); - assertEquals("user", variable.getName()); + @Test + @DisplayName("advancedEngine() should throw error when name is missing") + void advancedEngineVariableThrowsWhenNameMissing() { + Map user = new HashMap<>(); + user.put("name", "Test"); + + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.advancedEngine("{user}", "", user) + ); } // ============================================ @@ -161,7 +187,7 @@ void loopVariableWithArrayValue() { Map.of("name", "Item B", "price", 200) ); - TemplateVariable variable = TemplateVariable.loop("items", items); + TemplateVariable variable = TemplateVariable.loop("{items}", "items", items); assertEquals("{items}", variable.getPlaceholder()); assertEquals("items", variable.getName()); @@ -175,7 +201,7 @@ void loopVariableWithArrayValue() { void loopVariableWithEmptyArray() { List> products = Collections.emptyList(); - TemplateVariable variable = TemplateVariable.loop("products", products); + TemplateVariable variable = TemplateVariable.loop("{products}", "products", products); assertEquals("{products}", variable.getPlaceholder()); assertEquals(products, variable.getValue()); @@ -187,20 +213,29 @@ void loopVariableWithEmptyArray() { void loopVariableWithPrimitiveArray() { List tags = Arrays.asList("tag1", "tag2", "tag3"); - TemplateVariable variable = TemplateVariable.loop("tags", tags); + TemplateVariable variable = TemplateVariable.loop("{tags}", "tags", tags); assertEquals(tags, variable.getValue()); } @Test - @DisplayName("loop() should use custom placeholder when provided") - void loopVariableWithCustomPlaceholder() { + @DisplayName("loop() should throw error when placeholder is missing") + void loopVariableThrowsWhenPlaceholderMissing() { List> items = Collections.emptyList(); - TemplateVariable variable = TemplateVariable.loop("items", items, "{line_items}"); + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.loop("", "items", items) + ); + } + + @Test + @DisplayName("loop() should throw error when name is missing") + void loopVariableThrowsWhenNameMissing() { + List> items = Collections.emptyList(); - assertEquals("{line_items}", variable.getPlaceholder()); - assertEquals("items", variable.getName()); + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.loop("{items}", "", items) + ); } // ============================================ @@ -210,7 +245,7 @@ void loopVariableWithCustomPlaceholder() { @Test @DisplayName("conditional() should create variable with boolean true") void conditionalVariableWithBooleanTrue() { - TemplateVariable variable = TemplateVariable.conditional("is_premium", true); + TemplateVariable variable = TemplateVariable.conditional("{is_premium}", "is_premium", true); assertEquals("{is_premium}", variable.getPlaceholder()); assertEquals("is_premium", variable.getName()); @@ -221,7 +256,7 @@ void conditionalVariableWithBooleanTrue() { @Test @DisplayName("conditional() should create variable with boolean false") void conditionalVariableWithBooleanFalse() { - TemplateVariable variable = TemplateVariable.conditional("show_discount", false); + TemplateVariable variable = TemplateVariable.conditional("{show_discount}", "show_discount", false); assertEquals(false, variable.getValue()); assertTrue(variable.getUsesAdvancedTemplatingEngine()); @@ -230,18 +265,25 @@ void conditionalVariableWithBooleanFalse() { @Test @DisplayName("conditional() should create variable with truthy value") void conditionalVariableWithTruthyValue() { - TemplateVariable variable = TemplateVariable.conditional("count", 5); + TemplateVariable variable = TemplateVariable.conditional("{count}", "count", 5); assertEquals(5, variable.getValue()); } @Test - @DisplayName("conditional() should use custom placeholder when provided") - void conditionalVariableWithCustomPlaceholder() { - TemplateVariable variable = TemplateVariable.conditional("is_active", true, "{active_flag}"); + @DisplayName("conditional() should throw error when placeholder is missing") + void conditionalVariableThrowsWhenPlaceholderMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.conditional("", "is_active", true) + ); + } - assertEquals("{active_flag}", variable.getPlaceholder()); - assertEquals("is_active", variable.getName()); + @Test + @DisplayName("conditional() should throw error when name is missing") + void conditionalVariableThrowsWhenNameMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.conditional("{is_active}", "", true) + ); } // ============================================ @@ -251,7 +293,7 @@ void conditionalVariableWithCustomPlaceholder() { @Test @DisplayName("image() should create variable with URL") void imageVariableWithUrl() { - TemplateVariable variable = TemplateVariable.image("logo", "https://example.com/logo.png"); + TemplateVariable variable = TemplateVariable.image("{logo}", "logo", "https://example.com/logo.png"); assertEquals("{logo}", variable.getPlaceholder()); assertEquals("logo", variable.getName()); @@ -263,12 +305,36 @@ void imageVariableWithUrl() { @DisplayName("image() should create variable with base64") void imageVariableWithBase64() { String base64Image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."; - TemplateVariable variable = TemplateVariable.image("signature", base64Image); + TemplateVariable variable = TemplateVariable.image("{signature}", "signature", base64Image); assertEquals(base64Image, variable.getValue()); assertEquals("image", variable.getMimeType()); } + @Test + @DisplayName("image() should throw error when placeholder is missing") + void imageVariableThrowsWhenPlaceholderMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.image("", "logo", "https://example.com/logo.png") + ); + } + + @Test + @DisplayName("image() should throw error when name is missing") + void imageVariableThrowsWhenNameMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.image("{logo}", "", "https://example.com/logo.png") + ); + } + + @Test + @DisplayName("image() should throw error when imageUrl is missing") + void imageVariableThrowsWhenImageUrlMissing() { + assertThrows(IllegalArgumentException.class, () -> + TemplateVariable.image("{logo}", "logo", "") + ); + } + @Test @DisplayName("image() should use custom placeholder when provided") void imageVariableWithCustomPlaceholder() { @@ -387,7 +453,7 @@ void generateDocumentWithNestedObjectVariables() throws Exception { .name("Nested Document") .description("Document with nested objects") .variables(Collections.singletonList( - TemplateVariable.nested("user", user) + TemplateVariable.advancedEngine("user", user) )) .build(); @@ -443,7 +509,7 @@ void generateDocumentWithHelperCreatedVariables() throws Exception { .description("Document using helper functions") .variables(Arrays.asList( TemplateVariable.simple("title", "Quarterly Report"), - TemplateVariable.nested("company", Map.of("name", "Company XYZ", "employees", 500)), + TemplateVariable.advancedEngine("company", Map.of("name", "Company XYZ", "employees", 500)), TemplateVariable.loop("departments", Arrays.asList(Map.of("name", "Dept A"), Map.of("name", "Dept B"))), TemplateVariable.conditional("show_financials", true), TemplateVariable.image("logo", "https://example.com/logo.png") From d789c9c5839ca517e48d7ae54d6700e7bbbb46b9 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:29:25 +0000 Subject: [PATCH 13/39] docs: update readme for sdks Signed-off-by: Kushal --- packages/go-sdk/README.md | 4 ++-- packages/java-sdk/README.md | 4 ++-- packages/js-sdk/README.md | 4 ++-- packages/py-sdk/README.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/go-sdk/README.md b/packages/go-sdk/README.md index 3dd8e9c..de6f61b 100644 --- a/packages/go-sdk/README.md +++ b/packages/go-sdk/README.md @@ -269,8 +269,8 @@ result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateReq turbodocx.NewSimpleVariable("invoice_number", "INV-2024-001"), turbodocx.NewSimpleVariable("total", 1500), - // Nested objects (access with dot notation: {customer.name}, {customer.address.city}) - turbodocx.NewNestedVariable("customer", map[string]interface{}{ + // Advanced engine variable (access with dot notation: {customer.name}, {customer.address.city}) + turbodocx.NewAdvancedEngineVariable("customer", map[string]interface{}{ "name": "Acme Corp", "email": "billing@acme.com", "address": map[string]interface{}{ diff --git a/packages/java-sdk/README.md b/packages/java-sdk/README.md index ed406d7..d38e567 100644 --- a/packages/java-sdk/README.md +++ b/packages/java-sdk/README.md @@ -305,8 +305,8 @@ GenerateTemplateResponse result = client.turboTemplate().generate( TemplateVariable.simple("invoice_number", "INV-2024-001"), TemplateVariable.simple("total", 1500), - // Nested objects (access with dot notation: {customer.name}, {customer.address.city}) - TemplateVariable.nested("customer", Map.of( + // Advanced engine variable (access with dot notation: {customer.name}, {customer.address.city}) + TemplateVariable.advancedEngine("customer", Map.of( "name", "Acme Corp", "email", "billing@acme.com", "address", Map.of( diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index 518e548..68d2e26 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -287,8 +287,8 @@ const result = await TurboTemplate.generate({ TurboTemplate.createSimpleVariable('invoice_number', 'INV-2024-001'), TurboTemplate.createSimpleVariable('total', 1500), - // Nested objects (access with dot notation: {customer.name}, {customer.address.city}) - TurboTemplate.createNestedVariable('customer', { + // Advanced engine variable (access with dot notation: {customer.name}, {customer.address.city}) + TurboTemplate.createAdvancedEngineVariable('customer', { name: 'Acme Corp', email: 'billing@acme.com', address: { diff --git a/packages/py-sdk/README.md b/packages/py-sdk/README.md index 704ca28..6e65194 100644 --- a/packages/py-sdk/README.md +++ b/packages/py-sdk/README.md @@ -314,8 +314,8 @@ result = await TurboTemplate.generate({ TurboTemplate.create_simple_variable("invoice_number", "INV-2024-001"), TurboTemplate.create_simple_variable("total", 1500), - # Nested objects (access with dot notation: {customer.name}, {customer.address.city}) - TurboTemplate.create_nested_variable("customer", { + # Advanced engine variable (access with dot notation: {customer.name}, {customer.address.city}) + TurboTemplate.create_advanced_engine_variable("customer", { "name": "Acme Corp", "email": "billing@acme.com", "address": { From 15e909e1454bbdac620bd554ba37a4af7ae828ae Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:30:01 +0000 Subject: [PATCH 14/39] tests: update tests for python and go-sdks Signed-off-by: Kushal --- packages/js-sdk/tests/turbotemplate.test.ts | 140 ++++++++------- packages/py-sdk/tests/test_turbotemplate.py | 179 ++++++++++++-------- 2 files changed, 188 insertions(+), 131 deletions(-) diff --git a/packages/js-sdk/tests/turbotemplate.test.ts b/packages/js-sdk/tests/turbotemplate.test.ts index 2695ecd..f6d3321 100644 --- a/packages/js-sdk/tests/turbotemplate.test.ts +++ b/packages/js-sdk/tests/turbotemplate.test.ts @@ -2,7 +2,7 @@ * TurboTemplate Module Tests * * Tests for advanced templating features: - * - Helper functions (createSimpleVariable, createNestedVariable, etc.) + * - Helper functions (createSimpleVariable, createAdvancedEngineVariable, etc.) * - Variable validation * - Generate template functionality * - Placeholder and name handling @@ -61,60 +61,66 @@ describe('TurboTemplate Module', () => { describe('Helper Functions', () => { describe('createSimpleVariable', () => { - it('should create a simple variable with name and value', () => { - const variable = TurboTemplate.createSimpleVariable('customer_name', 'Person A'); + it('should create a simple variable with placeholder, name, value and mimeType', () => { + const variable = TurboTemplate.createSimpleVariable('{customer_name}', 'customer_name', 'Person A', 'text'); expect(variable).toEqual({ placeholder: '{customer_name}', name: 'customer_name', value: 'Person A', + mimeType: 'text', }); }); it('should create a simple variable with number value', () => { - const variable = TurboTemplate.createSimpleVariable('order_total', 1500); + const variable = TurboTemplate.createSimpleVariable('{order_total}', 'order_total', 1500, 'text'); expect(variable).toEqual({ placeholder: '{order_total}', name: 'order_total', value: 1500, + mimeType: 'text', }); }); it('should create a simple variable with boolean value', () => { - const variable = TurboTemplate.createSimpleVariable('is_active', true); + const variable = TurboTemplate.createSimpleVariable('{is_active}', 'is_active', true, 'text'); expect(variable).toEqual({ placeholder: '{is_active}', name: 'is_active', value: true, + mimeType: 'text', }); }); - it('should use custom placeholder when provided', () => { - const variable = TurboTemplate.createSimpleVariable('customer_name', 'Person A', '{custom_placeholder}'); + it('should create a simple variable with html mimeType', () => { + const variable = TurboTemplate.createSimpleVariable('{content}', 'content', 'Bold', 'html'); expect(variable).toEqual({ - placeholder: '{custom_placeholder}', - name: 'customer_name', - value: 'Person A', + placeholder: '{content}', + name: 'content', + value: 'Bold', + mimeType: 'html', }); }); - it('should handle name that already has curly braces', () => { - const variable = TurboTemplate.createSimpleVariable('{customer_name}', 'Person A'); + it('should throw error when placeholder is missing', () => { + expect(() => TurboTemplate.createSimpleVariable('', 'name', 'value', 'text')).toThrow('placeholder is required'); + }); - expect(variable).toEqual({ - placeholder: '{customer_name}', - name: '{customer_name}', - value: 'Person A', - }); + it('should throw error when name is missing', () => { + expect(() => TurboTemplate.createSimpleVariable('{test}', '', 'value', 'text')).toThrow('name is required'); + }); + + it('should throw error when mimeType is missing', () => { + expect(() => TurboTemplate.createSimpleVariable('{test}', 'test', 'value', '' as any)).toThrow('mimeType is required'); }); }); - describe('createNestedVariable', () => { + describe('createAdvancedEngineVariable', () => { it('should create a nested variable with object value', () => { - const variable = TurboTemplate.createNestedVariable('user', { + const variable = TurboTemplate.createAdvancedEngineVariable('{user}', 'user', { firstName: 'Foo', lastName: 'Bar', email: 'foo@example.com', @@ -134,7 +140,7 @@ describe('TurboTemplate Module', () => { }); it('should create a nested variable with deeply nested object', () => { - const variable = TurboTemplate.createNestedVariable('company', { + const variable = TurboTemplate.createAdvancedEngineVariable('{company}', 'company', { name: 'Company ABC', address: { street: '123 Test Street', @@ -155,17 +161,18 @@ describe('TurboTemplate Module', () => { expect(variable.usesAdvancedTemplatingEngine).toBe(true); }); - it('should use custom placeholder when provided', () => { - const variable = TurboTemplate.createNestedVariable('user', { name: 'Test' }, '{custom_user}'); + it('should throw error when placeholder is missing', () => { + expect(() => TurboTemplate.createAdvancedEngineVariable('', 'user', { name: 'Test' })).toThrow('placeholder is required'); + }); - expect(variable.placeholder).toBe('{custom_user}'); - expect(variable.name).toBe('user'); + it('should throw error when name is missing', () => { + expect(() => TurboTemplate.createAdvancedEngineVariable('{user}', '', { name: 'Test' })).toThrow('name is required'); }); }); describe('createLoopVariable', () => { it('should create a loop variable with array value', () => { - const variable = TurboTemplate.createLoopVariable('items', [ + const variable = TurboTemplate.createLoopVariable('{items}', 'items', [ { name: 'Item A', price: 100 }, { name: 'Item B', price: 200 }, ]); @@ -183,62 +190,67 @@ describe('TurboTemplate Module', () => { }); it('should create a loop variable with empty array', () => { - const variable = TurboTemplate.createLoopVariable('products', []); + const variable = TurboTemplate.createLoopVariable('{products}', 'products', []); expect(variable.value).toEqual([]); expect(variable.mimeType).toBe('json'); }); it('should create a loop variable with primitive array', () => { - const variable = TurboTemplate.createLoopVariable('tags', ['tag1', 'tag2', 'tag3']); + const variable = TurboTemplate.createLoopVariable('{tags}', 'tags', ['tag1', 'tag2', 'tag3']); expect(variable.value).toEqual(['tag1', 'tag2', 'tag3']); }); - it('should use custom placeholder when provided', () => { - const variable = TurboTemplate.createLoopVariable('items', [], '{line_items}'); + it('should throw error when placeholder is missing', () => { + expect(() => TurboTemplate.createLoopVariable('', 'items', [])).toThrow('placeholder is required'); + }); - expect(variable.placeholder).toBe('{line_items}'); - expect(variable.name).toBe('items'); + it('should throw error when name is missing', () => { + expect(() => TurboTemplate.createLoopVariable('{items}', '', [])).toThrow('name is required'); }); }); describe('createConditionalVariable', () => { it('should create a conditional variable with boolean true', () => { - const variable = TurboTemplate.createConditionalVariable('is_premium', true); + const variable = TurboTemplate.createConditionalVariable('{is_premium}', 'is_premium', true); expect(variable).toEqual({ placeholder: '{is_premium}', name: 'is_premium', value: true, + mimeType: 'json', usesAdvancedTemplatingEngine: true, }); }); it('should create a conditional variable with boolean false', () => { - const variable = TurboTemplate.createConditionalVariable('show_discount', false); + const variable = TurboTemplate.createConditionalVariable('{show_discount}', 'show_discount', false); expect(variable.value).toBe(false); + expect(variable.mimeType).toBe('json'); expect(variable.usesAdvancedTemplatingEngine).toBe(true); }); it('should create a conditional variable with truthy value', () => { - const variable = TurboTemplate.createConditionalVariable('count', 5); + const variable = TurboTemplate.createConditionalVariable('{count}', 'count', 5); expect(variable.value).toBe(5); + expect(variable.mimeType).toBe('json'); }); - it('should use custom placeholder when provided', () => { - const variable = TurboTemplate.createConditionalVariable('is_active', true, '{active_flag}'); + it('should throw error when placeholder is missing', () => { + expect(() => TurboTemplate.createConditionalVariable('', 'is_active', true)).toThrow('placeholder is required'); + }); - expect(variable.placeholder).toBe('{active_flag}'); - expect(variable.name).toBe('is_active'); + it('should throw error when name is missing', () => { + expect(() => TurboTemplate.createConditionalVariable('{is_active}', '', true)).toThrow('name is required'); }); }); describe('createImageVariable', () => { it('should create an image variable with URL', () => { - const variable = TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png'); + const variable = TurboTemplate.createImageVariable('{logo}', 'logo', 'https://example.com/logo.png'); expect(variable).toEqual({ placeholder: '{logo}', @@ -250,17 +262,22 @@ describe('TurboTemplate Module', () => { it('should create an image variable with base64', () => { const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...'; - const variable = TurboTemplate.createImageVariable('signature', base64Image); + const variable = TurboTemplate.createImageVariable('{signature}', 'signature', base64Image); expect(variable.value).toBe(base64Image); expect(variable.mimeType).toBe('image'); }); - it('should use custom placeholder when provided', () => { - const variable = TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png', '{company_logo}'); + it('should throw error when placeholder is missing', () => { + expect(() => TurboTemplate.createImageVariable('', 'logo', 'https://example.com/logo.png')).toThrow('placeholder is required'); + }); - expect(variable.placeholder).toBe('{company_logo}'); - expect(variable.name).toBe('logo'); + it('should throw error when name is missing', () => { + expect(() => TurboTemplate.createImageVariable('{logo}', '', 'https://example.com/logo.png')).toThrow('name is required'); + }); + + it('should throw error when imageUrl is missing', () => { + expect(() => TurboTemplate.createImageVariable('{logo}', 'logo', '')).toThrow('imageUrl is required'); }); }); }); @@ -271,6 +288,7 @@ describe('TurboTemplate Module', () => { placeholder: '{name}', name: 'name', value: 'Test', + mimeType: 'text', }); expect(result.isValid).toBe(true); @@ -301,7 +319,8 @@ describe('TurboTemplate Module', () => { placeholder: '{items}', name: 'items', value: [1, 2, 3], - }); + mimeType: 'text', + } as any); expect(result.isValid).toBe(true); expect(result.warnings).toContain('Array values should use mimeType: "json"'); @@ -347,7 +366,8 @@ describe('TurboTemplate Module', () => { placeholder: '{user}', name: 'user', value: { name: 'Test' }, - }); + mimeType: 'text', + } as any); expect(result.isValid).toBe(true); expect(result.warnings).toContain('Complex objects should explicitly set mimeType to "json"'); @@ -370,8 +390,8 @@ describe('TurboTemplate Module', () => { name: 'Test Document', description: 'Test description', variables: [ - { placeholder: '{customer_name}', name: 'customer_name', value: 'Person A' }, - { placeholder: '{order_total}', name: 'order_total', value: 1500 }, + { placeholder: '{customer_name}', name: 'customer_name', value: 'Person A', mimeType: 'text' }, + { placeholder: '{order_total}', name: 'order_total', value: 1500, mimeType: 'text' }, ], }); @@ -510,11 +530,11 @@ describe('TurboTemplate Module', () => { name: 'Helper Document', description: 'Document using helper functions', variables: [ - TurboTemplate.createSimpleVariable('title', 'Quarterly Report'), - TurboTemplate.createNestedVariable('company', { name: 'Company XYZ', employees: 500 }), - TurboTemplate.createLoopVariable('departments', [{ name: 'Dept A' }, { name: 'Dept B' }]), - TurboTemplate.createConditionalVariable('show_financials', true), - TurboTemplate.createImageVariable('logo', 'https://example.com/logo.png'), + TurboTemplate.createSimpleVariable('{title}', 'title', 'Quarterly Report', 'text'), + TurboTemplate.createAdvancedEngineVariable('{company}', 'company', { name: 'Company XYZ', employees: 500 }), + TurboTemplate.createLoopVariable('{departments}', 'departments', [{ name: 'Dept A' }, { name: 'Dept B' }]), + TurboTemplate.createConditionalVariable('{show_financials}', 'show_financials', true), + TurboTemplate.createImageVariable('{logo}', 'logo', 'https://example.com/logo.png'), ], }); @@ -535,7 +555,7 @@ describe('TurboTemplate Module', () => { templateId: 'template-123', name: 'Options Document', description: 'Document with all options', - variables: [{ placeholder: '{test}', name: 'test', value: 'value' }], + variables: [{ placeholder: '{test}', name: 'test', value: 'value', mimeType: 'text' }], replaceFonts: true, defaultFont: 'Arial', outputFormat: 'pdf', @@ -580,7 +600,7 @@ describe('TurboTemplate Module', () => { templateId: 'template-123', name: 'Text Document', description: 'Document using text property', - variables: [{ placeholder: '{legacy}', name: 'legacy', text: 'Legacy value' }], + variables: [{ placeholder: '{legacy}', name: 'legacy', text: 'Legacy value', mimeType: 'text' }], }); expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( @@ -613,7 +633,7 @@ describe('TurboTemplate Module', () => { name: 'Both Fields Document', description: 'Document with both placeholder and name', variables: [ - { placeholder: '{customer}', name: 'customer', value: 'Person A' }, + { placeholder: '{customer}', name: 'customer', value: 'Person A', mimeType: 'text' }, ], }); @@ -636,7 +656,7 @@ describe('TurboTemplate Module', () => { name: 'Distinct Fields Document', description: 'Document with distinct placeholder and name', variables: [ - { placeholder: '{cust_name}', name: 'customerFullName', value: 'Person A' }, + { placeholder: '{cust_name}', name: 'customerFullName', value: 'Person A', mimeType: 'text' }, ], }); @@ -662,7 +682,7 @@ describe('TurboTemplate Module', () => { templateId: 'invalid-template', name: 'Error Document', description: 'Document that should fail', - variables: [{ placeholder: '{test}', name: 'test', value: 'value' }], + variables: [{ placeholder: '{test}', name: 'test', value: 'value', mimeType: 'text' }], }) ).rejects.toEqual(apiError); }); @@ -682,7 +702,7 @@ describe('TurboTemplate Module', () => { templateId: 'template-123', name: 'Validation Error Document', description: 'Document that should fail validation', - variables: [{ placeholder: '{test}', name: 'test', value: '' }], + variables: [{ placeholder: '{test}', name: 'test', value: '', mimeType: 'text' }], }) ).rejects.toEqual(validationError); }); @@ -702,7 +722,7 @@ describe('TurboTemplate Module', () => { templateId: 'template-123', name: 'Rate Limit Document', description: 'Document that should hit rate limit', - variables: [{ placeholder: '{test}', name: 'test', value: 'value' }], + variables: [{ placeholder: '{test}', name: 'test', value: 'value', mimeType: 'text' }], }) ).rejects.toEqual(rateLimitError); }); diff --git a/packages/py-sdk/tests/test_turbotemplate.py b/packages/py-sdk/tests/test_turbotemplate.py index 97207c2..d618927 100644 --- a/packages/py-sdk/tests/test_turbotemplate.py +++ b/packages/py-sdk/tests/test_turbotemplate.py @@ -2,7 +2,7 @@ TurboTemplate Module Tests Tests for advanced templating features: -- Helper functions (create_simple_variable, create_nested_variable, etc.) +- Helper functions (create_simple_variable, create_advanced_engine_variable, etc.) - Variable validation - Generate template functionality - Placeholder and name handling @@ -46,62 +46,83 @@ class TestCreateSimpleVariable: def test_create_simple_variable_with_string_value(self): """Should create a simple variable with string value""" - variable = TurboTemplate.create_simple_variable("customer_name", "Person A") + variable = TurboTemplate.create_simple_variable( + "{customer_name}", "customer_name", "Person A", "text" + ) assert variable == { "placeholder": "{customer_name}", "name": "customer_name", "value": "Person A", + "mimeType": "text", } def test_create_simple_variable_with_number_value(self): """Should create a simple variable with number value""" - variable = TurboTemplate.create_simple_variable("order_total", 1500) + variable = TurboTemplate.create_simple_variable( + "{order_total}", "order_total", 1500, "text" + ) assert variable == { "placeholder": "{order_total}", "name": "order_total", "value": 1500, + "mimeType": "text", } def test_create_simple_variable_with_boolean_value(self): """Should create a simple variable with boolean value""" - variable = TurboTemplate.create_simple_variable("is_active", True) + variable = TurboTemplate.create_simple_variable( + "{is_active}", "is_active", True, "text" + ) assert variable == { "placeholder": "{is_active}", "name": "is_active", "value": True, + "mimeType": "text", } - def test_create_simple_variable_with_custom_placeholder(self): - """Should use custom placeholder when provided""" + def test_create_simple_variable_with_html_mimetype(self): + """Should create a simple variable with html mimeType""" variable = TurboTemplate.create_simple_variable( - "customer_name", "Person A", "{custom_placeholder}" + "{content}", "content", "Bold", "html" ) assert variable == { - "placeholder": "{custom_placeholder}", - "name": "customer_name", - "value": "Person A", + "placeholder": "{content}", + "name": "content", + "value": "Bold", + "mimeType": "html", } - def test_create_simple_variable_with_curly_braces_in_name(self): - """Should handle name that already has curly braces""" - variable = TurboTemplate.create_simple_variable("{customer_name}", "Person A") + def test_create_simple_variable_throws_when_placeholder_missing(self): + """Should throw error when placeholder is missing""" + with pytest.raises(ValueError, match="placeholder is required"): + TurboTemplate.create_simple_variable("", "name", "value", "text") - assert variable == { - "placeholder": "{customer_name}", - "name": "{customer_name}", - "value": "Person A", - } + def test_create_simple_variable_throws_when_name_missing(self): + """Should throw error when name is missing""" + with pytest.raises(ValueError, match="name is required"): + TurboTemplate.create_simple_variable("{test}", "", "value", "text") - class TestCreateNestedVariable: - """Test create_nested_variable helper""" + def test_create_simple_variable_throws_when_mimetype_missing(self): + """Should throw error when mimeType is missing""" + with pytest.raises(ValueError, match="mime_type is required"): + TurboTemplate.create_simple_variable("{test}", "test", "value", "") - def test_create_nested_variable_with_object_value(self): + def test_create_simple_variable_throws_when_mimetype_invalid(self): + """Should throw error when mimeType is invalid""" + with pytest.raises(ValueError, match="mime_type must be 'text' or 'html'"): + TurboTemplate.create_simple_variable("{test}", "test", "value", "json") + + class TestCreateAdvancedEngineVariable: + """Test create_advanced_engine_variable helper""" + + def test_create_advanced_engine_variable_with_object_value(self): """Should create a nested variable with object value""" - variable = TurboTemplate.create_nested_variable( + variable = TurboTemplate.create_advanced_engine_variable( + "{user}", "user", { "firstName": "Foo", @@ -120,9 +141,10 @@ def test_create_nested_variable_with_object_value(self): assert variable["mimeType"] == "json" assert variable["usesAdvancedTemplatingEngine"] is True - def test_create_nested_variable_with_deeply_nested_object(self): + def test_create_advanced_engine_variable_with_deeply_nested_object(self): """Should create a nested variable with deeply nested object""" - variable = TurboTemplate.create_nested_variable( + variable = TurboTemplate.create_advanced_engine_variable( + "{company}", "company", { "name": "Company ABC", @@ -145,14 +167,15 @@ def test_create_nested_variable_with_deeply_nested_object(self): assert variable["mimeType"] == "json" assert variable["usesAdvancedTemplatingEngine"] is True - def test_create_nested_variable_with_custom_placeholder(self): - """Should use custom placeholder when provided""" - variable = TurboTemplate.create_nested_variable( - "user", {"name": "Test"}, "{custom_user}" - ) + def test_create_advanced_engine_variable_throws_when_placeholder_missing(self): + """Should throw error when placeholder is missing""" + with pytest.raises(ValueError, match="placeholder is required"): + TurboTemplate.create_advanced_engine_variable("", "user", {"name": "Test"}) - assert variable["placeholder"] == "{custom_user}" - assert variable["name"] == "user" + def test_create_advanced_engine_variable_throws_when_name_missing(self): + """Should throw error when name is missing""" + with pytest.raises(ValueError, match="name is required"): + TurboTemplate.create_advanced_engine_variable("{user}", "", {"name": "Test"}) class TestCreateLoopVariable: """Test create_loop_variable helper""" @@ -160,6 +183,7 @@ class TestCreateLoopVariable: def test_create_loop_variable_with_array_value(self): """Should create a loop variable with array value""" variable = TurboTemplate.create_loop_variable( + "{items}", "items", [ {"name": "Item A", "price": 100}, @@ -178,59 +202,66 @@ def test_create_loop_variable_with_array_value(self): def test_create_loop_variable_with_empty_array(self): """Should create a loop variable with empty array""" - variable = TurboTemplate.create_loop_variable("products", []) + variable = TurboTemplate.create_loop_variable("{products}", "products", []) assert variable["value"] == [] assert variable["mimeType"] == "json" def test_create_loop_variable_with_primitive_array(self): """Should create a loop variable with primitive array""" - variable = TurboTemplate.create_loop_variable("tags", ["tag1", "tag2", "tag3"]) + variable = TurboTemplate.create_loop_variable("{tags}", "tags", ["tag1", "tag2", "tag3"]) assert variable["value"] == ["tag1", "tag2", "tag3"] - def test_create_loop_variable_with_custom_placeholder(self): - """Should use custom placeholder when provided""" - variable = TurboTemplate.create_loop_variable("items", [], "{line_items}") + def test_create_loop_variable_throws_when_placeholder_missing(self): + """Should throw error when placeholder is missing""" + with pytest.raises(ValueError, match="placeholder is required"): + TurboTemplate.create_loop_variable("", "items", []) - assert variable["placeholder"] == "{line_items}" - assert variable["name"] == "items" + def test_create_loop_variable_throws_when_name_missing(self): + """Should throw error when name is missing""" + with pytest.raises(ValueError, match="name is required"): + TurboTemplate.create_loop_variable("{items}", "", []) class TestCreateConditionalVariable: """Test create_conditional_variable helper""" def test_create_conditional_variable_with_boolean_true(self): """Should create a conditional variable with boolean true""" - variable = TurboTemplate.create_conditional_variable("is_premium", True) + variable = TurboTemplate.create_conditional_variable("{is_premium}", "is_premium", True) assert variable == { "placeholder": "{is_premium}", "name": "is_premium", "value": True, + "mimeType": "json", "usesAdvancedTemplatingEngine": True, } def test_create_conditional_variable_with_boolean_false(self): """Should create a conditional variable with boolean false""" - variable = TurboTemplate.create_conditional_variable("show_discount", False) + variable = TurboTemplate.create_conditional_variable("{show_discount}", "show_discount", False) assert variable["value"] is False + assert variable["mimeType"] == "json" assert variable["usesAdvancedTemplatingEngine"] is True def test_create_conditional_variable_with_truthy_value(self): """Should create a conditional variable with truthy value""" - variable = TurboTemplate.create_conditional_variable("count", 5) + variable = TurboTemplate.create_conditional_variable("{count}", "count", 5) assert variable["value"] == 5 + assert variable["mimeType"] == "json" - def test_create_conditional_variable_with_custom_placeholder(self): - """Should use custom placeholder when provided""" - variable = TurboTemplate.create_conditional_variable( - "is_active", True, "{active_flag}" - ) + def test_create_conditional_variable_throws_when_placeholder_missing(self): + """Should throw error when placeholder is missing""" + with pytest.raises(ValueError, match="placeholder is required"): + TurboTemplate.create_conditional_variable("", "is_active", True) - assert variable["placeholder"] == "{active_flag}" - assert variable["name"] == "is_active" + def test_create_conditional_variable_throws_when_name_missing(self): + """Should throw error when name is missing""" + with pytest.raises(ValueError, match="name is required"): + TurboTemplate.create_conditional_variable("{is_active}", "", True) class TestCreateImageVariable: """Test create_image_variable helper""" @@ -238,7 +269,7 @@ class TestCreateImageVariable: def test_create_image_variable_with_url(self): """Should create an image variable with URL""" variable = TurboTemplate.create_image_variable( - "logo", "https://example.com/logo.png" + "{logo}", "logo", "https://example.com/logo.png" ) assert variable == { @@ -251,19 +282,25 @@ def test_create_image_variable_with_url(self): def test_create_image_variable_with_base64(self): """Should create an image variable with base64""" base64_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." - variable = TurboTemplate.create_image_variable("signature", base64_image) + variable = TurboTemplate.create_image_variable("{signature}", "signature", base64_image) assert variable["value"] == base64_image assert variable["mimeType"] == "image" - def test_create_image_variable_with_custom_placeholder(self): - """Should use custom placeholder when provided""" - variable = TurboTemplate.create_image_variable( - "logo", "https://example.com/logo.png", "{company_logo}" - ) + def test_create_image_variable_throws_when_placeholder_missing(self): + """Should throw error when placeholder is missing""" + with pytest.raises(ValueError, match="placeholder is required"): + TurboTemplate.create_image_variable("", "logo", "https://example.com/logo.png") + + def test_create_image_variable_throws_when_name_missing(self): + """Should throw error when name is missing""" + with pytest.raises(ValueError, match="name is required"): + TurboTemplate.create_image_variable("{logo}", "", "https://example.com/logo.png") - assert variable["placeholder"] == "{company_logo}" - assert variable["name"] == "logo" + def test_create_image_variable_throws_when_imageurl_missing(self): + """Should throw error when imageUrl is missing""" + with pytest.raises(ValueError, match="image_url is required"): + TurboTemplate.create_image_variable("{logo}", "logo", "") class TestValidateVariable: @@ -371,8 +408,8 @@ async def test_generate_document_with_simple_variables(self): "name": "Test Document", "description": "Test description", "variables": [ - {"placeholder": "{customer_name}", "name": "customer_name", "value": "Person A"}, - {"placeholder": "{order_total}", "name": "order_total", "value": 1500}, + {"placeholder": "{customer_name}", "name": "customer_name", "value": "Person A", "mimeType": "text"}, + {"placeholder": "{order_total}", "name": "order_total", "value": 1500, "mimeType": "text"}, ], } ) @@ -483,15 +520,15 @@ async def test_generate_document_with_helper_created_variables(self): "name": "Helper Document", "description": "Document using helper functions", "variables": [ - TurboTemplate.create_simple_variable("title", "Quarterly Report"), - TurboTemplate.create_nested_variable( - "company", {"name": "Company XYZ", "employees": 500} + TurboTemplate.create_simple_variable("{title}", "title", "Quarterly Report", "text"), + TurboTemplate.create_advanced_engine_variable( + "{company}", "company", {"name": "Company XYZ", "employees": 500} ), TurboTemplate.create_loop_variable( - "departments", [{"name": "Dept A"}, {"name": "Dept B"}] + "{departments}", "departments", [{"name": "Dept A"}, {"name": "Dept B"}] ), - TurboTemplate.create_conditional_variable("show_financials", True), - TurboTemplate.create_image_variable("logo", "https://example.com/logo.png"), + TurboTemplate.create_conditional_variable("{show_financials}", "show_financials", True), + TurboTemplate.create_image_variable("{logo}", "logo", "https://example.com/logo.png"), ], } ) @@ -515,7 +552,7 @@ async def test_generate_includes_optional_request_parameters(self): "templateId": "template-123", "name": "Options Document", "description": "Document with all options", - "variables": [{"placeholder": "{test}", "name": "test", "value": "value"}], + "variables": [{"placeholder": "{test}", "name": "test", "value": "value", "mimeType": "text"}], "replaceFonts": True, "defaultFont": "Arial", "outputFormat": "pdf", @@ -564,7 +601,7 @@ async def test_generate_handles_text_property_as_fallback(self): "templateId": "template-123", "name": "Text Document", "description": "Document using text property", - "variables": [{"placeholder": "{legacy}", "name": "legacy", "text": "Legacy value"}], + "variables": [{"placeholder": "{legacy}", "name": "legacy", "text": "Legacy value", "mimeType": "text"}], } ) @@ -598,7 +635,7 @@ async def test_require_both_placeholder_and_name_in_generated_request(self): "name": "Both Fields Document", "description": "Document with both placeholder and name", "variables": [ - {"placeholder": "{customer}", "name": "customer", "value": "Person A"} + {"placeholder": "{customer}", "name": "customer", "value": "Person A", "mimeType": "text"} ], } ) @@ -625,7 +662,7 @@ async def test_allow_distinct_placeholder_and_name_values(self): "name": "Distinct Fields Document", "description": "Document with distinct placeholder and name", "variables": [ - {"placeholder": "{cust_name}", "name": "customerFullName", "value": "Person A"} + {"placeholder": "{cust_name}", "name": "customerFullName", "value": "Person A", "mimeType": "text"} ], } ) @@ -653,7 +690,7 @@ async def test_throw_error_when_not_configured(self): "templateId": "template-123", "name": "Test", "description": "Test", - "variables": [{"placeholder": "{test}", "name": "test", "value": "value"}], + "variables": [{"placeholder": "{test}", "name": "test", "value": "value", "mimeType": "text"}], } ) @@ -674,6 +711,6 @@ async def test_handle_api_errors_gracefully(self): "templateId": "invalid-template", "name": "Error Document", "description": "Document that should fail", - "variables": [{"placeholder": "{test}", "name": "test", "value": "value"}], + "variables": [{"placeholder": "{test}", "name": "test", "value": "value", "mimeType": "text"}], } ) From 4c8718baba317ef51c7ff7487e93d26023fa04fa Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:36:11 +0000 Subject: [PATCH 15/39] tests: update placeholder and name tests for variables Signed-off-by: Kushal --- packages/js-sdk/tests/turbotemplate.test.ts | 4 ++-- packages/py-sdk/tests/test_turbotemplate.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/js-sdk/tests/turbotemplate.test.ts b/packages/js-sdk/tests/turbotemplate.test.ts index f6d3321..a2f78e6 100644 --- a/packages/js-sdk/tests/turbotemplate.test.ts +++ b/packages/js-sdk/tests/turbotemplate.test.ts @@ -295,13 +295,13 @@ describe('TurboTemplate Module', () => { expect(result.errors).toBeUndefined(); }); - it('should return error when placeholder and name are both missing', () => { + it('should return error when placeholder or name is missing', () => { const result = TurboTemplate.validateVariable({ value: 'Test', } as any); expect(result.isValid).toBe(false); - expect(result.errors).toContain('Variable must have either "placeholder" or "name" property'); + expect(result.errors).toContain('Variable must have both "placeholder" and "name" properties'); }); it('should return error when value and text are both missing', () => { diff --git a/packages/py-sdk/tests/test_turbotemplate.py b/packages/py-sdk/tests/test_turbotemplate.py index d618927..89ddeb8 100644 --- a/packages/py-sdk/tests/test_turbotemplate.py +++ b/packages/py-sdk/tests/test_turbotemplate.py @@ -315,12 +315,12 @@ def test_validate_correct_simple_variable(self): assert result["isValid"] is True assert result["errors"] is None - def test_error_when_placeholder_and_name_missing(self): - """Should return error when placeholder and name are both missing""" + def test_error_when_placeholder_or_name_missing(self): + """Should return error when placeholder or name is missing""" result = TurboTemplate.validate_variable({"value": "Test"}) assert result["isValid"] is False - assert 'Variable must have either "placeholder" or "name" property' in result["errors"] + assert 'Variable must have both "placeholder" and "name" properties' in result["errors"] def test_error_when_value_and_text_missing(self): """Should return error when value and text are both missing""" From 9307738e7a6e5661f95d63acfcf84d69391879f0 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sat, 24 Jan 2026 07:36:31 +0000 Subject: [PATCH 16/39] chore: update version Signed-off-by: Kushal --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b38b35f..3922083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3846,7 +3846,7 @@ }, "packages/js-sdk": { "name": "@turbodocx/sdk", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", From af2dfd2cf4bc7aa36bf46196f81c5586ebb5ae53 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 08:53:54 +0000 Subject: [PATCH 17/39] fix: handle null/undefined values in generate Signed-off-by: Kushal --- packages/js-sdk/src/modules/template.ts | 29 +++---- packages/js-sdk/tests/turbotemplate.test.ts | 93 ++++++++++++++++++--- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts index 7504c17..428290d 100644 --- a/packages/js-sdk/src/modules/template.ts +++ b/packages/js-sdk/src/modules/template.ts @@ -129,21 +129,20 @@ export class TurboTemplate { name: v.name, }; + // Add value/text if present - allow null/undefined values + if ('value' in v) { + variable.value = v.value; + } + if ('text' in v) { + variable.text = v.text; + } + // mimeType is required if (!v.mimeType) { throw new Error(`Variable "${variable.placeholder}" must have a 'mimeType' property`); } variable.mimeType = v.mimeType; - // Handle value - keep objects/arrays as-is for JSON serialization - if (v.value !== undefined && v.value !== null) { - variable.value = v.value; - } else if (v.text !== undefined && v.text !== null) { - variable.text = v.text; - } else { - throw new Error(`Variable "${variable.placeholder}" must have either 'value' or 'text' property`); - } - // Add advanced templating flags if specified if (v.usesAdvancedTemplatingEngine != null) { variable.usesAdvancedTemplatingEngine = v.usesAdvancedTemplatingEngine; @@ -193,17 +192,11 @@ export class TurboTemplate { errors.push('Variable must have both "placeholder" and "name" properties'); } - // Check value/text - const hasValue = variable.value !== undefined && variable.value !== null; - const hasText = variable.text !== undefined && variable.text !== null; - - if (!hasValue && !hasText) { - errors.push('Variable must have either "value" or "text" property'); - } + // Check value/text - allow null/undefined values, don't enforce either property // Check advanced templating settings - if (variable.mimeType === 'json' || (typeof variable.value === 'object' && variable.value !== null)) { - if (!variable.mimeType) { + if (typeof variable.value === 'object' && variable.value !== null && !Array.isArray(variable.value)) { + if (variable.mimeType !== 'json') { warnings.push('Complex objects should explicitly set mimeType to "json"'); } } diff --git a/packages/js-sdk/tests/turbotemplate.test.ts b/packages/js-sdk/tests/turbotemplate.test.ts index a2f78e6..b999cc7 100644 --- a/packages/js-sdk/tests/turbotemplate.test.ts +++ b/packages/js-sdk/tests/turbotemplate.test.ts @@ -304,14 +304,13 @@ describe('TurboTemplate Module', () => { expect(result.errors).toContain('Variable must have both "placeholder" and "name" properties'); }); - it('should return error when value and text are both missing', () => { + it('should allow variable without value and text properties', () => { const result = TurboTemplate.validateVariable({ placeholder: '{name}', name: 'name', } as any); - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Variable must have either "value" or "text" property'); + expect(result.isValid).toBe(true); }); it('should warn about array without json mimeType', () => { @@ -573,18 +572,24 @@ describe('TurboTemplate Module', () => { ); }); - it('should throw error when variable has no value or text', async () => { - MockedHttpClient.prototype.post = jest.fn(); + it('should allow variable with no value or text property', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-no-value', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); TurboTemplate.configure({ apiKey: 'test-key' }); - await expect( - TurboTemplate.generate({ - templateId: 'template-123', - name: 'Error Document', - description: 'Document that should fail', - variables: [{ placeholder: '{test}', name: 'test' } as any], - }) - ).rejects.toThrow('Variable "{test}" must have either \'value\' or \'text\' property'); + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'No Value Document', + description: 'Document with variable that has no value/text', + variables: [{ placeholder: '{test}', name: 'test', mimeType: 'text' } as any], + }); + + expect(result.success).toBe(true); + expect(result.deliverableId).toBe('doc-no-value'); }); it('should handle text property as fallback', async () => { @@ -616,6 +621,68 @@ describe('TurboTemplate Module', () => { }) ); }); + + it('should allow variable with null value', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-null', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Null Value Document', + description: 'Document with null value', + variables: [{ placeholder: '{test}', name: 'test', value: null, mimeType: 'text' }], + }); + + expect(result.success).toBe(true); + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + variables: expect.arrayContaining([ + expect.objectContaining({ + placeholder: '{test}', + name: 'test', + value: null, + }), + ]), + }) + ); + }); + + it('should allow variable with undefined value', async () => { + const mockResponse = { + success: true, + deliverableId: 'doc-undefined', + }; + + MockedHttpClient.prototype.post = jest.fn().mockResolvedValue(mockResponse); + TurboTemplate.configure({ apiKey: 'test-key' }); + + const result = await TurboTemplate.generate({ + templateId: 'template-123', + name: 'Undefined Value Document', + description: 'Document with undefined value', + variables: [{ placeholder: '{test}', name: 'test', value: undefined, mimeType: 'text' }], + }); + + expect(result.success).toBe(true); + expect(MockedHttpClient.prototype.post).toHaveBeenCalledWith( + '/v1/deliverable', + expect.objectContaining({ + variables: expect.arrayContaining([ + expect.objectContaining({ + placeholder: '{test}', + name: 'test', + value: undefined, + }), + ]), + }) + ); + }); }); describe('Placeholder and Name Handling', () => { From e1ba80833b130910469e4f6c8182018a0cc52d8f Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 08:56:16 +0000 Subject: [PATCH 18/39] fix: allow null/undefined values for variables Signed-off-by: Kushal --- .../src/turbodocx_sdk/modules/template.py | 22 +++-- packages/py-sdk/tests/test_turbotemplate.py | 86 +++++++++++++------ 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py index 52f1a33..610986d 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -31,6 +31,8 @@ def configure( access_token: Optional[str] = None, base_url: str = "https://api.turbodocx.com", org_id: Optional[str] = None, + sender_email: Optional[str] = None, + sender_name: Optional[str] = None, ) -> None: """ Configure the TurboTemplate module with API credentials @@ -40,6 +42,8 @@ def configure( access_token: OAuth2 access token (alternative to API key) base_url: Base URL for the API (optional, defaults to https://api.turbodocx.com) org_id: Organization ID (required) + sender_email: Reply-to email address for signature requests (optional for template generation). + sender_name: Sender name for signature requests (optional for template generation). Example: >>> TurboTemplate.configure( @@ -52,6 +56,8 @@ def configure( access_token=access_token, base_url=base_url, org_id=org_id, + sender_email=sender_email, + sender_name=sender_name, ) @classmethod @@ -157,14 +163,11 @@ async def generate(cls, request: GenerateTemplateRequest) -> GenerateTemplateRes variable["mimeType"] = v["mimeType"] # Handle value - keep objects/arrays as-is for JSON serialization - if "value" in v and v["value"] is not None: + # Allow null/None values, don't require either property + if "value" in v: variable["value"] = v["value"] - elif "text" in v and v["text"] is not None: + if "text" in v: variable["text"] = v["text"] - else: - raise ValueError( - f'Variable "{variable["placeholder"]}" must have either "value" or "text" property' - ) # Add advanced templating flags if specified if "usesAdvancedTemplatingEngine" in v: @@ -221,12 +224,7 @@ def validate_variable(cls, variable: TemplateVariable) -> VariableValidation: if not variable.get("placeholder") or not variable.get("name"): errors.append('Variable must have both "placeholder" and "name" properties') - # Check value/text - has_value = "value" in variable and variable["value"] is not None - has_text = "text" in variable and variable["text"] is not None - - if not has_value and not has_text: - errors.append('Variable must have either "value" or "text" property') + # Check value/text - allow None values, don't enforce either property # Check advanced templating settings mime_type = variable.get("mimeType") diff --git a/packages/py-sdk/tests/test_turbotemplate.py b/packages/py-sdk/tests/test_turbotemplate.py index 89ddeb8..6e4d700 100644 --- a/packages/py-sdk/tests/test_turbotemplate.py +++ b/packages/py-sdk/tests/test_turbotemplate.py @@ -23,7 +23,11 @@ def setup(self): def test_configure_with_api_key_and_org_id(self): """Should configure the client with API key and org ID""" - TurboTemplate.configure(api_key="test-api-key", org_id="test-org-id") + TurboTemplate.configure( + api_key="test-api-key", + org_id="test-org-id", + sender_email="test@company.com" + ) assert TurboTemplate._client is not None assert TurboTemplate._client.api_key == "test-api-key" assert TurboTemplate._client.org_id == "test-org-id" @@ -33,6 +37,7 @@ def test_configure_with_custom_base_url(self): TurboTemplate.configure( api_key="test-api-key", org_id="test-org-id", + sender_email="test@company.com", base_url="https://custom-api.example.com", ) assert TurboTemplate._client.base_url == "https://custom-api.example.com" @@ -322,12 +327,11 @@ def test_error_when_placeholder_or_name_missing(self): assert result["isValid"] is False assert 'Variable must have both "placeholder" and "name" properties' in result["errors"] - def test_error_when_value_and_text_missing(self): - """Should return error when value and text are both missing""" + def test_allow_variable_without_value_and_text(self): + """Should allow variable without value or text property""" result = TurboTemplate.validate_variable({"placeholder": "{name}", "name": "name"}) - assert result["isValid"] is False - assert 'Variable must have either "value" or "text" property' in result["errors"] + assert result["isValid"] is True def test_warn_about_array_without_json_mimetype(self): """Should warn about array without json mimeType""" @@ -401,7 +405,7 @@ async def test_generate_document_with_simple_variables(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") result = await TurboTemplate.generate( { "templateId": "template-123", @@ -438,7 +442,7 @@ async def test_generate_document_with_nested_object_variables(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") result = await TurboTemplate.generate( { "templateId": "template-123", @@ -476,7 +480,7 @@ async def test_generate_document_with_loop_array_variables(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") result = await TurboTemplate.generate( { "templateId": "template-123", @@ -513,7 +517,7 @@ async def test_generate_document_with_helper_created_variables(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") result = await TurboTemplate.generate( { "templateId": "template-123", @@ -546,7 +550,7 @@ async def test_generate_includes_optional_request_parameters(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") await TurboTemplate.generate( { "templateId": "template-123", @@ -568,22 +572,27 @@ async def test_generate_includes_optional_request_parameters(self): assert body["metadata"] == {"customField": "value"} @pytest.mark.asyncio - async def test_generate_throws_error_when_variable_has_no_value_or_text(self): - """Should throw error when variable has no value or text""" + async def test_generate_allows_variable_with_no_value_or_text(self): + """Should allow variable with no value or text property""" + mock_response = {"success": True, "deliverableId": "doc-no-value"} + with patch.object(TurboTemplate, "_get_client") as mock_get_client: mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") - with pytest.raises(ValueError, match='must have either "value" or "text" property'): - await TurboTemplate.generate( - { - "templateId": "template-123", - "name": "Error Document", - "description": "Document that should fail", - "variables": [{"placeholder": "{test}", "name": "test"}], - } - ) + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") + result = await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "No Value Document", + "description": "Document with variable that has no value/text", + "variables": [{"placeholder": "{test}", "name": "test", "mimeType": "text"}], + } + ) + + assert result["success"] is True + assert result["deliverableId"] == "doc-no-value" @pytest.mark.asyncio async def test_generate_handles_text_property_as_fallback(self): @@ -595,7 +604,7 @@ async def test_generate_handles_text_property_as_fallback(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") await TurboTemplate.generate( { "templateId": "template-123", @@ -609,6 +618,31 @@ async def test_generate_handles_text_property_as_fallback(self): body = call_args[1]["json"] assert body["variables"][0]["text"] == "Legacy value" + @pytest.mark.asyncio + async def test_generate_allows_null_value(self): + """Should allow variable with None/null value""" + mock_response = {"success": True, "deliverableId": "doc-null"} + + with patch.object(TurboTemplate, "_get_client") as mock_get_client: + mock_client = MagicMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_get_client.return_value = mock_client + + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") + result = await TurboTemplate.generate( + { + "templateId": "template-123", + "name": "Null Value Document", + "description": "Document with None value", + "variables": [{"placeholder": "{test}", "name": "test", "value": None, "mimeType": "text"}], + } + ) + + assert result["success"] is True + call_args = mock_client.post.call_args + body = call_args[1]["json"] + assert body["variables"][0]["value"] is None + class TestPlaceholderAndNameHandling: """Test placeholder and name handling""" @@ -628,7 +662,7 @@ async def test_require_both_placeholder_and_name_in_generated_request(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") await TurboTemplate.generate( { "templateId": "template-123", @@ -655,7 +689,7 @@ async def test_allow_distinct_placeholder_and_name_values(self): mock_client.post = AsyncMock(return_value=mock_response) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") await TurboTemplate.generate( { "templateId": "template-123", @@ -704,7 +738,7 @@ async def test_handle_api_errors_gracefully(self): mock_client.post = AsyncMock(side_effect=api_error) mock_get_client.return_value = mock_client - TurboTemplate.configure(api_key="test-key", org_id="test-org") + TurboTemplate.configure(api_key="test-key", org_id="test-org", sender_email="test@company.com") with pytest.raises(Exception, match="Template not found"): await TurboTemplate.generate( { From 8536a5ca3f2a70000667113d2bca54608994eeb1 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 08:57:14 +0000 Subject: [PATCH 19/39] fix: handle null/undefined values for go sdk Signed-off-by: Kushal --- .../go-sdk/examples/advanced_templating.go | 2 +- packages/go-sdk/turbotemplate.go | 6 +-- packages/go-sdk/turbotemplate_test.go | 48 +++++++++++++++++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/go-sdk/examples/advanced_templating.go b/packages/go-sdk/examples/advanced_templating.go index 53fb51a..e48d914 100644 --- a/packages/go-sdk/examples/advanced_templating.go +++ b/packages/go-sdk/examples/advanced_templating.go @@ -6,7 +6,7 @@ import ( "log" "os" - "github.com/turbodocx/sdk/packages/go-sdk" + turbodocx "github.com/TurboDocx/SDK/packages/go-sdk" ) func main() { diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index 219374f..975d8e3 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -189,9 +189,9 @@ func (c *TurboTemplateClient) Generate(ctx context.Context, req *GenerateTemplat if v.MimeType == "" { return nil, fmt.Errorf("variable %d (%s) must have MimeType", i, v.Placeholder) } - if v.Value == nil && (v.Text == nil || *v.Text == "") { - return nil, fmt.Errorf("variable %d (%s) must have either Value or Text", i, v.Placeholder) - } + // Allow nil/null values - just check that at least one field is set + // Note: We cannot distinguish between "field not set" vs "field set to nil" in Go + // So we accept the variable as long as it has been initialized with either field } // Marshal request to JSON diff --git a/packages/go-sdk/turbotemplate_test.go b/packages/go-sdk/turbotemplate_test.go index 37cefaf..711db60 100644 --- a/packages/go-sdk/turbotemplate_test.go +++ b/packages/go-sdk/turbotemplate_test.go @@ -508,20 +508,58 @@ func TestTurboTemplateClient_Generate(t *testing.T) { assert.Contains(t, err.Error(), "variables are required") }) - t.Run("returns error when variable has no value or text", func(t *testing.T) { + t.Run("allows variable with no value or text", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-no-value", + }) + })) + defer server.Close() + client, _ := NewClientWithConfig(ClientConfig{ APIKey: "test-api-key", OrgID: "test-org-id", + BaseURL: server.URL, SenderEmail: "test@example.com", }) - _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + mimeType := MimeTypeText + result, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ TemplateID: "template-123", - Variables: []TemplateVariable{{Placeholder: "{test}", Name: "test"}}, + Variables: []TemplateVariable{{Placeholder: "{test}", Name: "test", MimeType: mimeType}}, }) - require.Error(t, err) - assert.Contains(t, err.Error(), "must have either Value or Text") + require.NoError(t, err) + assert.True(t, result.Success) + }) + + t.Run("allows variable with nil value", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "deliverableId": "doc-nil-value", + }) + })) + defer server.Close() + + client, _ := NewClientWithConfig(ClientConfig{ + APIKey: "test-api-key", + OrgID: "test-org-id", + BaseURL: server.URL, + SenderEmail: "test@example.com", + }) + + mimeType := MimeTypeText + result, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ + TemplateID: "template-123", + Variables: []TemplateVariable{{Placeholder: "{test}", Name: "test", Value: nil, MimeType: mimeType}}, + }) + + require.NoError(t, err) + assert.True(t, result.Success) }) t.Run("returns error when placeholder is missing", func(t *testing.T) { From 142778580b3a1b771dbfbe0b929e80caed785637 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:07:15 +0000 Subject: [PATCH 20/39] fix: http client call Signed-off-by: Kushal --- packages/go-sdk/turbotemplate.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index 975d8e3..f0ce0d2 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -1,9 +1,7 @@ package turbodocx import ( - "bytes" "context" - "encoding/json" "fmt" ) @@ -194,24 +192,12 @@ func (c *TurboTemplateClient) Generate(ctx context.Context, req *GenerateTemplat // So we accept the variable as long as it has been initialized with either field } - // Marshal request to JSON - body, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - // Make request - resp, err := c.httpClient.Post(ctx, "/v1/deliverable", "application/json", bytes.NewReader(body)) + var result GenerateTemplateResponse + err := c.httpClient.Post(ctx, "/v1/deliverable", req, &result) if err != nil { return nil, err } - defer resp.Body.Close() - - // Parse response - var result GenerateTemplateResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) - } return &result, nil } From 29f91fdb6b5814f602536e66a1b16c7a2683924b Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:25:34 +0000 Subject: [PATCH 21/39] fix: tests for go sdk Signed-off-by: Kushal --- .../go-sdk/examples/advanced_templating.go | 22 +++++++-------- packages/go-sdk/turbotemplate_test.go | 28 ++++++++++++------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/go-sdk/examples/advanced_templating.go b/packages/go-sdk/examples/advanced_templating.go index e48d914..9b1d372 100644 --- a/packages/go-sdk/examples/advanced_templating.go +++ b/packages/go-sdk/examples/advanced_templating.go @@ -73,7 +73,7 @@ func nestedObjects(ctx context.Context, client *turbodocx.Client) { { Placeholder: "{user}", Name: "user", - MimeType: &mimeTypeJSON, + MimeType: mimeTypeJSON, Value: map[string]interface{}{ "name": "Person A", "email": "persona@example.com", @@ -112,7 +112,7 @@ func loopsAndArrays(ctx context.Context, client *turbodocx.Client) { { Placeholder: "{items}", Name: "items", - MimeType: &mimeTypeJSON, + MimeType: mimeTypeJSON, Value: []map[string]interface{}{ {"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}, {"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}, @@ -147,8 +147,8 @@ func conditionals(ctx context.Context, client *turbodocx.Client) { Name: &name, Description: &description, Variables: []turbodocx.TemplateVariable{ - {Placeholder: "{is_premium}", Name: "is_premium", MimeType: &mimeTypeJSON, Value: true}, - {Placeholder: "{discount}", Name: "discount", MimeType: &mimeTypeJSON, Value: 0.2}, + {Placeholder: "{is_premium}", Name: "is_premium", MimeType: mimeTypeJSON, Value: true}, + {Placeholder: "{discount}", Name: "discount", MimeType: mimeTypeJSON, Value: 0.2}, }, }) if err != nil { @@ -175,14 +175,14 @@ func expressionsAndCalculations(ctx context.Context, client *turbodocx.Client) { { Placeholder: "{subtotal}", Name: "subtotal", - MimeType: &mimeTypeText, + MimeType: mimeTypeText, Value: "1000", UsesAdvancedTemplatingEngine: &usesAdvanced, }, { Placeholder: "{tax_rate}", Name: "tax_rate", - MimeType: &mimeTypeText, + MimeType: mimeTypeText, Value: "0.08", UsesAdvancedTemplatingEngine: &usesAdvanced, }, @@ -215,7 +215,7 @@ func complexInvoice(ctx context.Context, client *turbodocx.Client) { { Placeholder: "{customer}", Name: "customer", - MimeType: &mimeTypeJSON, + MimeType: mimeTypeJSON, Value: map[string]interface{}{ "name": "Company ABC", "email": "billing@example.com", @@ -235,7 +235,7 @@ func complexInvoice(ctx context.Context, client *turbodocx.Client) { { Placeholder: "{items}", Name: "items", - MimeType: &mimeTypeJSON, + MimeType: mimeTypeJSON, Value: []map[string]interface{}{ { "description": "Service A", @@ -258,16 +258,16 @@ func complexInvoice(ctx context.Context, client *turbodocx.Client) { { Placeholder: "{tax_rate}", Name: "tax_rate", - MimeType: &mimeTypeText, + MimeType: mimeTypeText, Value: "0.08", UsesAdvancedTemplatingEngine: &usesAdvanced, }, // Premium customer flag - {Placeholder: "{is_premium}", Name: "is_premium", MimeType: &mimeTypeJSON, Value: true}, + {Placeholder: "{is_premium}", Name: "is_premium", MimeType: mimeTypeJSON, Value: true}, { Placeholder: "{premium_discount}", Name: "premium_discount", - MimeType: &mimeTypeText, + MimeType: mimeTypeText, Value: "0.05", UsesAdvancedTemplatingEngine: &usesAdvanced, }, diff --git a/packages/go-sdk/turbotemplate_test.go b/packages/go-sdk/turbotemplate_test.go index 711db60..c6973c5 100644 --- a/packages/go-sdk/turbotemplate_test.go +++ b/packages/go-sdk/turbotemplate_test.go @@ -422,11 +422,11 @@ func TestTurboTemplateClient_Generate(t *testing.T) { Name: &name, Description: &desc, Variables: []TemplateVariable{ - NewSimpleVariable("title", "Quarterly Report"), - NewAdvancedEngineVariable("company", map[string]interface{}{"name": "Company XYZ", "employees": 500}), - NewLoopVariable("departments", []interface{}{map[string]interface{}{"name": "Dept A"}, map[string]interface{}{"name": "Dept B"}}), - NewConditionalVariable("show_financials", true), - NewImageVariable("logo", "https://example.com/logo.png"), + must(NewSimpleVariable("{title}", "title", "Quarterly Report", MimeTypeText)), + must(NewAdvancedEngineVariable("{company}", "company", map[string]interface{}{"name": "Company XYZ", "employees": 500})), + must(NewLoopVariable("{departments}", "departments", []interface{}{map[string]interface{}{"name": "Dept A"}, map[string]interface{}{"name": "Dept B"}})), + must(NewConditionalVariable("{show_financials}", "show_financials", true)), + must(NewImageVariable("{logo}", "logo", "https://example.com/logo.png")), }, }) @@ -467,7 +467,7 @@ func TestTurboTemplateClient_Generate(t *testing.T) { TemplateID: "template-123", Name: &name, Description: &desc, - Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + Variables: []TemplateVariable{must(NewSimpleVariable("{test}", "test", "value", MimeTypeText))}, ReplaceFonts: &replaceFonts, DefaultFont: &defaultFont, OutputFormat: &outputFormat, @@ -485,7 +485,7 @@ func TestTurboTemplateClient_Generate(t *testing.T) { }) _, err := client.TurboTemplate.Generate(context.Background(), &GenerateTemplateRequest{ - Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + Variables: []TemplateVariable{must(NewSimpleVariable("{test}", "test", "value", MimeTypeText))}, }) require.Error(t, err) @@ -694,7 +694,7 @@ func TestTurboTemplateClient_ErrorHandling(t *testing.T) { TemplateID: "invalid-template", Name: &name, Description: &desc, - Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + Variables: []TemplateVariable{must(NewSimpleVariable("{test}", "test", "value", MimeTypeText))}, }) require.Error(t, err) @@ -727,7 +727,7 @@ func TestTurboTemplateClient_ErrorHandling(t *testing.T) { TemplateID: "template-123", Name: &name, Description: &desc, - Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + Variables: []TemplateVariable{must(NewSimpleVariable("{test}", "test", "value", MimeTypeText))}, }) require.Error(t, err) @@ -760,7 +760,7 @@ func TestTurboTemplateClient_ErrorHandling(t *testing.T) { TemplateID: "template-123", Name: &name, Description: &desc, - Variables: []TemplateVariable{NewSimpleVariable("test", "value")}, + Variables: []TemplateVariable{must(NewSimpleVariable("{test}", "test", "value", MimeTypeText))}, }) require.Error(t, err) @@ -769,3 +769,11 @@ func TestTurboTemplateClient_ErrorHandling(t *testing.T) { assert.Equal(t, 429, rateLimitErr.StatusCode) }) } + +// must is a helper function to handle errors in variable creation for tests +func must(v TemplateVariable, err error) TemplateVariable { + if err != nil { + panic(err) + } + return v +} From 01548494a24f40cca79ec393313791115d767d9c Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:30:22 +0000 Subject: [PATCH 22/39] fix: mimeTypeJson value Signed-off-by: Kushal --- packages/go-sdk/turbotemplate_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/go-sdk/turbotemplate_test.go b/packages/go-sdk/turbotemplate_test.go index c6973c5..e57a323 100644 --- a/packages/go-sdk/turbotemplate_test.go +++ b/packages/go-sdk/turbotemplate_test.go @@ -308,7 +308,7 @@ func TestTurboTemplateClient_Generate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var reqBody GenerateTemplateRequest json.NewDecoder(r.Body).Decode(&reqBody) - assert.Equal(t, "json", string(*reqBody.Variables[0].MimeType)) + assert.Equal(t, "json", string(reqBody.Variables[0].MimeType)) assert.True(t, *reqBody.Variables[0].UsesAdvancedTemplatingEngine) w.Header().Set("Content-Type", "application/json") @@ -338,7 +338,7 @@ func TestTurboTemplateClient_Generate(t *testing.T) { { Placeholder: "{user}", Name: "user", - MimeType: &mimeTypeJSON, + MimeType: mimeTypeJSON, Value: map[string]interface{}{"firstName": "Foo", "lastName": "Bar"}, UsesAdvancedTemplatingEngine: &usesAdvanced, }, @@ -383,7 +383,7 @@ func TestTurboTemplateClient_Generate(t *testing.T) { { Placeholder: "{items}", Name: "items", - MimeType: &mimeTypeJSON, + MimeType: mimeTypeJSON, Value: []map[string]interface{}{{"name": "Item A"}, {"name": "Item B"}}, UsesAdvancedTemplatingEngine: &usesAdvanced, }, From 0f36164b5ca5eba8a7551b7b13d061c56c2b983d Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:34:33 +0000 Subject: [PATCH 23/39] tests: pass mimetype during variable creation Signed-off-by: Kushal --- packages/go-sdk/examples/advanced_templating.go | 4 ++++ packages/go-sdk/turbotemplate_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/go-sdk/examples/advanced_templating.go b/packages/go-sdk/examples/advanced_templating.go index 9b1d372..816d3e0 100644 --- a/packages/go-sdk/examples/advanced_templating.go +++ b/packages/go-sdk/examples/advanced_templating.go @@ -30,6 +30,10 @@ func main() { // complexInvoice(ctx, client) // usingHelpers(ctx, client) + // Suppress unused variable warnings when all examples are commented out + _ = client + _ = ctx + fmt.Println("Examples ready to run!") } diff --git a/packages/go-sdk/turbotemplate_test.go b/packages/go-sdk/turbotemplate_test.go index e57a323..da85d51 100644 --- a/packages/go-sdk/turbotemplate_test.go +++ b/packages/go-sdk/turbotemplate_test.go @@ -294,8 +294,8 @@ func TestTurboTemplateClient_Generate(t *testing.T) { Name: &name, Description: &desc, Variables: []TemplateVariable{ - {Placeholder: "{customer_name}", Name: "customer_name", Value: "Person A"}, - {Placeholder: "{order_total}", Name: "order_total", Value: 1500}, + {Placeholder: "{customer_name}", Name: "customer_name", Value: "Person A", MimeType: MimeTypeText}, + {Placeholder: "{order_total}", Name: "order_total", Value: 1500, MimeType: MimeTypeText}, }, }) @@ -625,7 +625,7 @@ func TestTurboTemplateClient_PlaceholderAndNameHandling(t *testing.T) { Name: &name, Description: &desc, Variables: []TemplateVariable{ - {Placeholder: "{customer}", Name: "customer", Value: "Person A"}, + {Placeholder: "{customer}", Name: "customer", Value: "Person A", MimeType: MimeTypeText}, }, }) @@ -661,7 +661,7 @@ func TestTurboTemplateClient_PlaceholderAndNameHandling(t *testing.T) { Name: &name, Description: &desc, Variables: []TemplateVariable{ - {Placeholder: "{cust_name}", Name: "customerFullName", Value: "Person A"}, + {Placeholder: "{cust_name}", Name: "customerFullName", Value: "Person A", MimeType: MimeTypeText}, }, }) From b71583c455fe1eed20acf5560cf5eda829b454f8 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:35:36 +0000 Subject: [PATCH 24/39] refactor: remove JsonProperty decorator Signed-off-by: Kushal --- .../models/GenerateTemplateRequest.java | 9 ----- .../models/GenerateTemplateResponse.java | 8 ----- .../turbodocx/models/TemplateVariable.java | 12 ------- .../java/com/turbodocx/TurboTemplateTest.java | 36 ++++++++++++++----- 4 files changed, 28 insertions(+), 37 deletions(-) diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java index 4f421aa..71fe22e 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateRequest.java @@ -1,6 +1,5 @@ package com.turbodocx.models; -import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Map; @@ -8,28 +7,20 @@ * Request for generating a document from template */ public class GenerateTemplateRequest { - @JsonProperty("templateId") private String templateId; - @JsonProperty("variables") private List variables; - @JsonProperty("name") private String name; - @JsonProperty("description") private String description; - @JsonProperty("replaceFonts") private Boolean replaceFonts; - @JsonProperty("defaultFont") private String defaultFont; - @JsonProperty("outputFormat") private String outputFormat; - @JsonProperty("metadata") private Map metadata; // Constructors diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java index 66ed09d..c959a0b 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/GenerateTemplateResponse.java @@ -1,27 +1,19 @@ package com.turbodocx.models; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * Response from template generation */ public class GenerateTemplateResponse { - @JsonProperty("success") private boolean success; - @JsonProperty("deliverableId") private String deliverableId; - @JsonProperty("buffer") private byte[] buffer; - @JsonProperty("downloadUrl") private String downloadUrl; - @JsonProperty("message") private String message; - @JsonProperty("error") private String error; // Constructors diff --git a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java index 6ba97b5..a73c575 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/models/TemplateVariable.java @@ -1,6 +1,5 @@ package com.turbodocx.models; -import com.fasterxml.jackson.annotation.json.JsonProperty; import java.util.List; import java.util.Map; @@ -10,37 +9,26 @@ * Supports both simple text replacement and advanced templating with Angular-like expressions */ public class TemplateVariable { - @JsonProperty("placeholder") private String placeholder; - @JsonProperty("name") private String name; - @JsonProperty("value") private Object value; - @JsonProperty("text") private String text; - @JsonProperty("mimeType") private String mimeType; - @JsonProperty("usesAdvancedTemplatingEngine") private Boolean usesAdvancedTemplatingEngine; - @JsonProperty("nestedInAdvancedTemplatingEngine") private Boolean nestedInAdvancedTemplatingEngine; - @JsonProperty("allowRichTextInjection") private Boolean allowRichTextInjection; - @JsonProperty("description") private String description; - @JsonProperty("defaultValue") private Boolean defaultValue; - @JsonProperty("subvariables") private List subvariables; // Constructors diff --git a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java index 4b6fbfc..88630b0 100644 --- a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java +++ b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java @@ -371,14 +371,19 @@ void builderThrowsErrorWhenNameMissing() { } @Test - @DisplayName("builder should throw error when value and text are both missing") - void builderThrowsErrorWhenValueAndTextMissing() { - assertThrows(IllegalStateException.class, () -> { - TemplateVariable.builder() - .placeholder("{test}") - .name("test") - .build(); - }); + @DisplayName("builder should allow variable without value or text") + void builderAllowsVariableWithoutValueOrText() { + TemplateVariable variable = TemplateVariable.builder() + .placeholder("{test}") + .name("test") + .mimeType(VariableMimeType.TEXT) + .build(); + + assertNotNull(variable); + assertEquals("{test}", variable.getPlaceholder()); + assertEquals("test", variable.getName()); + assertNull(variable.getValue()); + assertNull(variable.getText()); } @Test @@ -387,12 +392,27 @@ void builderAcceptsTextAsAlternative() { TemplateVariable variable = TemplateVariable.builder() .placeholder("{test}") .name("test") + .mimeType(VariableMimeType.TEXT) .text("text value") .build(); assertEquals("text value", variable.getText()); } + @Test + @DisplayName("builder should allow null value") + void builderAllowsNullValue() { + TemplateVariable variable = TemplateVariable.builder() + .placeholder("{test}") + .name("test") + .value(null) + .mimeType(VariableMimeType.TEXT) + .build(); + + assertNotNull(variable); + assertNull(variable.getValue()); + } + // ============================================ // Generate Tests // ============================================ From 9f9e65f99da6960e2f360318e0a9e2518c4741ff Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:40:29 +0000 Subject: [PATCH 25/39] tests: fix tests for java-sdk Signed-off-by: Kushal --- .../java-sdk/src/main/java/com/turbodocx/TurboTemplate.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java index 6191c93..698cd76 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java @@ -2,6 +2,7 @@ import com.turbodocx.models.GenerateTemplateRequest; import com.turbodocx.models.GenerateTemplateResponse; +import java.io.IOException; /** * TurboTemplate provides document templating operations @@ -73,9 +74,9 @@ public TurboTemplate(HttpClient httpClient) { * * @param request Template ID and variables * @return Generated document response - * @throws TurboDocxException if the request fails + * @throws IOException if the request fails */ - public GenerateTemplateResponse generate(GenerateTemplateRequest request) throws TurboDocxException { + public GenerateTemplateResponse generate(GenerateTemplateRequest request) throws IOException { if (request == null) { throw new IllegalArgumentException("Request cannot be null"); } From b2972e236b2118eb075e722e49e2307f0f10fe62 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:47:57 +0000 Subject: [PATCH 26/39] tests: fix test failures for java-sdk Signed-off-by: Kushal --- .../java/com/turbodocx/TurboTemplateTest.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java index 88630b0..ba523bc 100644 --- a/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java +++ b/packages/java-sdk/src/test/java/com/turbodocx/TurboTemplateTest.java @@ -338,7 +338,7 @@ void imageVariableThrowsWhenImageUrlMissing() { @Test @DisplayName("image() should use custom placeholder when provided") void imageVariableWithCustomPlaceholder() { - TemplateVariable variable = TemplateVariable.image("logo", "https://example.com/logo.png", "{company_logo}"); + TemplateVariable variable = TemplateVariable.image("{company_logo}", "logo", "https://example.com/logo.png"); assertEquals("{company_logo}", variable.getPlaceholder()); assertEquals("logo", variable.getName()); @@ -435,8 +435,8 @@ void generateDocumentWithSimpleVariables() throws Exception { .name("Test Document") .description("Test description") .variables(Arrays.asList( - TemplateVariable.simple("customer_name", "Person A"), - TemplateVariable.simple("order_total", 1500) + TemplateVariable.simple("{customer_name}", "customer_name", "Person A", VariableMimeType.TEXT), + TemplateVariable.simple("{order_total}", "order_total", 1500, VariableMimeType.TEXT) )) .build(); @@ -473,7 +473,7 @@ void generateDocumentWithNestedObjectVariables() throws Exception { .name("Nested Document") .description("Document with nested objects") .variables(Collections.singletonList( - TemplateVariable.advancedEngine("user", user) + TemplateVariable.advancedEngine("{user}", "user", user) )) .build(); @@ -503,7 +503,7 @@ void generateDocumentWithLoopArrayVariables() throws Exception { .name("Loop Document") .description("Document with loops") .variables(Collections.singletonList( - TemplateVariable.loop("items", items) + TemplateVariable.loop("{items}", "items", items) )) .build(); @@ -528,11 +528,11 @@ void generateDocumentWithHelperCreatedVariables() throws Exception { .name("Helper Document") .description("Document using helper functions") .variables(Arrays.asList( - TemplateVariable.simple("title", "Quarterly Report"), - TemplateVariable.advancedEngine("company", Map.of("name", "Company XYZ", "employees", 500)), - TemplateVariable.loop("departments", Arrays.asList(Map.of("name", "Dept A"), Map.of("name", "Dept B"))), - TemplateVariable.conditional("show_financials", true), - TemplateVariable.image("logo", "https://example.com/logo.png") + TemplateVariable.simple("{title}", "title", "Quarterly Report", VariableMimeType.TEXT), + TemplateVariable.advancedEngine("{company}", "company", Map.of("name", "Company XYZ", "employees", 500)), + TemplateVariable.loop("{departments}", "departments", Arrays.asList(Map.of("name", "Dept A"), Map.of("name", "Dept B"))), + TemplateVariable.conditional("{show_financials}", "show_financials", true), + TemplateVariable.image("{logo}", "logo", "https://example.com/logo.png") )) .build(); @@ -557,7 +557,7 @@ void generateIncludesOptionalRequestParameters() throws Exception { .name("Options Document") .description("Document with all options") .variables(Collections.singletonList( - TemplateVariable.simple("test", "value") + TemplateVariable.simple("{test}", "test", "value", VariableMimeType.TEXT) )) .replaceFonts(true) .defaultFont("Arial") @@ -602,6 +602,7 @@ void requireBothPlaceholderAndName() throws Exception { .placeholder("{customer}") .name("customer") .value("Person A") + .mimeType(VariableMimeType.TEXT) .build() )) .build(); @@ -631,6 +632,7 @@ void allowDistinctPlaceholderAndName() throws Exception { .placeholder("{cust_name}") .name("customerFullName") .value("Person A") + .mimeType(VariableMimeType.TEXT) .build() )) .build(); @@ -660,7 +662,7 @@ void handleApiErrors() { .name("Error Document") .description("Document that should fail") .variables(Collections.singletonList( - TemplateVariable.simple("test", "value") + TemplateVariable.simple("{test}", "test", "value", VariableMimeType.TEXT) )) .build(); @@ -688,7 +690,7 @@ void handleValidationErrors() { .name("Validation Error Document") .description("Document that should fail validation") .variables(Collections.singletonList( - TemplateVariable.simple("test", "value") + TemplateVariable.simple("{test}", "test", "value", VariableMimeType.TEXT) )) .build(); @@ -715,7 +717,7 @@ void handleRateLimitErrors() { .name("Rate Limit Document") .description("Document that should hit rate limit") .variables(Collections.singletonList( - TemplateVariable.simple("test", "value") + TemplateVariable.simple("{test}", "test", "value", VariableMimeType.TEXT) )) .build(); From 8273e0a7dacd340034ed4355ea11e0573cc8c598 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 09:51:55 +0000 Subject: [PATCH 27/39] chore: remove filters reference Signed-off-by: Kushal --- packages/go-sdk/turbotemplate.go | 1 - .../java-sdk/src/main/java/com/turbodocx/TurboTemplate.java | 1 - packages/js-sdk/src/modules/template.ts | 1 - packages/js-sdk/src/types/template.ts | 5 +---- packages/py-sdk/src/turbodocx_sdk/modules/template.py | 1 - 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/go-sdk/turbotemplate.go b/packages/go-sdk/turbotemplate.go index f0ce0d2..16203e5 100644 --- a/packages/go-sdk/turbotemplate.go +++ b/packages/go-sdk/turbotemplate.go @@ -118,7 +118,6 @@ type TurboTemplateClient struct { // - Loops: {#products}...{/products} // - Conditionals: {#if condition}...{/if} // - Expressions: {price + tax} -// - Filters: {name | uppercase} // // Example: // diff --git a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java index 698cd76..2516518 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/TurboTemplate.java @@ -14,7 +14,6 @@ *

  • Loops: {#products}...{/products}
  • *
  • Conditionals: {#if condition}...{/if}
  • *
  • Expressions: {price + tax}
  • - *
  • Filters: {name | uppercase}
  • * * *

    Example usage:

    diff --git a/packages/js-sdk/src/modules/template.ts b/packages/js-sdk/src/modules/template.ts index 428290d..81d8d36 100644 --- a/packages/js-sdk/src/modules/template.ts +++ b/packages/js-sdk/src/modules/template.ts @@ -58,7 +58,6 @@ export class TurboTemplate { * - Loops: {#products}...{/products} * - Conditionals: {#if condition}...{/if} * - Expressions: {price + tax} - * - Filters: {name | uppercase} * * @param request - Template ID and variables * @returns Generated document diff --git a/packages/js-sdk/src/types/template.ts b/packages/js-sdk/src/types/template.ts index 44a0cfc..3e436cd 100644 --- a/packages/js-sdk/src/types/template.ts +++ b/packages/js-sdk/src/types/template.ts @@ -40,7 +40,7 @@ export interface TemplateVariable { /** * Enable advanced templating engine for this variable - * Allows Angular-like expressions: loops, conditions, filters, etc. + * Allows Angular-like expressions: loops, conditions, etc. */ usesAdvancedTemplatingEngine?: boolean; @@ -201,9 +201,6 @@ export interface AdvancedTemplatingFeatures { /** Support for conditionals (e.g., {#if condition}...{/if}) */ conditionals: boolean; - /** Support for filters (e.g., {value | uppercase}) */ - filters: boolean; - /** Support for expressions (e.g., {price * quantity}) */ expressions: boolean; diff --git a/packages/py-sdk/src/turbodocx_sdk/modules/template.py b/packages/py-sdk/src/turbodocx_sdk/modules/template.py index 610986d..dfa1ad7 100644 --- a/packages/py-sdk/src/turbodocx_sdk/modules/template.py +++ b/packages/py-sdk/src/turbodocx_sdk/modules/template.py @@ -80,7 +80,6 @@ async def generate(cls, request: GenerateTemplateRequest) -> GenerateTemplateRes - Loops: {#products}...{/products} - Conditionals: {#if condition}...{/if} - Expressions: {price + tax} - - Filters: {name | uppercase} Args: request: Template ID and variables From afb8ca8ab84e3fb49945f6b3e1739ece735d109a Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 10:00:29 +0000 Subject: [PATCH 28/39] feat: update php sdk to include deliverbale generation Signed-off-by: Kushal --- .../examples/turbotemplate-advanced.php | 209 ++++++++++ .../php-sdk/examples/turbotemplate-simple.php | 153 ++++++++ packages/php-sdk/src/TurboTemplate.php | 177 +++++++++ .../Requests/GenerateTemplateRequest.php | 76 ++++ .../Responses/GenerateTemplateResponse.php | 38 ++ .../php-sdk/src/Types/TemplateVariable.php | 238 ++++++++++++ .../php-sdk/src/Types/VariableMimeType.php | 16 + .../php-sdk/tests/Unit/TurboTemplateTest.php | 361 ++++++++++++++++++ 8 files changed, 1268 insertions(+) create mode 100644 packages/php-sdk/examples/turbotemplate-advanced.php create mode 100644 packages/php-sdk/examples/turbotemplate-simple.php create mode 100644 packages/php-sdk/src/TurboTemplate.php create mode 100644 packages/php-sdk/src/Types/Requests/GenerateTemplateRequest.php create mode 100644 packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php create mode 100644 packages/php-sdk/src/Types/TemplateVariable.php create mode 100644 packages/php-sdk/src/Types/VariableMimeType.php create mode 100644 packages/php-sdk/tests/Unit/TurboTemplateTest.php diff --git a/packages/php-sdk/examples/turbotemplate-advanced.php b/packages/php-sdk/examples/turbotemplate-advanced.php new file mode 100644 index 0000000..6a85ed0 --- /dev/null +++ b/packages/php-sdk/examples/turbotemplate-advanced.php @@ -0,0 +1,209 @@ + 'Company ABC', + 'email' => 'billing@example.com', + 'address' => [ + 'street' => '123 Test Street', + 'city' => 'Test City', + 'state' => 'TS', + 'zip' => '00000' + ] + ]), + + // Invoice metadata + TemplateVariable::simple('{invoice_number}', 'invoice_number', 'INV-0000-001'), + TemplateVariable::simple('{invoice_date}', 'invoice_date', '2024-01-01'), + TemplateVariable::simple('{due_date}', 'due_date', '2024-02-01'), + + // Line items (array for loops) + TemplateVariable::loop('{items}', 'items', [ + [ + 'description' => 'Service A', + 'quantity' => 40, + 'rate' => 150 + ], + [ + 'description' => 'Service B', + 'quantity' => 1, + 'rate' => 5000 + ], + [ + 'description' => 'Service C', + 'quantity' => 12, + 'rate' => 500 + ] + ]), + + // Tax and totals (for expressions) + new TemplateVariable( + placeholder: '{tax_rate}', + name: 'tax_rate', + mimeType: VariableMimeType::TEXT, + value: '0.08', + usesAdvancedTemplatingEngine: true + ), + + // Premium customer flag (for conditionals) + TemplateVariable::conditional('{is_premium}', 'is_premium', true), + new TemplateVariable( + placeholder: '{premium_discount}', + name: 'premium_discount', + mimeType: VariableMimeType::TEXT, + value: '0.05', + usesAdvancedTemplatingEngine: true + ), + + // Payment terms + TemplateVariable::simple('{payment_terms}', 'payment_terms', 'Net 30'), + + // Notes + TemplateVariable::simple('{notes}', 'notes', 'Thank you for your business!') + ], + name: 'Invoice - Company ABC', + description: 'Monthly invoice', + outputFormat: 'pdf' + ) + ); + + echo "Complex invoice generated: {$result->deliverableId}\n"; +} + +/** + * Example: Expressions and Calculations + * + * Template: "Subtotal: ${subtotal}, Tax: ${subtotal * tax_rate}, Total: ${subtotal * (1 + tax_rate)}" + */ +function expressionsAndCalculations(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + new TemplateVariable( + placeholder: '{subtotal}', + name: 'subtotal', + mimeType: VariableMimeType::TEXT, + value: '1000', + usesAdvancedTemplatingEngine: true + ), + new TemplateVariable( + placeholder: '{tax_rate}', + name: 'tax_rate', + mimeType: VariableMimeType::TEXT, + value: '0.08', + usesAdvancedTemplatingEngine: true + ) + ], + name: 'Expressions Document', + description: 'Arithmetic expressions example' + ) + ); + + echo "Document with expressions generated: {$result->deliverableId}\n"; +} + +/** + * Example: Using All Helper Methods + */ +function usingHelpers(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + // Simple variable + TemplateVariable::simple('{title}', 'title', 'Quarterly Report'), + + // Advanced engine variable + TemplateVariable::advancedEngine('{company}', 'company', [ + 'name' => 'Company XYZ', + 'headquarters' => 'Test Location', + 'employees' => 500 + ]), + + // Loop variable + TemplateVariable::loop('{departments}', 'departments', [ + ['name' => 'Dept A', 'headcount' => 200], + ['name' => 'Dept B', 'headcount' => 150], + ['name' => 'Dept C', 'headcount' => 100] + ]), + + // Conditional + TemplateVariable::conditional('{show_financials}', 'show_financials', true), + + // Image + TemplateVariable::image('{company_logo}', 'company_logo', 'https://example.com/logo.png') + ], + name: 'Helper Functions Document', + description: 'Using helper functions example' + ) + ); + + echo "Document with helpers generated: {$result->deliverableId}\n"; +} + +/** + * Example: Custom Options + * + * Demonstrates using optional request parameters + */ +function customOptions(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + TemplateVariable::simple('{title}', 'title', 'Custom Document') + ], + name: 'Custom Options Document', + description: 'Document with custom options', + replaceFonts: true, + defaultFont: 'Arial', + outputFormat: 'pdf', + metadata: [ + 'customField' => 'value', + 'department' => 'Sales', + 'region' => 'North America' + ] + ) + ); + + echo "Document with custom options generated: {$result->deliverableId}\n"; +} + +// Uncomment the examples you want to run: +// complexInvoice(); +// expressionsAndCalculations(); +// usingHelpers(); +// customOptions(); + +echo "Advanced examples ready to run!\n"; diff --git a/packages/php-sdk/examples/turbotemplate-simple.php b/packages/php-sdk/examples/turbotemplate-simple.php new file mode 100644 index 0000000..f3fb4bd --- /dev/null +++ b/packages/php-sdk/examples/turbotemplate-simple.php @@ -0,0 +1,153 @@ +deliverableId}\n"; +} + +/** + * Example 2: Nested Objects with Dot Notation + * + * Template: "Name: {user.name}, Email: {user.email}, Company: {user.profile.company}" + */ +function nestedObjects(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + TemplateVariable::advancedEngine('{user}', 'user', [ + 'name' => 'Person A', + 'email' => 'persona@example.com', + 'profile' => [ + 'company' => 'Company XYZ', + 'title' => 'Role 1', + 'location' => 'Test City, TS' + ] + ]) + ], + name: 'Nested Objects Document', + description: 'Nested object with dot notation example' + ) + ); + + echo "Document with nested data generated: {$result->deliverableId}\n"; +} + +/** + * Example 3: Loops/Arrays + * + * Template: + * {#items} + * - {name}: {quantity} x ${price} = ${quantity * price} + * {/items} + */ +function loopsAndArrays(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + TemplateVariable::loop('{items}', 'items', [ + ['name' => 'Item A', 'quantity' => 5, 'price' => 100, 'sku' => 'SKU-001'], + ['name' => 'Item B', 'quantity' => 3, 'price' => 200, 'sku' => 'SKU-002'], + ['name' => 'Item C', 'quantity' => 10, 'price' => 50, 'sku' => 'SKU-003'] + ]) + ], + name: 'Array Loops Document', + description: 'Array loop iteration example' + ) + ); + + echo "Document with loop generated: {$result->deliverableId}\n"; +} + +/** + * Example 4: Conditionals + * + * Template: + * {#if is_premium} + * Premium Member Discount: {discount * 100}% + * {/if} + * {#if !is_premium} + * Become a premium member for exclusive discounts! + * {/if} + */ +function conditionals(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + TemplateVariable::conditional('{is_premium}', 'is_premium', true), + TemplateVariable::conditional('{discount}', 'discount', 0.2) + ], + name: 'Conditionals Document', + description: 'Boolean conditional example' + ) + ); + + echo "Document with conditionals generated: {$result->deliverableId}\n"; +} + +/** + * Example 5: Using Images + */ +function usingImages(): void +{ + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'your-template-id', + variables: [ + TemplateVariable::simple('{title}', 'title', 'Quarterly Report'), + TemplateVariable::image('{logo}', 'logo', 'https://example.com/logo.png') + ], + name: 'Document with Images', + description: 'Using image variables' + ) + ); + + echo "Document with images generated: {$result->deliverableId}\n"; +} + +// Uncomment the examples you want to run: +// simpleSubstitution(); +// nestedObjects(); +// loopsAndArrays(); +// conditionals(); +// usingImages(); + +echo "Examples ready to run!\n"; diff --git a/packages/php-sdk/src/TurboTemplate.php b/packages/php-sdk/src/TurboTemplate.php new file mode 100644 index 0000000..37eeb3a --- /dev/null +++ b/packages/php-sdk/src/TurboTemplate.php @@ -0,0 +1,177 @@ + 'John', + * 'email' => 'john@example.com' + * ]) + * ] + * ) + * ); + * // Template can use: {user.firstName}, {user.email} + * + * // Advanced: loops with arrays + * $result = TurboTemplate::generate( + * new GenerateTemplateRequest( + * templateId: 'template-uuid', + * variables: [ + * TemplateVariable::loop('{products}', 'products', [ + * ['name' => 'Laptop', 'price' => 999], + * ['name' => 'Mouse', 'price' => 29] + * ]) + * ] + * ) + * ); + * // Template can use: {#products}{name}: ${price}{/products} + * ``` + */ + public static function generate(GenerateTemplateRequest $request): GenerateTemplateResponse + { + $client = self::getClient(); + + // Convert request to JSON body + $body = $request->toArray(); + + // Make POST request to /v1/deliverable + $response = $client->post('/v1/deliverable', $body); + + return GenerateTemplateResponse::fromArray($response); + } + + /** + * Validate a variable configuration + * + * Checks if a variable is properly configured for advanced templating + * + * @param TemplateVariable $variable Variable to validate + * @return array{isValid: bool, errors?: array, warnings?: array} + */ + public static function validateVariable(TemplateVariable $variable): array + { + $errors = []; + $warnings = []; + + // Check placeholder/name + if (empty($variable->placeholder) || empty($variable->name)) { + $errors[] = 'Variable must have both "placeholder" and "name" properties'; + } + + // Check value/text - allow null values, don't enforce either property + + // Check advanced templating settings + if (is_array($variable->value) && !array_is_list($variable->value)) { + // Associative array (object) + if ($variable->mimeType !== VariableMimeType::JSON) { + $warnings[] = 'Complex objects should explicitly set mimeType to "json"'; + } + } + + // Check for arrays + if (is_array($variable->value) && array_is_list($variable->value)) { + if ($variable->mimeType !== VariableMimeType::JSON) { + $warnings[] = 'Array values should use mimeType: "json"'; + } + } + + // Check image variables + if ($variable->mimeType === VariableMimeType::IMAGE) { + if (!is_string($variable->value)) { + $errors[] = 'Image variables must have a string value (URL or base64)'; + } + } + + $result = ['isValid' => count($errors) === 0]; + if (count($errors) > 0) { + $result['errors'] = $errors; + } + if (count($warnings) > 0) { + $result['warnings'] = $warnings; + } + + return $result; + } +} diff --git a/packages/php-sdk/src/Types/Requests/GenerateTemplateRequest.php b/packages/php-sdk/src/Types/Requests/GenerateTemplateRequest.php new file mode 100644 index 0000000..2a66ce3 --- /dev/null +++ b/packages/php-sdk/src/Types/Requests/GenerateTemplateRequest.php @@ -0,0 +1,76 @@ + $variables Template variables + * @param string|null $name Document name + * @param string|null $description Document description + * @param bool|null $replaceFonts Whether to replace fonts + * @param string|null $defaultFont Default font to use + * @param string|null $outputFormat Output format (e.g., 'pdf', 'docx') + * @param array|null $metadata Additional metadata + */ + public function __construct( + public string $templateId, + public array $variables, + public ?string $name = null, + public ?string $description = null, + public ?bool $replaceFonts = null, + public ?string $defaultFont = null, + public ?string $outputFormat = null, + public ?array $metadata = null + ) { + if (empty($templateId)) { + throw new \InvalidArgumentException('templateId is required'); + } + if (empty($variables)) { + throw new \InvalidArgumentException('variables are required'); + } + } + + /** + * Convert to array for JSON serialization + * + * @return array + */ + public function toArray(): array + { + $data = [ + 'templateId' => $this->templateId, + 'variables' => array_map(fn($v) => $v->toArray(), $this->variables), + ]; + + // Add optional parameters if set + if ($this->name !== null) { + $data['name'] = $this->name; + } + if ($this->description !== null) { + $data['description'] = $this->description; + } + if ($this->replaceFonts !== null) { + $data['replaceFonts'] = $this->replaceFonts; + } + if ($this->defaultFont !== null) { + $data['defaultFont'] = $this->defaultFont; + } + if ($this->outputFormat !== null) { + $data['outputFormat'] = $this->outputFormat; + } + if ($this->metadata !== null) { + $data['metadata'] = $this->metadata; + } + + return $data; + } +} diff --git a/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php b/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php new file mode 100644 index 0000000..4516918 --- /dev/null +++ b/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php @@ -0,0 +1,38 @@ + $data + * @return self + */ + public static function fromArray(array $data): self + { + return new self( + success: $data['success'] ?? false, + deliverableId: $data['deliverableId'] ?? null, + message: $data['message'] ?? null + ); + } +} diff --git a/packages/php-sdk/src/Types/TemplateVariable.php b/packages/php-sdk/src/Types/TemplateVariable.php new file mode 100644 index 0000000..b39c53b --- /dev/null +++ b/packages/php-sdk/src/Types/TemplateVariable.php @@ -0,0 +1,238 @@ +|null $subvariables Nested sub-variables + */ + public function __construct( + public string $placeholder, + public string $name, + public VariableMimeType $mimeType, + public mixed $value = null, + public ?string $text = null, + public ?bool $usesAdvancedTemplatingEngine = null, + public ?bool $nestedInAdvancedTemplatingEngine = null, + public ?bool $allowRichTextInjection = null, + public ?string $description = null, + public ?bool $defaultValue = null, + public ?array $subvariables = null + ) { + } + + /** + * Convert to array for JSON serialization + * + * @return array + */ + public function toArray(): array + { + $data = [ + 'placeholder' => $this->placeholder, + 'name' => $this->name, + 'mimeType' => $this->mimeType->value, + ]; + + // Add value/text if present - allow null values + if (array_key_exists('value', get_object_vars($this))) { + $data['value'] = $this->value; + } + if ($this->text !== null) { + $data['text'] = $this->text; + } + + // Add optional boolean flags if set + if ($this->usesAdvancedTemplatingEngine !== null) { + $data['usesAdvancedTemplatingEngine'] = $this->usesAdvancedTemplatingEngine; + } + if ($this->nestedInAdvancedTemplatingEngine !== null) { + $data['nestedInAdvancedTemplatingEngine'] = $this->nestedInAdvancedTemplatingEngine; + } + if ($this->allowRichTextInjection !== null) { + $data['allowRichTextInjection'] = $this->allowRichTextInjection; + } + + // Add optional string fields if set + if ($this->description !== null) { + $data['description'] = $this->description; + } + if ($this->defaultValue !== null) { + $data['defaultValue'] = $this->defaultValue; + } + if ($this->subvariables !== null) { + $data['subvariables'] = array_map(fn($v) => $v->toArray(), $this->subvariables); + } + + return $data; + } + + /** + * Helper: Create a simple text variable + * + * @param string $placeholder Variable placeholder (e.g., "{customer_name}") + * @param string $name Variable name + * @param string|int|float|bool $value Variable value + * @param VariableMimeType $mimeType MIME type ('TEXT' or 'HTML') + * @return self + */ + public static function simple( + string $placeholder, + string $name, + string|int|float|bool $value, + VariableMimeType $mimeType = VariableMimeType::TEXT + ): self { + if (empty($placeholder)) { + throw new \InvalidArgumentException('placeholder is required'); + } + if (empty($name)) { + throw new \InvalidArgumentException('name is required'); + } + if ($mimeType !== VariableMimeType::TEXT && $mimeType !== VariableMimeType::HTML) { + throw new \InvalidArgumentException('mimeType must be TEXT or HTML for simple variables'); + } + + return new self( + placeholder: $placeholder, + name: $name, + mimeType: $mimeType, + value: $value + ); + } + + /** + * Helper: Create an advanced engine variable (for nested objects, complex data) + * + * @param string $placeholder Variable placeholder (e.g., "{user}") + * @param string $name Variable name + * @param array $value Nested object value + * @return self + */ + public static function advancedEngine( + string $placeholder, + string $name, + array $value + ): self { + if (empty($placeholder)) { + throw new \InvalidArgumentException('placeholder is required'); + } + if (empty($name)) { + throw new \InvalidArgumentException('name is required'); + } + + return new self( + placeholder: $placeholder, + name: $name, + mimeType: VariableMimeType::JSON, + value: $value, + usesAdvancedTemplatingEngine: true + ); + } + + /** + * Helper: Create a variable for array loops + * + * @param string $placeholder Variable placeholder (e.g., "{products}") + * @param string $name Variable name + * @param array $value Array/list value + * @return self + */ + public static function loop( + string $placeholder, + string $name, + array $value + ): self { + if (empty($placeholder)) { + throw new \InvalidArgumentException('placeholder is required'); + } + if (empty($name)) { + throw new \InvalidArgumentException('name is required'); + } + + return new self( + placeholder: $placeholder, + name: $name, + mimeType: VariableMimeType::JSON, + value: $value, + usesAdvancedTemplatingEngine: true + ); + } + + /** + * Helper: Create a variable for conditionals + * + * @param string $placeholder Variable placeholder (e.g., "{showDetails}") + * @param string $name Variable name + * @param mixed $value Boolean or truthy value + * @return self + */ + public static function conditional( + string $placeholder, + string $name, + mixed $value + ): self { + if (empty($placeholder)) { + throw new \InvalidArgumentException('placeholder is required'); + } + if (empty($name)) { + throw new \InvalidArgumentException('name is required'); + } + + return new self( + placeholder: $placeholder, + name: $name, + mimeType: VariableMimeType::JSON, + value: $value, + usesAdvancedTemplatingEngine: true + ); + } + + /** + * Helper: Create a variable for images + * + * @param string $placeholder Variable placeholder (e.g., "{logo}") + * @param string $name Variable name + * @param string $imageUrl Image URL or base64 data + * @return self + */ + public static function image( + string $placeholder, + string $name, + string $imageUrl + ): self { + if (empty($placeholder)) { + throw new \InvalidArgumentException('placeholder is required'); + } + if (empty($name)) { + throw new \InvalidArgumentException('name is required'); + } + if (empty($imageUrl)) { + throw new \InvalidArgumentException('imageUrl is required'); + } + + return new self( + placeholder: $placeholder, + name: $name, + mimeType: VariableMimeType::IMAGE, + value: $imageUrl + ); + } +} diff --git a/packages/php-sdk/src/Types/VariableMimeType.php b/packages/php-sdk/src/Types/VariableMimeType.php new file mode 100644 index 0000000..4f007d8 --- /dev/null +++ b/packages/php-sdk/src/Types/VariableMimeType.php @@ -0,0 +1,16 @@ +assertEquals('{customer_name}', $variable->placeholder); + $this->assertEquals('customer_name', $variable->name); + $this->assertEquals('Person A', $variable->value); + $this->assertEquals(VariableMimeType::TEXT, $variable->mimeType); + } + + public function testSimpleVariableWithNumberValue(): void + { + $variable = TemplateVariable::simple('{order_total}', 'order_total', 1500); + + $this->assertEquals('{order_total}', $variable->placeholder); + $this->assertEquals('order_total', $variable->name); + $this->assertEquals(1500, $variable->value); + } + + public function testSimpleVariableWithHtmlMimeType(): void + { + $variable = TemplateVariable::simple('{content}', 'content', 'Bold', VariableMimeType::HTML); + + $this->assertEquals('{content}', $variable->placeholder); + $this->assertEquals('content', $variable->name); + $this->assertEquals('Bold', $variable->value); + $this->assertEquals(VariableMimeType::HTML, $variable->mimeType); + } + + public function testSimpleVariableThrowsWhenPlaceholderMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::simple('', 'name', 'value'); + } + + public function testSimpleVariableThrowsWhenNameMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::simple('{test}', '', 'value'); + } + + public function testSimpleVariableThrowsWhenMimeTypeInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::simple('{test}', 'test', 'value', VariableMimeType::JSON); + } + + // ============================================ + // Helper Function Tests - advancedEngine() + // ============================================ + + public function testAdvancedEngineVariableWithObjectValue(): void + { + $user = [ + 'firstName' => 'Foo', + 'lastName' => 'Bar', + 'email' => 'foo@example.com' + ]; + + $variable = TemplateVariable::advancedEngine('{user}', 'user', $user); + + $this->assertEquals('{user}', $variable->placeholder); + $this->assertEquals('user', $variable->name); + $this->assertEquals($user, $variable->value); + $this->assertEquals(VariableMimeType::JSON, $variable->mimeType); + $this->assertTrue($variable->usesAdvancedTemplatingEngine); + } + + public function testAdvancedEngineVariableWithDeeplyNestedObject(): void + { + $company = [ + 'name' => 'Company ABC', + 'address' => [ + 'street' => '123 Test Street', + 'city' => 'Test City', + 'state' => 'TS' + ] + ]; + + $variable = TemplateVariable::advancedEngine('{company}', 'company', $company); + + $this->assertEquals('{company}', $variable->placeholder); + $this->assertEquals(VariableMimeType::JSON, $variable->mimeType); + $this->assertTrue($variable->usesAdvancedTemplatingEngine); + } + + public function testAdvancedEngineVariableThrowsWhenPlaceholderMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::advancedEngine('', 'user', ['name' => 'Test']); + } + + public function testAdvancedEngineVariableThrowsWhenNameMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::advancedEngine('{user}', '', ['name' => 'Test']); + } + + // ============================================ + // Helper Function Tests - loop() + // ============================================ + + public function testLoopVariableWithArrayValue(): void + { + $items = [ + ['name' => 'Item A', 'price' => 100], + ['name' => 'Item B', 'price' => 200] + ]; + + $variable = TemplateVariable::loop('{items}', 'items', $items); + + $this->assertEquals('{items}', $variable->placeholder); + $this->assertEquals('items', $variable->name); + $this->assertEquals($items, $variable->value); + $this->assertEquals(VariableMimeType::JSON, $variable->mimeType); + $this->assertTrue($variable->usesAdvancedTemplatingEngine); + } + + public function testLoopVariableWithEmptyArray(): void + { + $products = []; + + $variable = TemplateVariable::loop('{products}', 'products', $products); + + $this->assertEquals('{products}', $variable->placeholder); + $this->assertEquals($products, $variable->value); + $this->assertEquals(VariableMimeType::JSON, $variable->mimeType); + } + + public function testLoopVariableWithPrimitiveArray(): void + { + $tags = ['tag1', 'tag2', 'tag3']; + + $variable = TemplateVariable::loop('{tags}', 'tags', $tags); + + $this->assertEquals($tags, $variable->value); + } + + public function testLoopVariableThrowsWhenPlaceholderMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::loop('', 'items', []); + } + + public function testLoopVariableThrowsWhenNameMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::loop('{items}', '', []); + } + + // ============================================ + // Helper Function Tests - conditional() + // ============================================ + + public function testConditionalVariableWithBooleanTrue(): void + { + $variable = TemplateVariable::conditional('{is_premium}', 'is_premium', true); + + $this->assertEquals('{is_premium}', $variable->placeholder); + $this->assertEquals('is_premium', $variable->name); + $this->assertTrue($variable->value); + $this->assertTrue($variable->usesAdvancedTemplatingEngine); + } + + public function testConditionalVariableWithBooleanFalse(): void + { + $variable = TemplateVariable::conditional('{show_discount}', 'show_discount', false); + + $this->assertFalse($variable->value); + $this->assertTrue($variable->usesAdvancedTemplatingEngine); + } + + public function testConditionalVariableWithTruthyValue(): void + { + $variable = TemplateVariable::conditional('{count}', 'count', 5); + + $this->assertEquals(5, $variable->value); + } + + public function testConditionalVariableThrowsWhenPlaceholderMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::conditional('', 'is_active', true); + } + + public function testConditionalVariableThrowsWhenNameMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::conditional('{is_active}', '', true); + } + + // ============================================ + // Helper Function Tests - image() + // ============================================ + + public function testImageVariableWithUrl(): void + { + $variable = TemplateVariable::image('{logo}', 'logo', 'https://example.com/logo.png'); + + $this->assertEquals('{logo}', $variable->placeholder); + $this->assertEquals('logo', $variable->name); + $this->assertEquals('https://example.com/logo.png', $variable->value); + $this->assertEquals(VariableMimeType::IMAGE, $variable->mimeType); + } + + public function testImageVariableWithBase64(): void + { + $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...'; + $variable = TemplateVariable::image('{signature}', 'signature', $base64Image); + + $this->assertEquals($base64Image, $variable->value); + $this->assertEquals(VariableMimeType::IMAGE, $variable->mimeType); + } + + public function testImageVariableThrowsWhenPlaceholderMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::image('', 'logo', 'https://example.com/logo.png'); + } + + public function testImageVariableThrowsWhenNameMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::image('{logo}', '', 'https://example.com/logo.png'); + } + + public function testImageVariableThrowsWhenImageUrlMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + TemplateVariable::image('{logo}', 'logo', ''); + } + + // ============================================ + // Request Building Tests + // ============================================ + + public function testGenerateTemplateRequestWithSimpleVariables(): void + { + $request = new GenerateTemplateRequest( + templateId: 'template-123', + variables: [ + TemplateVariable::simple('{customer_name}', 'customer_name', 'Person A'), + TemplateVariable::simple('{order_total}', 'order_total', 1500) + ], + name: 'Test Document', + description: 'Test description' + ); + + $this->assertEquals('template-123', $request->templateId); + $this->assertCount(2, $request->variables); + $this->assertEquals('Test Document', $request->name); + } + + public function testGenerateTemplateRequestThrowsWhenTemplateIdMissing(): void + { + $this->expectException(\InvalidArgumentException::class); + new GenerateTemplateRequest( + templateId: '', + variables: [TemplateVariable::simple('{test}', 'test', 'value')] + ); + } + + public function testGenerateTemplateRequestThrowsWhenVariablesEmpty(): void + { + $this->expectException(\InvalidArgumentException::class); + new GenerateTemplateRequest( + templateId: 'template-123', + variables: [] + ); + } + + public function testGenerateTemplateRequestToArray(): void + { + $request = new GenerateTemplateRequest( + templateId: 'template-123', + variables: [ + TemplateVariable::simple('{test}', 'test', 'value') + ], + name: 'Test', + replaceFonts: true, + defaultFont: 'Arial', + outputFormat: 'pdf', + metadata: ['customField' => 'value'] + ); + + $array = $request->toArray(); + + $this->assertEquals('template-123', $array['templateId']); + $this->assertIsArray($array['variables']); + $this->assertEquals('Test', $array['name']); + $this->assertTrue($array['replaceFonts']); + $this->assertEquals('Arial', $array['defaultFont']); + $this->assertEquals('pdf', $array['outputFormat']); + $this->assertEquals(['customField' => 'value'], $array['metadata']); + } + + // ============================================ + // Variable Serialization Tests + // ============================================ + + public function testVariableToArrayIncludesAllFields(): void + { + $variable = new TemplateVariable( + placeholder: '{test}', + name: 'test', + mimeType: VariableMimeType::TEXT, + value: 'value', + usesAdvancedTemplatingEngine: true, + description: 'Test variable' + ); + + $array = $variable->toArray(); + + $this->assertEquals('{test}', $array['placeholder']); + $this->assertEquals('test', $array['name']); + $this->assertEquals('text', $array['mimeType']); + $this->assertEquals('value', $array['value']); + $this->assertTrue($array['usesAdvancedTemplatingEngine']); + $this->assertEquals('Test variable', $array['description']); + } + + public function testVariableToArrayAllowsNullValue(): void + { + $variable = new TemplateVariable( + placeholder: '{test}', + name: 'test', + mimeType: VariableMimeType::TEXT, + value: null + ); + + $array = $variable->toArray(); + + $this->assertArrayHasKey('value', $array); + $this->assertNull($array['value']); + } +} From 55868466993b5eff20ea4caaa6e6863b2458d6e3 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 10:05:20 +0000 Subject: [PATCH 29/39] chore: fix php-sdk ci issues Signed-off-by: Kushal --- .../examples/turbotemplate-advanced.php | 26 +++++++++---------- .../php-sdk/examples/turbotemplate-simple.php | 16 ++++++------ .../Responses/GenerateTemplateResponse.php | 3 +-- .../php-sdk/src/Types/TemplateVariable.php | 3 +-- .../php-sdk/tests/Unit/TurboTemplateTest.php | 12 ++++----- 5 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/php-sdk/examples/turbotemplate-advanced.php b/packages/php-sdk/examples/turbotemplate-advanced.php index 6a85ed0..25d9a70 100644 --- a/packages/php-sdk/examples/turbotemplate-advanced.php +++ b/packages/php-sdk/examples/turbotemplate-advanced.php @@ -35,8 +35,8 @@ function complexInvoice(): void 'street' => '123 Test Street', 'city' => 'Test City', 'state' => 'TS', - 'zip' => '00000' - ] + 'zip' => '00000', + ], ]), // Invoice metadata @@ -49,18 +49,18 @@ function complexInvoice(): void [ 'description' => 'Service A', 'quantity' => 40, - 'rate' => 150 + 'rate' => 150, ], [ 'description' => 'Service B', 'quantity' => 1, - 'rate' => 5000 + 'rate' => 5000, ], [ 'description' => 'Service C', 'quantity' => 12, - 'rate' => 500 - ] + 'rate' => 500, + ], ]), // Tax and totals (for expressions) @@ -86,7 +86,7 @@ function complexInvoice(): void TemplateVariable::simple('{payment_terms}', 'payment_terms', 'Net 30'), // Notes - TemplateVariable::simple('{notes}', 'notes', 'Thank you for your business!') + TemplateVariable::simple('{notes}', 'notes', 'Thank you for your business!'), ], name: 'Invoice - Company ABC', description: 'Monthly invoice', @@ -121,7 +121,7 @@ function expressionsAndCalculations(): void mimeType: VariableMimeType::TEXT, value: '0.08', usesAdvancedTemplatingEngine: true - ) + ), ], name: 'Expressions Document', description: 'Arithmetic expressions example' @@ -147,21 +147,21 @@ function usingHelpers(): void TemplateVariable::advancedEngine('{company}', 'company', [ 'name' => 'Company XYZ', 'headquarters' => 'Test Location', - 'employees' => 500 + 'employees' => 500, ]), // Loop variable TemplateVariable::loop('{departments}', 'departments', [ ['name' => 'Dept A', 'headcount' => 200], ['name' => 'Dept B', 'headcount' => 150], - ['name' => 'Dept C', 'headcount' => 100] + ['name' => 'Dept C', 'headcount' => 100], ]), // Conditional TemplateVariable::conditional('{show_financials}', 'show_financials', true), // Image - TemplateVariable::image('{company_logo}', 'company_logo', 'https://example.com/logo.png') + TemplateVariable::image('{company_logo}', 'company_logo', 'https://example.com/logo.png'), ], name: 'Helper Functions Document', description: 'Using helper functions example' @@ -182,7 +182,7 @@ function customOptions(): void new GenerateTemplateRequest( templateId: 'your-template-id', variables: [ - TemplateVariable::simple('{title}', 'title', 'Custom Document') + TemplateVariable::simple('{title}', 'title', 'Custom Document'), ], name: 'Custom Options Document', description: 'Document with custom options', @@ -192,7 +192,7 @@ function customOptions(): void metadata: [ 'customField' => 'value', 'department' => 'Sales', - 'region' => 'North America' + 'region' => 'North America', ] ) ); diff --git a/packages/php-sdk/examples/turbotemplate-simple.php b/packages/php-sdk/examples/turbotemplate-simple.php index f3fb4bd..c13d3e3 100644 --- a/packages/php-sdk/examples/turbotemplate-simple.php +++ b/packages/php-sdk/examples/turbotemplate-simple.php @@ -28,7 +28,7 @@ function simpleSubstitution(): void variables: [ TemplateVariable::simple('{customer_name}', 'customer_name', 'Foo Bar'), TemplateVariable::simple('{order_total}', 'order_total', 1500), - TemplateVariable::simple('{order_date}', 'order_date', '2024-01-01') + TemplateVariable::simple('{order_date}', 'order_date', '2024-01-01'), ], name: 'Simple Substitution Document', description: 'Basic variable substitution example' @@ -55,9 +55,9 @@ function nestedObjects(): void 'profile' => [ 'company' => 'Company XYZ', 'title' => 'Role 1', - 'location' => 'Test City, TS' - ] - ]) + 'location' => 'Test City, TS', + ], + ]), ], name: 'Nested Objects Document', description: 'Nested object with dot notation example' @@ -84,8 +84,8 @@ function loopsAndArrays(): void TemplateVariable::loop('{items}', 'items', [ ['name' => 'Item A', 'quantity' => 5, 'price' => 100, 'sku' => 'SKU-001'], ['name' => 'Item B', 'quantity' => 3, 'price' => 200, 'sku' => 'SKU-002'], - ['name' => 'Item C', 'quantity' => 10, 'price' => 50, 'sku' => 'SKU-003'] - ]) + ['name' => 'Item C', 'quantity' => 10, 'price' => 50, 'sku' => 'SKU-003'], + ]), ], name: 'Array Loops Document', description: 'Array loop iteration example' @@ -113,7 +113,7 @@ function conditionals(): void templateId: 'your-template-id', variables: [ TemplateVariable::conditional('{is_premium}', 'is_premium', true), - TemplateVariable::conditional('{discount}', 'discount', 0.2) + TemplateVariable::conditional('{discount}', 'discount', 0.2), ], name: 'Conditionals Document', description: 'Boolean conditional example' @@ -133,7 +133,7 @@ function usingImages(): void templateId: 'your-template-id', variables: [ TemplateVariable::simple('{title}', 'title', 'Quarterly Report'), - TemplateVariable::image('{logo}', 'logo', 'https://example.com/logo.png') + TemplateVariable::image('{logo}', 'logo', 'https://example.com/logo.png'), ], name: 'Document with Images', description: 'Using image variables' diff --git a/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php b/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php index 4516918..3a29c2b 100644 --- a/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php +++ b/packages/php-sdk/src/Types/Responses/GenerateTemplateResponse.php @@ -18,8 +18,7 @@ public function __construct( public bool $success, public ?string $deliverableId = null, public ?string $message = null - ) { - } + ) {} /** * Create from API response array diff --git a/packages/php-sdk/src/Types/TemplateVariable.php b/packages/php-sdk/src/Types/TemplateVariable.php index b39c53b..1d875cb 100644 --- a/packages/php-sdk/src/Types/TemplateVariable.php +++ b/packages/php-sdk/src/Types/TemplateVariable.php @@ -36,8 +36,7 @@ public function __construct( public ?string $description = null, public ?bool $defaultValue = null, public ?array $subvariables = null - ) { - } + ) {} /** * Convert to array for JSON serialization diff --git a/packages/php-sdk/tests/Unit/TurboTemplateTest.php b/packages/php-sdk/tests/Unit/TurboTemplateTest.php index bcbbb3a..66c6150 100644 --- a/packages/php-sdk/tests/Unit/TurboTemplateTest.php +++ b/packages/php-sdk/tests/Unit/TurboTemplateTest.php @@ -79,7 +79,7 @@ public function testAdvancedEngineVariableWithObjectValue(): void $user = [ 'firstName' => 'Foo', 'lastName' => 'Bar', - 'email' => 'foo@example.com' + 'email' => 'foo@example.com', ]; $variable = TemplateVariable::advancedEngine('{user}', 'user', $user); @@ -98,8 +98,8 @@ public function testAdvancedEngineVariableWithDeeplyNestedObject(): void 'address' => [ 'street' => '123 Test Street', 'city' => 'Test City', - 'state' => 'TS' - ] + 'state' => 'TS', + ], ]; $variable = TemplateVariable::advancedEngine('{company}', 'company', $company); @@ -129,7 +129,7 @@ public function testLoopVariableWithArrayValue(): void { $items = [ ['name' => 'Item A', 'price' => 100], - ['name' => 'Item B', 'price' => 200] + ['name' => 'Item B', 'price' => 200], ]; $variable = TemplateVariable::loop('{items}', 'items', $items); @@ -265,7 +265,7 @@ public function testGenerateTemplateRequestWithSimpleVariables(): void templateId: 'template-123', variables: [ TemplateVariable::simple('{customer_name}', 'customer_name', 'Person A'), - TemplateVariable::simple('{order_total}', 'order_total', 1500) + TemplateVariable::simple('{order_total}', 'order_total', 1500), ], name: 'Test Document', description: 'Test description' @@ -299,7 +299,7 @@ public function testGenerateTemplateRequestToArray(): void $request = new GenerateTemplateRequest( templateId: 'template-123', variables: [ - TemplateVariable::simple('{test}', 'test', 'value') + TemplateVariable::simple('{test}', 'test', 'value'), ], name: 'Test', replaceFonts: true, From f0138ce720624bb0cee6eb2d605b486beab1081e Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 10:49:00 +0000 Subject: [PATCH 30/39] docs: update php-sdk readme Signed-off-by: Kushal --- packages/php-sdk/README.md | 320 +++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) diff --git a/packages/php-sdk/README.md b/packages/php-sdk/README.md index c23da57..9bed0d5 100644 --- a/packages/php-sdk/README.md +++ b/packages/php-sdk/README.md @@ -18,6 +18,8 @@ - 🔄 **Industry Standard** — Guzzle HTTP client, PSR standards compliance - 🛡️ **Type-safe** — Catch errors at development time with static analysis - 🤖 **100% n8n Parity** — Same operations as our n8n community nodes +- ✍️ **TurboSign** — Digital signature workflows with comprehensive field types +- 📄 **TurboTemplate** — Advanced document generation with templating --- @@ -40,6 +42,8 @@ composer require turbodocx/sdk ## Quick Start +### TurboSign - Digital Signatures + ```php documentId}\n"; ``` +### TurboTemplate - Document Generation + +```php +deliverableId}\n"; +echo "Download URL: {$result->downloadUrl}\n"; +``` + --- ## Configuration @@ -301,6 +350,277 @@ foreach ($audit->entries as $entry) { --- +## TurboTemplate + +### `generate()` + +Generate a document from a template with variable substitution. + +```php +use TurboDocx\TurboTemplate; +use TurboDocx\Types\Requests\GenerateTemplateRequest; +use TurboDocx\Types\TemplateVariable; +use TurboDocx\Types\VariableMimeType; +use TurboDocx\Types\OutputFormat; + +$result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'template-uuid', + variables: [ + new TemplateVariable( + placeholder: '{customer_name}', + name: 'customer_name', + value: 'John Doe', + mimeType: VariableMimeType::TEXT + ), + new TemplateVariable( + placeholder: '{order_date}', + name: 'order_date', + value: '2024-01-15', + mimeType: VariableMimeType::TEXT + ), + ], + outputFormat: OutputFormat::PDF, + outputFilename: 'invoice.pdf', + name: 'Customer Invoice', + description: 'Q1 2024 Invoice' + ) +); + +echo "Deliverable ID: {$result->deliverableId}\n"; +echo "Download URL: {$result->downloadUrl}\n"; +``` + +### Variable Types + +#### Simple Text Variables + +```php +new TemplateVariable( + placeholder: '{name}', + name: 'name', + value: 'John Doe', + mimeType: VariableMimeType::TEXT +) +``` + +#### HTML Variables + +```php +new TemplateVariable( + placeholder: '{content}', + name: 'content', + value: '

    Hello

    This is bold text

    ', + mimeType: VariableMimeType::HTML, + allowRichTextInjection: true +) +``` + +#### Image Variables + +```php +// From URL +new TemplateVariable( + placeholder: '{logo}', + name: 'logo', + value: 'https://example.com/logo.png', + mimeType: VariableMimeType::IMAGE +) + +// From base64 +new TemplateVariable( + placeholder: '{signature}', + name: 'signature', + value: 'data:image/png;base64,iVBORw0KGgoAAAANS...', + mimeType: VariableMimeType::IMAGE +) +``` + +### Advanced Templating + +#### Nested Objects + +```php +new TemplateVariable( + placeholder: '{client}', + name: 'client', + value: [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'address' => [ + 'street' => '123 Main St', + 'city' => 'New York', + 'zip' => '10001', + ], + ], + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true +) +``` + +In your template: `{{client.name}}`, `{{client.address.city}}`, etc. + +#### Array Loops + +```php +new TemplateVariable( + placeholder: '{items}', + name: 'items', + value: [ + [ + 'name' => 'Product A', + 'price' => 99.99, + 'quantity' => 2, + ], + [ + 'name' => 'Product B', + 'price' => 149.99, + 'quantity' => 1, + ], + ], + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true +) +``` + +In your template: +``` +{{#each items}} + {{name}} - ${{price}} x {{quantity}} +{{/each}} +``` + +#### Conditionals + +```php +new TemplateVariable( + placeholder: '{customer_type}', + name: 'customer_type', + value: 'premium', + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true +), +new TemplateVariable( + placeholder: '{discount}', + name: 'discount', + value: '20%', + mimeType: VariableMimeType::TEXT +) +``` + +In your template: +``` +{{#if customer_type == "premium"}} + Special discount: {{discount}} +{{/if}} +``` + +#### Expressions + +```php +new TemplateVariable( + placeholder: '{price}', + name: 'price', + value: 100, + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true +), +new TemplateVariable( + placeholder: '{quantity}', + name: 'quantity', + value: 5, + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true +), +new TemplateVariable( + placeholder: '{tax_rate}', + name: 'tax_rate', + value: 0.08, + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true +) +``` + +In your template: +``` +Subtotal: {{price * quantity}} +Tax: {{price * quantity * tax_rate}} +Total: {{price * quantity * (1 + tax_rate)}} +``` + +### Output Formats + +```php +// Generate as PDF +$result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'template-uuid', + variables: $variables, + outputFormat: OutputFormat::PDF, + outputFilename: 'document.pdf' + ) +); + +// Generate as DOCX +$result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'template-uuid', + variables: $variables, + outputFormat: OutputFormat::DOCX, + outputFilename: 'document.docx' + ) +); +``` + +### Integration with TurboSign + +Generate a document and send it for signature: + +```php +// 1. Generate document with TurboTemplate +$templateResult = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: 'template-uuid', + variables: [ + new TemplateVariable( + placeholder: '{client_name}', + name: 'client_name', + value: 'John Doe', + mimeType: VariableMimeType::TEXT + ), + ], + outputFormat: OutputFormat::PDF, + outputFilename: 'contract.pdf' + ) +); + +// 2. Send for signature using the deliverable ID +$signResult = TurboSign::sendSignature( + new SendSignatureRequest( + deliverableId: $templateResult->deliverableId, + recipients: [ + new Recipient('John Doe', 'john@example.com', 1) + ], + fields: [ + new Field( + type: SignatureFieldType::SIGNATURE, + recipientEmail: 'john@example.com', + template: new TemplateConfig( + anchor: '{ClientSignature}', + placement: FieldPlacement::REPLACE, + size: ['width' => 100, 'height' => 30] + ) + ) + ], + documentName: 'Contract' + ) +); + +echo "Sent for signature: {$signResult->documentId}\n"; +``` + +--- + ## Field Types TurboSign supports 11 different field types: From e9d8ac56f3344c6a63d6d005ebf601e372b4d5e5 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 10:49:24 +0000 Subject: [PATCH 31/39] feat: add sdk for rust Signed-off-by: Kushal --- packages/rust-sdk/.gitignore | 17 + packages/rust-sdk/Cargo.toml | 21 + packages/rust-sdk/LICENSE | 21 + packages/rust-sdk/README.md | 786 ++++++++++++++++++ packages/rust-sdk/examples/advanced.rs | 151 ++++ packages/rust-sdk/examples/sign.rs | 131 +++ packages/rust-sdk/examples/simple.rs | 96 +++ packages/rust-sdk/src/http.rs | 212 +++++ packages/rust-sdk/src/lib.rs | 87 ++ packages/rust-sdk/src/modules/mod.rs | 5 + packages/rust-sdk/src/modules/sign.rs | 376 +++++++++ packages/rust-sdk/src/modules/template.rs | 118 +++ packages/rust-sdk/src/types/mod.rs | 14 + packages/rust-sdk/src/types/sign.rs | 523 ++++++++++++ packages/rust-sdk/src/types/template.rs | 355 ++++++++ packages/rust-sdk/src/utils/errors.rs | 36 + packages/rust-sdk/src/utils/mod.rs | 3 + packages/rust-sdk/tests/turbosign_test.rs | 554 ++++++++++++ packages/rust-sdk/tests/turbotemplate_test.rs | 140 ++++ 19 files changed, 3646 insertions(+) create mode 100644 packages/rust-sdk/.gitignore create mode 100644 packages/rust-sdk/Cargo.toml create mode 100644 packages/rust-sdk/LICENSE create mode 100644 packages/rust-sdk/README.md create mode 100644 packages/rust-sdk/examples/advanced.rs create mode 100644 packages/rust-sdk/examples/sign.rs create mode 100644 packages/rust-sdk/examples/simple.rs create mode 100644 packages/rust-sdk/src/http.rs create mode 100644 packages/rust-sdk/src/lib.rs create mode 100644 packages/rust-sdk/src/modules/mod.rs create mode 100644 packages/rust-sdk/src/modules/sign.rs create mode 100644 packages/rust-sdk/src/modules/template.rs create mode 100644 packages/rust-sdk/src/types/mod.rs create mode 100644 packages/rust-sdk/src/types/sign.rs create mode 100644 packages/rust-sdk/src/types/template.rs create mode 100644 packages/rust-sdk/src/utils/errors.rs create mode 100644 packages/rust-sdk/src/utils/mod.rs create mode 100644 packages/rust-sdk/tests/turbosign_test.rs create mode 100644 packages/rust-sdk/tests/turbotemplate_test.rs diff --git a/packages/rust-sdk/.gitignore b/packages/rust-sdk/.gitignore new file mode 100644 index 0000000..5d2348d --- /dev/null +++ b/packages/rust-sdk/.gitignore @@ -0,0 +1,17 @@ +# Rust +/target/ +**/*.rs.bk +*.pdb +Cargo.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment +.env +.env.local diff --git a/packages/rust-sdk/Cargo.toml b/packages/rust-sdk/Cargo.toml new file mode 100644 index 0000000..dc05a57 --- /dev/null +++ b/packages/rust-sdk/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "turbodocx_sdk" +version = "0.1.0" +edition = "2021" +authors = ["TurboDocx Team"] +description = "Official Rust SDK for TurboDocx API - Document generation and digital signatures" +license = "MIT" +repository = "https://github.com/turbodocx/sdk" +keywords = ["turbodocx", "document", "template", "signature", "pdf"] +categories = ["api-bindings"] + +[dependencies] +reqwest = { version = "0.12", features = ["json", "multipart", "rustls-tls"], default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.0", features = ["full"] } +thiserror = "1.0" +once_cell = "1.19" + +[dev-dependencies] +tokio-test = "0.4" diff --git a/packages/rust-sdk/LICENSE b/packages/rust-sdk/LICENSE new file mode 100644 index 0000000..907866e --- /dev/null +++ b/packages/rust-sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 TurboDocx + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/rust-sdk/README.md b/packages/rust-sdk/README.md new file mode 100644 index 0000000..a14d32d --- /dev/null +++ b/packages/rust-sdk/README.md @@ -0,0 +1,786 @@ +# TurboDocx Rust SDK + +Official Rust SDK for TurboDocx - Digital Signatures and Document Generation Platform. + +[![Crates.io](https://img.shields.io/crates/v/turbodocx-sdk.svg)](https://crates.io/crates/turbodocx-sdk) +[![Documentation](https://docs.rs/turbodocx-sdk/badge.svg)](https://docs.rs/turbodocx-sdk) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## Features + +- **TurboSign** - Digital signature workflows with comprehensive field types +- **TurboTemplate** - Advanced document generation with templating +- Async/await support with Tokio +- Type-safe API with comprehensive error handling +- Zero system dependencies (uses rustls-tls) +- Environment variable configuration + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +turbodocx-sdk = "0.1.0" +tokio = { version = "1.0", features = ["full"] } +``` + +## Quick Start + +### TurboSign - Digital Signatures + +```rust +use turbodocx_sdk::{ + TurboSign, SendSignatureRequest, Recipient, Field, SignatureFieldType, + http::HttpClientConfig +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Configure TurboSign + TurboSign::configure( + HttpClientConfig::new("your-api-key") + .with_org_id("your-org-id") + .with_sender_email("support@yourcompany.com") + .with_sender_name("Your Company") + )?; + + // Send signature request + let request = SendSignatureRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![ + Recipient::new("John Doe", "john@example.com", 1) + ], + fields: vec![ + Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "john@example.com" + ) + ], + document_name: Some("Service Agreement".to_string()), + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + let response = TurboSign::send_signature(request).await?; + println!("Document ID: {}", response.document_id); + + Ok(()) +} +``` + +### TurboTemplate - Document Generation + +```rust +use turbodocx_sdk::{ + TurboTemplate, GenerateTemplateRequest, TemplateVariable, + OutputFormat, http::HttpClientConfig +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Configure TurboTemplate + TurboTemplate::configure( + HttpClientConfig::new("your-api-key") + .with_org_id("your-org-id") + )?; + + // Generate document + let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::text("name", "John Doe"), + TemplateVariable::text("company", "Acme Corp"), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("contract.pdf".to_string()), + }; + + let response = TurboTemplate::generate(request).await?; + println!("Deliverable ID: {}", response.deliverable_id); + + Ok(()) +} +``` + +## Configuration + +The SDK can be configured via environment variables or programmatically: + +### Environment Variables + +```bash +export TURBODOCX_API_KEY="your-api-key" +export TURBODOCX_ORG_ID="your-org-id" +export TURBODOCX_SENDER_EMAIL="support@yourcompany.com" # For TurboSign +export TURBODOCX_SENDER_NAME="Your Company" # For TurboSign +export TURBODOCX_BASE_URL="https://api.turbodocx.com" # Optional +``` + +### Programmatic Configuration + +```rust +use turbodocx_sdk::http::HttpClientConfig; + +let config = HttpClientConfig::new("your-api-key") + .with_org_id("your-org-id") + .with_sender_email("support@company.com") + .with_sender_name("Company Support") + .with_base_url("https://api.turbodocx.com"); + +// Configure TurboSign +TurboSign::configure(config.clone())?; + +// Configure TurboTemplate +TurboTemplate::configure(config)?; +``` + +--- + +## TurboSign - Digital Signatures + +TurboSign enables you to send documents for digital signatures with customizable fields and workflows. + +### Core Concepts + +- **Recipients**: People who need to sign the document +- **Fields**: Signature, date, text, and other input fields +- **Positioning**: Coordinate-based or template anchor-based +- **Workflows**: Review links (preview) or direct sending + +### Send Signature Request + +Send a document for signatures immediately: + +```rust +use turbodocx_sdk::{TurboSign, SendSignatureRequest, Recipient, Field, SignatureFieldType}; + +let request = SendSignatureRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![ + Recipient::new("Alice Johnson", "alice@example.com", 1), + Recipient::new("Bob Smith", "bob@example.com", 2), + ], + fields: vec![ + Field::coordinate_based( + SignatureFieldType::Signature, + 1, // page + 100.0, // x + 500.0, // y + 200.0, // width + 50.0, // height + "alice@example.com" + ), + Field::coordinate_based( + SignatureFieldType::Date, + 1, + 320.0, + 500.0, + 100.0, + 30.0, + "alice@example.com" + ), + ], + document_name: Some("Employment Contract".to_string()), + document_description: Some("Annual employment agreement".to_string()), + sender_name: Some("HR Team".to_string()), + sender_email: Some("hr@company.com".to_string()), + cc_emails: Some(vec!["manager@company.com".to_string()]), +}; + +let response = TurboSign::send_signature(request).await?; +println!("Document ID: {}", response.document_id); +println!("Message: {}", response.message); +``` + +### Create Review Link + +Create a signature request without sending emails (for preview): + +```rust +use turbodocx_sdk::{TurboSign, CreateSignatureReviewLinkRequest}; + +let request = CreateSignatureReviewLinkRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![ + Recipient::new("John Doe", "john@example.com", 1) + ], + fields: vec![ + Field::anchor_based( + SignatureFieldType::Signature, + "{ClientSignature}", + "john@example.com" + ) + ], + document_name: Some("Service Agreement".to_string()), + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, +}; + +let response = TurboSign::create_signature_review_link(request).await?; +println!("Document ID: {}", response.document_id); +println!("Status: {}", response.status); +if let Some(preview_url) = response.preview_url { + println!("Preview URL: {}", preview_url); +} +``` + +### Field Types + +TurboSign supports 11 different field types: + +```rust +use turbodocx_sdk::SignatureFieldType; + +// Signature fields +SignatureFieldType::Signature // Full signature +SignatureFieldType::Initial // Initials only + +// Date fields +SignatureFieldType::Date // Date picker + +// Text fields +SignatureFieldType::Text // Free-form text +SignatureFieldType::FullName // Full name +SignatureFieldType::FirstName // First name only +SignatureFieldType::LastName // Last name only +SignatureFieldType::Email // Email address +SignatureFieldType::Title // Job title +SignatureFieldType::Company // Company name + +// Other fields +SignatureFieldType::Checkbox // Checkbox (true/false) +``` + +### Field Positioning + +#### Coordinate-Based Positioning + +Place fields at exact coordinates on the page: + +```rust +let field = Field::coordinate_based( + SignatureFieldType::Signature, + 1, // page number + 100.0, // x coordinate (from left) + 500.0, // y coordinate (from top) + 200.0, // width + 50.0, // height + "recipient@example.com" +); +``` + +#### Template Anchor-Based Positioning + +Place fields dynamically using text anchors in the document: + +```rust +// Simple anchor (replaces the anchor text) +let field = Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "recipient@example.com" +); + +// Advanced anchor with custom placement +let mut field = Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "recipient@example.com" +); + +field.template = Some(TemplateAnchor { + anchor: Some("{SignHere}".to_string()), + search_text: None, + placement: Some(Placement::After), // Place after the anchor + size: Some(FieldSize { + width: 200.0, + height: 50.0, + }), + offset: Some(FieldOffset { + x: 10.0, // 10 pixels to the right + y: 0.0, // Same vertical position + }), + case_sensitive: Some(false), + use_regex: Some(false), +}); +``` + +### Placement Options + +```rust +use turbodocx_sdk::Placement; + +Placement::Replace // Replace the anchor text +Placement::Before // Place before the anchor +Placement::After // Place after the anchor +Placement::Above // Place above the anchor +Placement::Below // Place below the anchor +``` + +### Document Management + +#### Get Document Status + +```rust +let status = TurboSign::get_status("document-id").await?; +println!("Status: {}", status.status); +println!("Created: {}", status.created_at); +``` + +#### Get Audit Trail + +```rust +let audit_trail = TurboSign::get_audit_trail("document-id").await?; +println!("Document: {}", audit_trail.document.name); +println!("Status: {}", audit_trail.document.status); + +for entry in audit_trail.audit_trail { + println!("{}: {} by {}", + entry.timestamp, + entry.action_type, + entry.user.email + ); +} +``` + +#### Void Document + +```rust +let response = TurboSign::void_document( + "document-id", + Some("Contract terms changed") +).await?; +println!("{}", response.message); +``` + +#### Resend Emails + +```rust +let response = TurboSign::resend_emails( + "document-id", + vec!["recipient-id-1", "recipient-id-2"] +).await?; +println!("Sent to {} recipients", response.recipient_count); +``` + +#### Download Signed Document + +```rust +let download_url = TurboSign::download("document-id").await?; +println!("Download from: {}", download_url); + +// Use the URL to download the file +// The URL is typically valid for a limited time +``` + +### Multiple Recipients with Signing Order + +```rust +let request = SendSignatureRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + recipients: vec![ + Recipient::new("First Signer", "first@example.com", 1), + Recipient::new("Second Signer", "second@example.com", 2), + Recipient::new("Final Signer", "final@example.com", 3), + ], + fields: vec![ + // Fields for first signer + Field::coordinate_based( + SignatureFieldType::Signature, + 1, 100.0, 500.0, 200.0, 50.0, + "first@example.com" + ), + // Fields for second signer + Field::coordinate_based( + SignatureFieldType::Signature, + 2, 100.0, 500.0, 200.0, 50.0, + "second@example.com" + ), + // Fields for final signer + Field::coordinate_based( + SignatureFieldType::Signature, + 3, 100.0, 500.0, 200.0, 50.0, + "final@example.com" + ), + ], + // ... other fields +}; +``` + +### Field Customization + +```rust +let mut field = Field::coordinate_based( + SignatureFieldType::Text, + 1, 100.0, 500.0, 300.0, 100.0, + "recipient@example.com" +); + +// Customize field properties +field.is_multiline = Some(true); +field.is_readonly = Some(false); +field.required = Some(true); +field.default_value = Some("Enter text here".to_string()); +field.background_color = Some("#FFFF00".to_string()); +field.tooltip = Some("Please enter your comments".to_string()); +field.font_size = Some(12.0); +field.font_family = Some("Arial".to_string()); +``` + +### Using with TurboTemplate Deliverables + +You can send TurboTemplate-generated documents for signature: + +```rust +// 1. Generate document with TurboTemplate +let template_request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::text("client_name", "John Doe"), + TemplateVariable::text("contract_date", "2024-01-15"), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("contract.pdf".to_string()), +}; + +let template_response = TurboTemplate::generate(template_request).await?; +let deliverable_id = template_response.deliverable_id; + +// 2. Send for signature using the deliverable_id +let sign_request = SendSignatureRequest { + deliverable_id: Some(deliverable_id), + file_link: None, + file: None, + file_name: None, + template_id: None, + recipients: vec![ + Recipient::new("John Doe", "john@example.com", 1) + ], + fields: vec![ + Field::anchor_based( + SignatureFieldType::Signature, + "{ClientSignature}", + "john@example.com" + ) + ], + document_name: Some("Contract".to_string()), + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, +}; + +let sign_response = TurboSign::send_signature(sign_request).await?; +``` + +--- + +## TurboTemplate - Document Generation + +TurboTemplate enables advanced document generation with variable substitution, loops, conditionals, and more. + +### Simple Variable Substitution + +```rust +use turbodocx_sdk::{TurboTemplate, GenerateTemplateRequest, TemplateVariable, OutputFormat}; + +let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::text("name", "John Doe"), + TemplateVariable::text("company", "Acme Corporation"), + TemplateVariable::text("date", "2024-01-15"), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("document.pdf".to_string()), +}; + +let response = TurboTemplate::generate(request).await?; +println!("Deliverable ID: {}", response.deliverable_id); +println!("Download URL: {}", response.download_url); +``` + +### Nested Objects + +```rust +use serde_json::json; + +let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::object("client", json!({ + "name": "John Doe", + "email": "john@example.com", + "address": { + "street": "123 Main St", + "city": "New York", + "zip": "10001" + } + })), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("document.pdf".to_string()), +}; +``` + +In your template, use: `{{client.name}}`, `{{client.address.city}}`, etc. + +### Array Loops + +```rust +let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::object("items", json!([ + { + "name": "Product A", + "price": 99.99, + "quantity": 2 + }, + { + "name": "Product B", + "price": 149.99, + "quantity": 1 + } + ])), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("invoice.pdf".to_string()), +}; +``` + +In your template: +``` +{{#each items}} + {{name}} - ${{price}} x {{quantity}} +{{/each}} +``` + +### Conditionals + +```rust +let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::text("customer_type", "premium"), + TemplateVariable::text("discount", "20%"), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("offer.pdf".to_string()), +}; +``` + +In your template: +``` +{{#if customer_type == "premium"}} + Special discount: {{discount}} +{{/if}} +``` + +### Images + +```rust +use turbodocx_sdk::VariableMimeType; + +let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::new( + "company_logo", + "https://example.com/logo.png", + VariableMimeType::Image + ), + TemplateVariable::new( + "signature", + "data:image/png;base64,iVBORw0KGgoAAAANS...", + VariableMimeType::Image + ), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("letterhead.pdf".to_string()), +}; +``` + +### Expressions + +```rust +let request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![ + TemplateVariable::text("price", "100"), + TemplateVariable::text("quantity", "5"), + TemplateVariable::text("tax_rate", "0.08"), + ], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("invoice.pdf".to_string()), +}; +``` + +In your template: +``` +Subtotal: {{price * quantity}} +Tax: {{price * quantity * tax_rate}} +Total: {{price * quantity * (1 + tax_rate)}} +``` + +### Output Formats + +```rust +use turbodocx_sdk::OutputFormat; + +// Generate as PDF +let pdf_request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![], + output_format: Some(OutputFormat::Pdf), + output_filename: Some("document.pdf".to_string()), +}; + +// Generate as DOCX +let docx_request = GenerateTemplateRequest { + template_id: "template-uuid".to_string(), + variables: vec![], + output_format: Some(OutputFormat::Docx), + output_filename: Some("document.docx".to_string()), +}; +``` + +### Advanced Templating Features + +TurboTemplate supports powerful template expressions: + +- **Variable Substitution**: `{{variable_name}}` +- **Nested Properties**: `{{user.address.city}}` +- **Array Loops**: `{{#each items}}...{{/each}}` +- **Conditionals**: `{{#if condition}}...{{/if}}` +- **Expressions**: `{{price * quantity}}` +- **Comparisons**: `{{#if age > 18}}...{{/if}}` +- **Logical Operators**: `{{#if premium && active}}...{{/if}}` + +--- + +## Error Handling + +The SDK uses a comprehensive error type that covers all failure scenarios: + +```rust +use turbodocx_sdk::TurboDocxError; + +match TurboSign::send_signature(request).await { + Ok(response) => { + println!("Success: {}", response.document_id); + } + Err(TurboDocxError::Authentication(msg)) => { + eprintln!("Auth error: {}", msg); + } + Err(TurboDocxError::InvalidRequest(msg)) => { + eprintln!("Invalid request: {}", msg); + } + Err(TurboDocxError::NotFound(msg)) => { + eprintln!("Not found: {}", msg); + } + Err(TurboDocxError::RateLimitExceeded(msg)) => { + eprintln!("Rate limit: {}", msg); + } + Err(TurboDocxError::ServerError(msg)) => { + eprintln!("Server error: {}", msg); + } + Err(e) => { + eprintln!("Error: {}", e); + } +} +``` + +### Error Types + +- `Authentication` - Invalid API key or credentials +- `InvalidRequest` - Malformed request or missing required fields +- `NotFound` - Resource not found +- `RateLimitExceeded` - Too many requests +- `ServerError` - Server-side error +- `Network` - Network connectivity issues +- `Serialization` - JSON serialization/deserialization errors +- `Other` - Other errors + +--- + +## Testing + +Run the test suite: + +```bash +# Run all tests +cargo test + +# Run tests with output +cargo test -- --nocapture + +# Run specific test module +cargo test turbosign_test + +# Run with coverage +cargo test --all-features +``` + +The SDK includes comprehensive tests: +- Unit tests for type construction and serialization +- Integration tests for API operations +- Documentation tests for code examples + +--- + +## Examples + +See the `examples/` directory for complete working examples: + +- [`sign.rs`](examples/sign.rs) - Digital signature workflows +- [`simple.rs`](examples/simple.rs) - Basic document generation +- [`advanced.rs`](examples/advanced.rs) - Advanced templating features + +Run examples: + +```bash +# Run TurboSign example +cargo run --example sign + +# Run TurboTemplate simple example +cargo run --example simple + +# Run TurboTemplate advanced example +cargo run --example advanced +``` + +--- + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Support + +For issues and questions: +- GitHub Issues: [https://github.com/turbodocx/sdk/issues](https://github.com/turbodocx/sdk/issues) +- Documentation: [https://docs.turbodocx.com](https://docs.turbodocx.com) +- Email: support@turbodocx.com diff --git a/packages/rust-sdk/examples/advanced.rs b/packages/rust-sdk/examples/advanced.rs new file mode 100644 index 0000000..34819de --- /dev/null +++ b/packages/rust-sdk/examples/advanced.rs @@ -0,0 +1,151 @@ +use serde_json::json; +use std::collections::HashMap; +use turbodocx_sdk::{GenerateTemplateRequest, OutputFormat, TemplateVariable, TurboTemplate}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Example 1: Complex Invoice with Multiple Features + println!("=== Example 1: Complex Invoice ==="); + + let customer_data = json!({ + "name": "Acme Corporation", + "contact": "Jane Smith", + "email": "jane@acme.com", + "address": { + "street": "123 Business Ave", + "city": "San Francisco", + "state": "CA", + "zip": "94102" + } + }); + + let items = vec![ + json!({"description": "Consulting Services", "quantity": 40, "rate": 150}), + json!({"description": "Software License", "quantity": 1, "rate": 5000}), + json!({"description": "Support Package", "quantity": 12, "rate": 500}), + ]; + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![ + // Customer information (nested object) + TemplateVariable::advanced_engine("{customer}", "customer", customer_data)?, + // Invoice metadata + TemplateVariable::simple("{invoice_number}", "invoice_number", "INV-2024-001"), + TemplateVariable::simple("{invoice_date}", "invoice_date", "2024-01-15"), + TemplateVariable::simple("{due_date}", "due_date", "2024-02-15"), + // Line items (array for loop) + TemplateVariable::loop_var("{items}", "items", items)?, + // Totals + TemplateVariable::simple("{subtotal}", "subtotal", 17000), + TemplateVariable::simple("{tax_rate}", "tax_rate", 0.08), + TemplateVariable::simple("{tax_amount}", "tax_amount", 1360), + TemplateVariable::simple("{total}", "total", 18360), + // Terms + TemplateVariable::simple("{payment_terms}", "payment_terms", "Net 30"), + TemplateVariable::simple("{notes}", "notes", "Thank you for your business!"), + ], + ) + .with_name("Invoice - Acme Corporation") + .with_description("Monthly invoice") + .with_output_format(OutputFormat::Pdf); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 2: Using Expressions for Calculations + println!("\n=== Example 2: Expressions ==="); + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![ + TemplateVariable::simple("{price}", "price", 100), + TemplateVariable::simple("{quantity}", "quantity", 5), + TemplateVariable::simple("{tax_rate}", "tax_rate", 0.08), + ], + ) + .with_name("Expressions Document") + .with_description("Arithmetic expressions example"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 3: Using All Helper Functions + println!("\n=== Example 3: All Helper Functions ==="); + + let company_data = json!({ + "name": "TechCorp", + "headquarters": "Mountain View, CA", + "employees": 500 + }); + + let departments = vec![ + json!({"name": "Engineering", "headcount": 200}), + json!({"name": "Sales", "headcount": 150}), + json!({"name": "Marketing", "headcount": 100}), + ]; + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![ + // Advanced engine variable (nested object) + TemplateVariable::advanced_engine("{company}", "company", company_data)?, + // Loop variable (array) + TemplateVariable::loop_var("{departments}", "departments", departments)?, + // Conditional + TemplateVariable::conditional("{show_financials}", "show_financials", true), + // Image + TemplateVariable::image( + "{company_logo}", + "company_logo", + "https://example.com/logo.png", + ), + ], + ) + .with_name("Helper Functions Document") + .with_description("Using helper functions example"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 4: Custom Options + println!("\n=== Example 4: Custom Options ==="); + + let mut metadata = HashMap::new(); + metadata.insert("customField".to_string(), json!("value")); + metadata.insert("department".to_string(), json!("Sales")); + metadata.insert("region".to_string(), json!("North America")); + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![TemplateVariable::simple( + "{title}", + "title", + "Custom Document", + )], + ) + .with_name("Custom Options Document") + .with_description("Document with custom options") + .with_font_replacement(true, Some("Arial")) + .with_output_format(OutputFormat::Pdf) + .with_metadata(metadata); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 5: HTML Content + println!("\n=== Example 5: HTML Content ==="); + + let html_content = r#"

    Welcome

    This is formatted text.

    "#; + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![TemplateVariable::html("{content}", "content", html_content)], + ) + .with_name("HTML Content Document"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + Ok(()) +} diff --git a/packages/rust-sdk/examples/sign.rs b/packages/rust-sdk/examples/sign.rs new file mode 100644 index 0000000..ec86586 --- /dev/null +++ b/packages/rust-sdk/examples/sign.rs @@ -0,0 +1,131 @@ +use turbodocx_sdk::{ + CreateSignatureReviewLinkRequest, Field, Recipient, SendSignatureRequest, SignatureFieldType, + TurboSign, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Example 1: Create Review Link (Coordinate-Based Fields) + println!("=== Example 1: Create Review Link (Coordinate-Based) ==="); + + let request = CreateSignatureReviewLinkRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![ + Recipient::new("John Doe", "john@example.com", 1), + Recipient::new("Jane Smith", "jane@example.com", 2), + ], + fields: vec![ + Field::coordinate_based( + SignatureFieldType::Signature, + 1, // page + 100.0, // x + 500.0, // y + 200.0, // width + 50.0, // height + "john@example.com", + ), + Field::coordinate_based( + SignatureFieldType::Date, + 1, + 320.0, + 500.0, + 100.0, + 30.0, + "john@example.com", + ), + Field::coordinate_based( + SignatureFieldType::Signature, + 2, + 100.0, + 500.0, + 200.0, + 50.0, + "jane@example.com", + ), + ], + document_name: Some("Service Agreement".to_string()), + document_description: Some("Annual service contract".to_string()), + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + let response = TurboSign::create_signature_review_link(request).await?; + println!("✓ Document ID: {}", response.document_id); + println!("✓ Status: {}", response.status); + if let Some(preview_url) = response.preview_url { + println!("✓ Preview URL: {}", preview_url); + } + + // Example 2: Send Signature (Template Anchor-Based Fields) + println!("\n=== Example 2: Send Signature (Template Anchor-Based) ==="); + + let request = SendSignatureRequest { + deliverable_id: Some("deliverable-uuid".to_string()), + file: None, + file_name: None, + file_link: None, + template_id: None, + recipients: vec![Recipient::new("Alice Johnson", "alice@example.com", 1)], + fields: vec![ + Field::anchor_based( + SignatureFieldType::Signature, + "{ClientSignature}", + "alice@example.com", + ), + Field::anchor_based( + SignatureFieldType::Date, + "{SignDate}", + "alice@example.com", + ), + Field::anchor_based( + SignatureFieldType::FullName, + "{ClientName}", + "alice@example.com", + ), + ], + document_name: Some("Engagement Letter".to_string()), + document_description: None, + sender_name: Some("Support Team".to_string()), + sender_email: Some("support@company.com".to_string()), + cc_emails: Some(vec!["manager@company.com".to_string()]), + }; + + let response = TurboSign::send_signature(request).await?; + println!("✓ Document ID: {}", response.document_id); + println!("✓ Message: {}", response.message); + + // Example 3: Document Management + let document_id = "doc-uuid"; + + println!("\n=== Example 3: Get Document Status ==="); + let status = TurboSign::get_status(document_id).await?; + println!("✓ Status: {}", status.status); + + println!("\n=== Example 4: Get Audit Trail ==="); + let audit_trail = TurboSign::get_audit_trail(document_id).await?; + println!("✓ Document: {}", audit_trail.document.name); + println!("✓ Audit entries: {}", audit_trail.audit_trail.len()); + for entry in audit_trail.audit_trail.iter().take(3) { + println!(" - {}: {}", entry.timestamp, entry.action_type); + } + + println!("\n=== Example 5: Resend Emails ==="); + let response = TurboSign::resend_emails(document_id, vec!["recipient-id"]).await?; + println!("✓ Sent to {} recipients", response.recipient_count); + + println!("\n=== Example 6: Void Document ==="); + let response = + TurboSign::void_document(document_id, Some("Contract terms changed")).await?; + println!("✓ {}", response.message); + + println!("\n=== Example 7: Download Signed Document ==="); + let download_url = TurboSign::download(document_id).await?; + println!("✓ Download URL: {}", download_url); + + Ok(()) +} diff --git a/packages/rust-sdk/examples/simple.rs b/packages/rust-sdk/examples/simple.rs new file mode 100644 index 0000000..d5e6eae --- /dev/null +++ b/packages/rust-sdk/examples/simple.rs @@ -0,0 +1,96 @@ +use serde_json::json; +use turbodocx_sdk::{GenerateTemplateRequest, TemplateVariable, TurboTemplate}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Example 1: Simple Variable Substitution + println!("=== Example 1: Simple Variable Substitution ==="); + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![ + TemplateVariable::simple("{customer_name}", "customer_name", "John Doe"), + TemplateVariable::simple("{order_total}", "order_total", 1500), + TemplateVariable::simple("{order_date}", "order_date", "2024-01-01"), + ], + ) + .with_name("Simple Substitution Document") + .with_description("Basic variable substitution example"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 2: Nested Objects with Dot Notation + println!("\n=== Example 2: Nested Objects ==="); + let user_data = json!({ + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "profile": { + "company": "Acme Inc", + "title": "Software Engineer", + "location": "San Francisco, CA" + } + }); + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![TemplateVariable::advanced_engine( + "{user}", "user", user_data, + )?], + ) + .with_name("Nested Objects Document") + .with_description("Nested object with dot notation example"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 3: Array Loops + println!("\n=== Example 3: Array Loops ==="); + let items = vec![ + json!({"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}), + json!({"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}), + json!({"name": "Item C", "quantity": 10, "price": 50, "sku": "SKU-003"}), + ]; + + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![TemplateVariable::loop_var("{items}", "items", items)?], + ) + .with_name("Array Loops Document") + .with_description("Array loop iteration example"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 4: Conditionals + println!("\n=== Example 4: Conditionals ==="); + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![ + TemplateVariable::conditional("{is_premium}", "is_premium", true), + TemplateVariable::conditional("{discount}", "discount", 0.2), + ], + ) + .with_name("Conditionals Document") + .with_description("Boolean conditional example"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + // Example 5: Images + println!("\n=== Example 5: Images ==="); + let request = GenerateTemplateRequest::new( + "your-template-id", + vec![ + TemplateVariable::simple("{title}", "title", "Quarterly Report"), + TemplateVariable::image("{logo}", "logo", "https://example.com/logo.png"), + ], + ) + .with_name("Document with Images") + .with_description("Using image variables"); + + let response = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", response.deliverable_id); + + Ok(()) +} diff --git a/packages/rust-sdk/src/http.rs b/packages/rust-sdk/src/http.rs new file mode 100644 index 0000000..ee9eeaf --- /dev/null +++ b/packages/rust-sdk/src/http.rs @@ -0,0 +1,212 @@ +use crate::utils::{Result, TurboDocxError}; +use reqwest::{header, Client, Method, Response}; +use serde::{de::DeserializeOwned, Serialize}; +use std::env; + +/// Configuration for the HTTP client +#[derive(Debug, Clone)] +pub struct HttpClientConfig { + /// TurboDocx API key + pub api_key: Option, + + /// OAuth access token (alternative to API key) + pub access_token: Option, + + /// Base URL for the API + pub base_url: String, + + /// Organization ID + pub org_id: Option, + + /// Sender email (required for TurboSign) + pub sender_email: Option, + + /// Sender name (for email display) + pub sender_name: Option, +} + +impl Default for HttpClientConfig { + fn default() -> Self { + Self { + api_key: env::var("TURBODOCX_API_KEY").ok(), + access_token: None, + base_url: env::var("TURBODOCX_BASE_URL") + .unwrap_or_else(|_| "https://api.turbodocx.com".to_string()), + org_id: env::var("TURBODOCX_ORG_ID").ok(), + sender_email: env::var("TURBODOCX_SENDER_EMAIL").ok(), + sender_name: env::var("TURBODOCX_SENDER_NAME").ok(), + } + } +} + +impl HttpClientConfig { + /// Create a new configuration with the given API key + pub fn new>(api_key: S) -> Self { + Self { + api_key: Some(api_key.into()), + ..Default::default() + } + } + + /// Set the access token + pub fn with_access_token>(mut self, token: S) -> Self { + self.access_token = Some(token.into()); + self + } + + /// Set the base URL + pub fn with_base_url>(mut self, url: S) -> Self { + self.base_url = url.into(); + self + } + + /// Set the organization ID + pub fn with_org_id>(mut self, org_id: S) -> Self { + self.org_id = Some(org_id.into()); + self + } + + /// Set sender email + pub fn with_sender_email>(mut self, email: S) -> Self { + self.sender_email = Some(email.into()); + self + } + + /// Set sender name + pub fn with_sender_name>(mut self, name: S) -> Self { + self.sender_name = Some(name.into()); + self + } +} + +/// HTTP client for making API requests +pub struct HttpClient { + pub(crate) config: HttpClientConfig, + client: Client, +} + +impl HttpClient { + /// Create a new HTTP client with the given configuration + pub fn new(config: HttpClientConfig) -> Result { + let client = Client::builder() + .build() + .map_err(|e| TurboDocxError::Network(e.to_string()))?; + + Ok(Self { config, client }) + } + + /// Make a request to the API + pub async fn request( + &self, + method: Method, + path: &str, + body: Option, + ) -> Result { + let url = format!("{}{}", self.config.base_url, path); + + let mut request = self.client.request(method, &url); + + // Add authorization header + if let Some(ref api_key) = self.config.api_key { + request = request.header(header::AUTHORIZATION, format!("Bearer {}", api_key)); + } else if let Some(ref token) = self.config.access_token { + request = request.header(header::AUTHORIZATION, format!("Bearer {}", token)); + } + + // Add organization ID header + if let Some(ref org_id) = self.config.org_id { + request = request.header("x-rapiddocx-org-id", org_id); + } + + // Add content type + request = request.header(header::CONTENT_TYPE, "application/json"); + + // Add body if provided + if let Some(body) = body { + request = request.json(&body); + } + + let response = request.send().await?; + self.handle_response(response).await + } + + /// Handle the API response + async fn handle_response(&self, response: Response) -> Result { + let status = response.status(); + + if !status.is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + + return Err(match status.as_u16() { + 401 => TurboDocxError::Authentication(error_text), + 400 => TurboDocxError::Validation(error_text), + 404 => TurboDocxError::NotFound(error_text), + 429 => TurboDocxError::RateLimit(error_text), + _ => TurboDocxError::Api { + status: status.as_u16(), + message: error_text, + }, + }); + } + + // Try to parse as JSON + let json_value: serde_json::Value = response.json().await?; + + // Check if response is wrapped in { data: ... } + let data = if let Some(data) = json_value.get("data") { + data.clone() + } else { + json_value + }; + + // Deserialize to target type + serde_json::from_value(data).map_err(TurboDocxError::Serialization) + } + + /// Make a GET request + pub async fn get(&self, path: &str) -> Result { + self.request(Method::GET, path, None::<()>).await + } + + /// Make a POST request + pub async fn post(&self, path: &str, body: impl Serialize) -> Result { + self.request(Method::POST, path, Some(body)).await + } + + /// Make a PUT request + pub async fn put(&self, path: &str, body: impl Serialize) -> Result { + self.request(Method::PUT, path, Some(body)).await + } + + /// Make a DELETE request + pub async fn delete(&self, path: &str) -> Result { + self.request(Method::DELETE, path, None::<()>).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_default() { + let config = HttpClientConfig::default(); + assert_eq!(config.base_url, "https://api.turbodocx.com"); + } + + #[test] + fn test_config_builder() { + let config = HttpClientConfig::new("test-api-key") + .with_base_url("https://test.example.com") + .with_org_id("org-123") + .with_sender_email("test@example.com"); + + assert_eq!(config.api_key, Some("test-api-key".to_string())); + assert_eq!(config.base_url, "https://test.example.com"); + assert_eq!(config.org_id, Some("org-123".to_string())); + assert_eq!(config.sender_email, Some("test@example.com".to_string())); + } +} diff --git a/packages/rust-sdk/src/lib.rs b/packages/rust-sdk/src/lib.rs new file mode 100644 index 0000000..52d1408 --- /dev/null +++ b/packages/rust-sdk/src/lib.rs @@ -0,0 +1,87 @@ +//! # TurboDocx Rust SDK +//! +//! Official Rust SDK for TurboDocx API - Advanced document generation and digital signatures +//! +//! ## Features +//! +//! - **TurboTemplate**: Advanced document generation with Angular-like templating +//! - Variable substitution, nested objects, loops, conditionals, expressions +//! - **Type-safe API**: Strongly typed with comprehensive error handling +//! - **Async/await**: Built on tokio and reqwest for high performance +//! - **Environment variables**: Auto-configuration from environment +//! +//! ## Quick Start +//! +//! ```no_run +//! use turbodocx_sdk::{TurboTemplate, GenerateTemplateRequest, TemplateVariable}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Configure (or use environment variables) +//! use turbodocx_sdk::http::HttpClientConfig; +//! TurboTemplate::configure( +//! HttpClientConfig::new("your-api-key") +//! .with_org_id("your-org-id") +//! )?; +//! +//! // Generate a document +//! let request = GenerateTemplateRequest::new( +//! "your-template-id", +//! vec![ +//! TemplateVariable::simple("{name}", "name", "John Doe"), +//! TemplateVariable::simple("{amount}", "amount", 1000), +//! ], +//! ) +//! .with_name("My Document"); +//! +//! let response = TurboTemplate::generate(request).await?; +//! println!("Deliverable ID: {:?}", response.deliverable_id); +//! +//! Ok(()) +//! } +//! ``` +//! +//! ## Environment Variables +//! +//! - `TURBODOCX_API_KEY`: Your TurboDocx API key +//! - `TURBODOCX_BASE_URL`: API base URL (defaults to https://api.turbodocx.com) +//! - `TURBODOCX_ORG_ID`: Organization ID +//! + +pub mod http; +pub mod modules; +pub mod types; +pub mod utils; + +// Re-export main types and modules +pub use http::{HttpClient, HttpClientConfig}; +pub use modules::{TurboSign, TurboTemplate}; +pub use types::{ + // Sign types + AuditTrailDocument, + AuditTrailEntry, + AuditTrailResponse, + AuditTrailUser, + CreateSignatureReviewLinkRequest, + CreateSignatureReviewLinkResponse, + DocumentStatusResponse, + Field, + FieldOffset, + FieldSize, + Placement, + Recipient, + RecipientStatus, + ResendEmailResponse, + SendSignatureRequest, + SendSignatureResponse, + SignatureFieldType, + TemplateAnchor, + VoidDocumentResponse, + // Template types + GenerateTemplateRequest, + GenerateTemplateResponse, + OutputFormat, + TemplateVariable, + VariableMimeType, +}; +pub use utils::{Result, TurboDocxError}; diff --git a/packages/rust-sdk/src/modules/mod.rs b/packages/rust-sdk/src/modules/mod.rs new file mode 100644 index 0000000..43eced5 --- /dev/null +++ b/packages/rust-sdk/src/modules/mod.rs @@ -0,0 +1,5 @@ +pub mod sign; +pub mod template; + +pub use sign::TurboSign; +pub use template::TurboTemplate; diff --git a/packages/rust-sdk/src/modules/sign.rs b/packages/rust-sdk/src/modules/sign.rs new file mode 100644 index 0000000..012723e --- /dev/null +++ b/packages/rust-sdk/src/modules/sign.rs @@ -0,0 +1,376 @@ +use crate::http::{HttpClient, HttpClientConfig}; +use crate::types::{ + AuditTrailResponse, CreateSignatureReviewLinkRequest, CreateSignatureReviewLinkResponse, + DocumentStatusResponse, ResendEmailResponse, SendSignatureRequest, SendSignatureResponse, + VoidDocumentResponse, +}; +use crate::utils::Result; +use once_cell::sync::OnceCell; +use std::sync::Mutex; + +static CLIENT: OnceCell>> = OnceCell::new(); + +/// TurboSign module for digital signature operations +/// +/// ## Features +/// - Create signature review links (prepare without sending emails) +/// - Send signature requests (prepare and send in one call) +/// - Void documents +/// - Resend signature request emails +/// - Get audit trail +/// - Get document status +/// - Download signed documents +/// +/// ## Configuration +/// +/// **Important:** senderEmail is REQUIRED for TurboSign operations. Without it, +/// emails will default to "API Service User via TurboSign". senderName is +/// strongly recommended to provide a better sender experience. +/// +/// ```no_run +/// use turbodocx_sdk::{TurboSign, http::HttpClientConfig}; +/// +/// TurboSign::configure( +/// HttpClientConfig::new("your-api-key") +/// .with_org_id("your-org-id") +/// .with_sender_email("support@yourcompany.com") // REQUIRED +/// .with_sender_name("Your Company Name") // Strongly recommended +/// )?; +/// # Ok::<(), turbodocx_sdk::TurboDocxError>(()) +/// ``` +pub struct TurboSign; + +impl TurboSign { + /// Configure the TurboSign module with custom settings + /// + /// # Arguments + /// + /// * `config` - HTTP client configuration with API credentials and sender info + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::{TurboSign, http::HttpClientConfig}; + /// + /// TurboSign::configure( + /// HttpClientConfig::new("your-api-key") + /// .with_org_id("your-org-id") + /// .with_sender_email("support@company.com") + /// .with_sender_name("Company Support") + /// )?; + /// # Ok::<(), turbodocx_sdk::TurboDocxError>(()) + /// ``` + pub fn configure(config: HttpClientConfig) -> Result<()> { + let client = HttpClient::new(config)?; + let cell = CLIENT.get_or_init(|| Mutex::new(None)); + let mut guard = cell.lock().unwrap(); + *guard = Some(client); + Ok(()) + } + + /// Get or create the HTTP client + fn get_client() -> Result { + let cell = CLIENT.get_or_init(|| Mutex::new(None)); + let mut guard = cell.lock().unwrap(); + + if guard.is_none() { + // Auto-initialize from environment variables + let config = HttpClientConfig::default(); + *guard = Some(HttpClient::new(config)?); + } + + // Clone the client + guard + .as_ref() + .map(|c| HttpClient::new(c.config.clone())) + .transpose()? + .ok_or_else(|| crate::utils::TurboDocxError::Other("Client not initialized".into())) + } + + /// Create signature review link without sending emails + /// + /// This uploads a document with signature fields and recipients, + /// but does NOT send signature request emails. Use this to preview + /// field placement before sending. + /// + /// # Arguments + /// + /// * `request` - Document, recipients, and fields configuration + /// + /// # Returns + /// + /// Document ready for review with preview URL + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::{TurboSign, CreateSignatureReviewLinkRequest, Recipient, Field, SignatureFieldType}; + /// + /// # async fn example() -> Result<(), Box> { + /// let request = CreateSignatureReviewLinkRequest { + /// file_link: Some("https://example.com/contract.pdf".to_string()), + /// file: None, + /// file_name: None, + /// deliverable_id: None, + /// template_id: None, + /// recipients: vec![ + /// Recipient::new("John Doe", "john@example.com", 1) + /// ], + /// fields: vec![ + /// Field::coordinate_based( + /// SignatureFieldType::Signature, + /// 1, 100.0, 500.0, 200.0, 50.0, + /// "john@example.com" + /// ) + /// ], + /// document_name: Some("Contract".to_string()), + /// document_description: None, + /// sender_name: None, + /// sender_email: None, + /// cc_emails: None, + /// }; + /// + /// let response = TurboSign::create_signature_review_link(request).await?; + /// println!("Preview URL: {:?}", response.preview_url); + /// # Ok(()) + /// # } + /// ``` + pub async fn create_signature_review_link( + request: CreateSignatureReviewLinkRequest, + ) -> Result { + let client = Self::get_client()?; + client + .post("/v1/signature/create-review-link", request) + .await + } + + /// Send signature request (prepare and send in single call) + /// + /// This uploads a document with signature fields and recipients, + /// and immediately sends signature request emails. + /// + /// # Arguments + /// + /// * `request` - Document, recipients, and fields configuration + /// + /// # Returns + /// + /// Document ID and confirmation message + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::{TurboSign, SendSignatureRequest, Recipient, Field, SignatureFieldType}; + /// + /// # async fn example() -> Result<(), Box> { + /// let request = SendSignatureRequest { + /// deliverable_id: Some("deliverable-uuid".to_string()), + /// file: None, + /// file_name: None, + /// file_link: None, + /// template_id: None, + /// recipients: vec![ + /// Recipient::new("John Doe", "john@example.com", 1) + /// ], + /// fields: vec![ + /// Field::anchor_based( + /// SignatureFieldType::Signature, + /// "{SignHere}", + /// "john@example.com" + /// ) + /// ], + /// document_name: Some("Contract".to_string()), + /// document_description: None, + /// sender_name: None, + /// sender_email: None, + /// cc_emails: None, + /// }; + /// + /// let response = TurboSign::send_signature(request).await?; + /// println!("Document ID: {}", response.document_id); + /// # Ok(()) + /// # } + /// ``` + pub async fn send_signature(request: SendSignatureRequest) -> Result { + let client = Self::get_client()?; + client.post("/v1/signature/send", request).await + } + + /// Void a signature request + /// + /// Cancels a signature request and notifies recipients. + /// + /// # Arguments + /// + /// * `document_id` - The document ID to void + /// * `reason` - Reason for voiding (optional) + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::TurboSign; + /// + /// # async fn example() -> Result<(), Box> { + /// let response = TurboSign::void_document( + /// "doc-uuid", + /// Some("Contract terms changed") + /// ).await?; + /// println!("{}", response.message); + /// # Ok(()) + /// # } + /// ``` + pub async fn void_document( + document_id: &str, + reason: Option<&str>, + ) -> Result { + let client = Self::get_client()?; + let mut body = serde_json::json!({ + "documentId": document_id + }); + if let Some(reason) = reason { + body["reason"] = serde_json::json!(reason); + } + client.post("/v1/signature/void", body).await + } + + /// Resend signature request emails to specific recipients + /// + /// # Arguments + /// + /// * `document_id` - The document ID + /// * `recipient_ids` - List of recipient IDs to resend to + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::TurboSign; + /// + /// # async fn example() -> Result<(), Box> { + /// let response = TurboSign::resend_emails( + /// "doc-uuid", + /// vec!["recipient-id-1", "recipient-id-2"] + /// ).await?; + /// println!("Sent to {} recipients", response.recipient_count); + /// # Ok(()) + /// # } + /// ``` + pub async fn resend_emails( + document_id: &str, + recipient_ids: Vec<&str>, + ) -> Result { + let client = Self::get_client()?; + let body = serde_json::json!({ + "documentId": document_id, + "recipientIds": recipient_ids + }); + client.post("/v1/signature/resend", body).await + } + + /// Get audit trail for a document + /// + /// Returns the complete signing history with cryptographic verification. + /// + /// # Arguments + /// + /// * `document_id` - The document ID + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::TurboSign; + /// + /// # async fn example() -> Result<(), Box> { + /// let audit_trail = TurboSign::get_audit_trail("doc-uuid").await?; + /// println!("Document: {}", audit_trail.document.name); + /// for entry in audit_trail.audit_trail { + /// println!("{}: {}", entry.timestamp, entry.action_type); + /// } + /// # Ok(()) + /// # } + /// ``` + pub async fn get_audit_trail(document_id: &str) -> Result { + let client = Self::get_client()?; + client + .get(&format!("/v1/signature/{}/audit-trail", document_id)) + .await + } + + /// Get document status + /// + /// Returns the current status of a signature request. + /// + /// # Arguments + /// + /// * `document_id` - The document ID + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::TurboSign; + /// + /// # async fn example() -> Result<(), Box> { + /// let status = TurboSign::get_status("doc-uuid").await?; + /// println!("Status: {}", status.status); + /// # Ok(()) + /// # } + /// ``` + pub async fn get_status(document_id: &str) -> Result { + let client = Self::get_client()?; + client + .get(&format!("/v1/signature/{}/status", document_id)) + .await + } + + /// Download signed document + /// + /// Returns a presigned S3 URL to download the completed document. + /// + /// # Arguments + /// + /// * `document_id` - The document ID + /// + /// # Returns + /// + /// Download URL (valid for limited time) + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::TurboSign; + /// + /// # async fn example() -> Result<(), Box> { + /// let download_url = TurboSign::download("doc-uuid").await?; + /// println!("Download from: {}", download_url); + /// # Ok(()) + /// # } + /// ``` + pub async fn download(document_id: &str) -> Result { + let client = Self::get_client()?; + let response: serde_json::Value = client + .get(&format!("/v1/signature/{}/download", document_id)) + .await?; + + response + .get("downloadUrl") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| { + crate::utils::TurboDocxError::Other("No download URL in response".into()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::HttpClientConfig; + + #[test] + fn test_configure() { + let config = HttpClientConfig::new("test-key") + .with_org_id("test-org") + .with_sender_email("test@example.com"); + let result = TurboSign::configure(config); + assert!(result.is_ok()); + } +} diff --git a/packages/rust-sdk/src/modules/template.rs b/packages/rust-sdk/src/modules/template.rs new file mode 100644 index 0000000..6b71b8e --- /dev/null +++ b/packages/rust-sdk/src/modules/template.rs @@ -0,0 +1,118 @@ +use crate::http::{HttpClient, HttpClientConfig}; +use crate::types::{GenerateTemplateRequest, GenerateTemplateResponse}; +use crate::utils::Result; +use once_cell::sync::OnceCell; +use std::sync::Mutex; + +static CLIENT: OnceCell>> = OnceCell::new(); + +/// TurboTemplate module for advanced document generation +/// +/// Supports Angular-like templating with features like: +/// - Variable substitution: {firstName} +/// - Nested objects with dot notation: {user.firstName} +/// - Loops: {#items}...{/items} +/// - Conditionals: {#isActive}...{/isActive} +/// - Expressions: {price + tax}, {quantity * price} +pub struct TurboTemplate; + +impl TurboTemplate { + /// Configure the TurboTemplate module with custom settings + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::TurboTemplate; + /// use turbodocx_sdk::http::HttpClientConfig; + /// + /// TurboTemplate::configure( + /// HttpClientConfig::new("your-api-key") + /// .with_org_id("your-org-id") + /// ); + /// ``` + pub fn configure(config: HttpClientConfig) -> Result<()> { + let client = HttpClient::new(config)?; + let cell = CLIENT.get_or_init(|| Mutex::new(None)); + let mut guard = cell.lock().unwrap(); + *guard = Some(client); + Ok(()) + } + + /// Get or create the HTTP client + fn get_client() -> Result { + let cell = CLIENT.get_or_init(|| Mutex::new(None)); + let mut guard = cell.lock().unwrap(); + + if guard.is_none() { + // Auto-initialize from environment variables + let config = HttpClientConfig::default(); + *guard = Some(HttpClient::new(config)?); + } + + // Clone the client (cheap because reqwest::Client uses Arc internally) + guard + .as_ref() + .map(|c| HttpClient::new(c.config.clone())) + .transpose()? + .ok_or_else(|| crate::utils::TurboDocxError::Other("Client not initialized".into())) + } + + /// Generate a document from a template + /// + /// # Arguments + /// + /// * `request` - The template generation request containing template ID and variables + /// + /// # Example + /// + /// ```no_run + /// use turbodocx_sdk::{TurboTemplate, GenerateTemplateRequest, TemplateVariable}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// let request = GenerateTemplateRequest::new( + /// "your-template-id", + /// vec![ + /// TemplateVariable::simple("{customer_name}", "customer_name", "John Doe"), + /// TemplateVariable::simple("{order_total}", "order_total", 1500), + /// ], + /// ) + /// .with_name("Invoice Document") + /// .with_description("Customer invoice"); + /// + /// let response = TurboTemplate::generate(request).await?; + /// println!("Deliverable ID: {:?}", response.deliverable_id); + /// Ok(()) + /// } + /// ``` + pub async fn generate(request: GenerateTemplateRequest) -> Result { + let client = Self::get_client()?; + client.post("/v1/deliverable", request).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::TemplateVariable; + + #[test] + fn test_configure() { + let config = HttpClientConfig::new("test-key"); + let result = TurboTemplate::configure(config); + assert!(result.is_ok()); + } + + #[test] + fn test_request_serialization() { + let request = GenerateTemplateRequest::new( + "template-123", + vec![TemplateVariable::simple("{name}", "name", "Test")], + ) + .with_name("Test Document"); + + let json = serde_json::to_string(&request).unwrap(); + assert!(json.contains("template-123")); + assert!(json.contains("Test Document")); + } +} diff --git a/packages/rust-sdk/src/types/mod.rs b/packages/rust-sdk/src/types/mod.rs new file mode 100644 index 0000000..5f7122f --- /dev/null +++ b/packages/rust-sdk/src/types/mod.rs @@ -0,0 +1,14 @@ +pub mod sign; +pub mod template; + +pub use sign::{ + AuditTrailDocument, AuditTrailEntry, AuditTrailResponse, AuditTrailUser, + CreateSignatureReviewLinkRequest, CreateSignatureReviewLinkResponse, DocumentStatusResponse, + Field, FieldOffset, FieldSize, Placement, Recipient, RecipientStatus, ResendEmailResponse, + SendSignatureRequest, SendSignatureResponse, SignatureFieldType, TemplateAnchor, + VoidDocumentResponse, +}; +pub use template::{ + GenerateTemplateRequest, GenerateTemplateResponse, OutputFormat, TemplateVariable, + VariableMimeType, +}; diff --git a/packages/rust-sdk/src/types/sign.rs b/packages/rust-sdk/src/types/sign.rs new file mode 100644 index 0000000..8b814a5 --- /dev/null +++ b/packages/rust-sdk/src/types/sign.rs @@ -0,0 +1,523 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Signature field type +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum SignatureFieldType { + Signature, + Initial, + Date, + Text, + FullName, + Title, + Company, + FirstName, + LastName, + Email, + Checkbox, +} + +/// Placement relative to anchor/searchText +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Placement { + Replace, + Before, + After, + Above, + Below, +} + +/// Template anchor configuration for dynamic field positioning +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TemplateAnchor { + /// Text anchor pattern like {TagName} + #[serde(skip_serializing_if = "Option::is_none")] + pub anchor: Option, + + /// Alternative: search for any text in document + #[serde(skip_serializing_if = "Option::is_none")] + pub search_text: Option, + + /// Where to place field relative to anchor/searchText + #[serde(skip_serializing_if = "Option::is_none")] + pub placement: Option, + + /// Size of the field + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// Offset from anchor position + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + + /// Case sensitive search (default: false) + #[serde(skip_serializing_if = "Option::is_none")] + pub case_sensitive: Option, + + /// Use regex for anchor/searchText (default: false) + #[serde(skip_serializing_if = "Option::is_none")] + pub use_regex: Option, +} + +/// Field size +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FieldSize { + pub width: f64, + pub height: f64, +} + +/// Field offset +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FieldOffset { + pub x: f64, + pub y: f64, +} + +/// Field configuration for signature placement +/// Supports both coordinate-based and template anchor-based positioning +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Field { + /// Field type + #[serde(rename = "type")] + pub field_type: SignatureFieldType, + + /// Page number (1-indexed) - required for coordinate-based + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + + /// X coordinate position + #[serde(skip_serializing_if = "Option::is_none")] + pub x: Option, + + /// Y coordinate position + #[serde(skip_serializing_if = "Option::is_none")] + pub y: Option, + + /// Field width in pixels + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + + /// Field height in pixels + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + + /// Recipient email - which recipient fills this field + pub recipient_email: String, + + /// Default value for the field (for checkbox: "true" or "false") + #[serde(skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + /// Whether this is a multiline text field + #[serde(skip_serializing_if = "Option::is_none")] + pub is_multiline: Option, + + /// Whether this field is read-only (pre-filled, non-editable) + #[serde(skip_serializing_if = "Option::is_none")] + pub is_readonly: Option, + + /// Whether this field is required + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option, + + /// Background color (hex, rgb, or named colors) + #[serde(skip_serializing_if = "Option::is_none")] + pub background_color: Option, + + /// Template anchor configuration for dynamic positioning + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, +} + +impl Field { + /// Create a coordinate-based signature field + pub fn coordinate_based( + field_type: SignatureFieldType, + page: u32, + x: f64, + y: f64, + width: f64, + height: f64, + recipient_email: impl Into, + ) -> Self { + Self { + field_type, + page: Some(page), + x: Some(x), + y: Some(y), + width: Some(width), + height: Some(height), + recipient_email: recipient_email.into(), + default_value: None, + is_multiline: None, + is_readonly: None, + required: None, + background_color: None, + template: None, + } + } + + /// Create a template anchor-based field + pub fn anchor_based( + field_type: SignatureFieldType, + anchor: impl Into, + recipient_email: impl Into, + ) -> Self { + Self { + field_type, + page: None, + x: None, + y: None, + width: None, + height: None, + recipient_email: recipient_email.into(), + default_value: None, + is_multiline: None, + is_readonly: None, + required: None, + background_color: None, + template: Some(TemplateAnchor { + anchor: Some(anchor.into()), + search_text: None, + placement: Some(Placement::Replace), + size: None, + offset: None, + case_sensitive: None, + use_regex: None, + }), + } + } +} + +/// Recipient configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Recipient { + /// Recipient's full name + pub name: String, + + /// Recipient's email address + pub email: String, + + /// Signing order (1-indexed) + pub signing_order: u32, +} + +impl Recipient { + pub fn new(name: impl Into, email: impl Into, signing_order: u32) -> Self { + Self { + name: name.into(), + email: email.into(), + signing_order, + } + } +} + +/// Request to create signature review link (prepare without sending emails) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSignatureReviewLinkRequest { + /// File path to PDF + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, + + /// Original filename (used when file is bytes) + #[serde(skip_serializing_if = "Option::is_none")] + pub file_name: Option, + + /// URL to document file + #[serde(skip_serializing_if = "Option::is_none")] + pub file_link: Option, + + /// TurboDocx deliverable ID + #[serde(skip_serializing_if = "Option::is_none")] + pub deliverable_id: Option, + + /// TurboDocx template ID + #[serde(skip_serializing_if = "Option::is_none")] + pub template_id: Option, + + /// Recipients who will sign + pub recipients: Vec, + + /// Signature fields configuration + pub fields: Vec, + + /// Document name + #[serde(skip_serializing_if = "Option::is_none")] + pub document_name: Option, + + /// Document description + #[serde(skip_serializing_if = "Option::is_none")] + pub document_description: Option, + + /// Sender name + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_name: Option, + + /// Sender email + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_email: Option, + + /// CC emails + #[serde(skip_serializing_if = "Option::is_none")] + pub cc_emails: Option>, +} + +/// Response from create signature review link +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSignatureReviewLinkResponse { + /// Whether the request was successful + pub success: bool, + + /// Document ID + pub document_id: String, + + /// Document status + pub status: String, + + /// Preview URL for reviewing the document + #[serde(skip_serializing_if = "Option::is_none")] + pub preview_url: Option, + + /// Recipients with their status + #[serde(skip_serializing_if = "Option::is_none")] + pub recipients: Option>, + + /// Response message + pub message: String, +} + +/// Recipient status in response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecipientStatus { + pub id: String, + pub name: String, + pub email: String, + pub status: String, +} + +/// Request to send signature (prepare and send in single call) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendSignatureRequest { + /// File path to PDF + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, + + /// Original filename + #[serde(skip_serializing_if = "Option::is_none")] + pub file_name: Option, + + /// URL to document file + #[serde(skip_serializing_if = "Option::is_none")] + pub file_link: Option, + + /// TurboDocx deliverable ID + #[serde(skip_serializing_if = "Option::is_none")] + pub deliverable_id: Option, + + /// TurboDocx template ID + #[serde(skip_serializing_if = "Option::is_none")] + pub template_id: Option, + + /// Recipients who will sign + pub recipients: Vec, + + /// Signature fields configuration + pub fields: Vec, + + /// Document name + #[serde(skip_serializing_if = "Option::is_none")] + pub document_name: Option, + + /// Document description + #[serde(skip_serializing_if = "Option::is_none")] + pub document_description: Option, + + /// Sender name + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_name: Option, + + /// Sender email + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_email: Option, + + /// CC emails + #[serde(skip_serializing_if = "Option::is_none")] + pub cc_emails: Option>, +} + +/// Response from send signature +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendSignatureResponse { + /// Whether the request was successful + pub success: bool, + + /// Document ID + pub document_id: String, + + /// Response message + pub message: String, +} + +/// Response from voiding a document +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VoidDocumentResponse { + /// Whether the void was successful + pub success: bool, + + /// Response message + pub message: String, +} + +/// Response from resending emails +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResendEmailResponse { + /// Whether the resend was successful + pub success: bool, + + /// Number of recipients who received email + pub recipient_count: u32, +} + +/// Audit trail user information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditTrailUser { + /// User name + pub name: String, + + /// User email + pub email: String, +} + +/// Audit trail entry +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuditTrailEntry { + /// Entry ID + pub id: String, + + /// Document ID + pub document_id: String, + + /// Action type + pub action_type: String, + + /// Timestamp of the event + pub timestamp: String, + + /// Previous hash + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_hash: Option, + + /// Current hash + #[serde(skip_serializing_if = "Option::is_none")] + pub current_hash: Option, + + /// Created on timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub created_on: Option, + + /// Additional details + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option>, + + /// User who performed the action + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + + /// User ID + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + + /// Recipient info + #[serde(skip_serializing_if = "Option::is_none")] + pub recipient: Option, + + /// Recipient ID + #[serde(skip_serializing_if = "Option::is_none")] + pub recipient_id: Option, +} + +/// Audit trail document information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuditTrailDocument { + /// Document ID + pub id: String, + + /// Document name + pub name: String, +} + +/// Response from get audit trail +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuditTrailResponse { + /// Document info + pub document: AuditTrailDocument, + + /// List of audit trail entries + pub audit_trail: Vec, +} + +/// Response from get document status +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DocumentStatusResponse { + /// Current document status + pub status: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_coordinate_based_field() { + let field = Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + ); + + assert_eq!(field.field_type, SignatureFieldType::Signature); + assert_eq!(field.page, Some(1)); + assert_eq!(field.x, Some(100.0)); + assert_eq!(field.recipient_email, "john@example.com"); + } + + #[test] + fn test_anchor_based_field() { + let field = Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "john@example.com", + ); + + assert_eq!(field.field_type, SignatureFieldType::Signature); + assert!(field.template.is_some()); + assert_eq!( + field.template.as_ref().unwrap().anchor, + Some("{SignHere}".to_string()) + ); + } + + #[test] + fn test_recipient() { + let recipient = Recipient::new("John Doe", "john@example.com", 1); + assert_eq!(recipient.name, "John Doe"); + assert_eq!(recipient.email, "john@example.com"); + assert_eq!(recipient.signing_order, 1); + } +} diff --git a/packages/rust-sdk/src/types/template.rs b/packages/rust-sdk/src/types/template.rs new file mode 100644 index 0000000..080543a --- /dev/null +++ b/packages/rust-sdk/src/types/template.rs @@ -0,0 +1,355 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// MIME type for template variables +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "lowercase")] +pub enum VariableMimeType { + #[default] + Text, + Html, + Json, + Image, + Markdown, +} + +/// Represents a template variable with its configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TemplateVariable { + /// Placeholder in the template (e.g., "{customer_name}") + pub placeholder: String, + + /// Variable name (e.g., "customer_name") + pub name: String, + + /// MIME type of the variable + pub mime_type: VariableMimeType, + + /// Variable value (can be string, number, boolean, object, or array) + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + + /// Legacy alternative to value + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + + /// Whether this variable uses advanced templating engine + #[serde(skip_serializing_if = "Option::is_none")] + pub uses_advanced_templating_engine: Option, + + /// Whether this variable is nested in advanced templating engine + #[serde(skip_serializing_if = "Option::is_none")] + pub nested_in_advanced_templating_engine: Option, + + /// Allow rich text injection (HTML) + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_rich_text_injection: Option, + + /// Variable description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Default value flag + #[serde(skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + /// Sub-variables for nested structures + #[serde(skip_serializing_if = "Option::is_none")] + pub subvariables: Option>, +} + +impl TemplateVariable { + /// Create a simple text variable + pub fn simple, V: Into>( + placeholder: S, + name: S, + value: V, + ) -> Self { + Self { + placeholder: placeholder.into(), + name: name.into(), + mime_type: VariableMimeType::Text, + value: Some(value.into()), + text: None, + uses_advanced_templating_engine: None, + nested_in_advanced_templating_engine: None, + allow_rich_text_injection: None, + description: None, + default_value: None, + subvariables: None, + } + } + + /// Create a simple variable with HTML content + pub fn html>(placeholder: S, name: S, html: S) -> Self { + Self { + placeholder: placeholder.into(), + name: name.into(), + mime_type: VariableMimeType::Html, + value: Some(html.into().into()), + text: None, + uses_advanced_templating_engine: None, + nested_in_advanced_templating_engine: None, + allow_rich_text_injection: Some(true), + description: None, + default_value: None, + subvariables: None, + } + } + + /// Create a variable for advanced templating (nested objects with dot notation) + pub fn advanced_engine, V: Serialize>( + placeholder: S, + name: S, + value: V, + ) -> Result { + Ok(Self { + placeholder: placeholder.into(), + name: name.into(), + mime_type: VariableMimeType::Json, + value: Some(serde_json::to_value(value)?), + text: None, + uses_advanced_templating_engine: Some(true), + nested_in_advanced_templating_engine: None, + allow_rich_text_injection: None, + description: None, + default_value: None, + subvariables: None, + }) + } + + /// Create a loop variable (array iteration) + pub fn loop_var, V: Serialize>( + placeholder: S, + name: S, + items: V, + ) -> Result { + Ok(Self { + placeholder: placeholder.into(), + name: name.into(), + mime_type: VariableMimeType::Json, + value: Some(serde_json::to_value(items)?), + text: None, + uses_advanced_templating_engine: None, + nested_in_advanced_templating_engine: None, + allow_rich_text_injection: None, + description: None, + default_value: None, + subvariables: None, + }) + } + + /// Create a conditional variable + pub fn conditional, V: Into>( + placeholder: S, + name: S, + condition: V, + ) -> Self { + Self { + placeholder: placeholder.into(), + name: name.into(), + mime_type: VariableMimeType::Json, + value: Some(condition.into()), + text: None, + uses_advanced_templating_engine: Some(true), + nested_in_advanced_templating_engine: None, + allow_rich_text_injection: None, + description: None, + default_value: None, + subvariables: None, + } + } + + /// Create an image variable + pub fn image>(placeholder: S, name: S, image_url: S) -> Self { + Self { + placeholder: placeholder.into(), + name: name.into(), + mime_type: VariableMimeType::Image, + value: Some(image_url.into().into()), + text: None, + uses_advanced_templating_engine: None, + nested_in_advanced_templating_engine: None, + allow_rich_text_injection: None, + description: None, + default_value: None, + subvariables: None, + } + } +} + +/// Output format for generated documents +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "lowercase")] +pub enum OutputFormat { + #[default] + Docx, + Pdf, +} + +/// Request to generate a template +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateTemplateRequest { + /// Template ID (UUID) + pub template_id: String, + + /// Template variables + pub variables: Vec, + + /// Document name + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Document description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Replace fonts in the document + #[serde(skip_serializing_if = "Option::is_none")] + pub replace_fonts: Option, + + /// Default font to use + #[serde(skip_serializing_if = "Option::is_none")] + pub default_font: Option, + + /// Output format + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, + + /// Additional metadata + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, +} + +impl GenerateTemplateRequest { + /// Create a new template generation request + pub fn new>(template_id: S, variables: Vec) -> Self { + Self { + template_id: template_id.into(), + variables, + name: None, + description: None, + replace_fonts: None, + default_font: None, + output_format: None, + metadata: None, + } + } + + /// Set document name + pub fn with_name>(mut self, name: S) -> Self { + self.name = Some(name.into()); + self + } + + /// Set document description + pub fn with_description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set font replacement options + pub fn with_font_replacement>( + mut self, + replace: bool, + default_font: Option, + ) -> Self { + self.replace_fonts = Some(replace); + self.default_font = default_font.map(|f| f.into()); + self + } + + /// Set output format + pub fn with_output_format(mut self, format: OutputFormat) -> Self { + self.output_format = Some(format); + self + } + + /// Set metadata + pub fn with_metadata(mut self, metadata: HashMap) -> Self { + self.metadata = Some(metadata); + self + } +} + +/// Response from template generation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateTemplateResponse { + /// Success status + pub success: bool, + + /// Generated deliverable ID + #[serde(skip_serializing_if = "Option::is_none")] + pub deliverable_id: Option, + + /// Download URL (if available) + #[serde(skip_serializing_if = "Option::is_none")] + pub download_url: Option, + + /// Response message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Error message (if any) + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_simple_variable() { + let var = TemplateVariable::simple("{name}", "name", "John Doe"); + assert_eq!(var.placeholder, "{name}"); + assert_eq!(var.name, "name"); + assert_eq!(var.mime_type, VariableMimeType::Text); + assert_eq!(var.value, Some(json!("John Doe"))); + } + + #[test] + fn test_loop_variable() { + let items = vec![ + json!({"name": "Item 1", "price": 100}), + json!({"name": "Item 2", "price": 200}), + ]; + let var = TemplateVariable::loop_var("{items}", "items", items).unwrap(); + assert_eq!(var.placeholder, "{items}"); + assert_eq!(var.mime_type, VariableMimeType::Json); + } + + #[test] + fn test_conditional_variable() { + let var = TemplateVariable::conditional("{is_active}", "is_active", true); + assert_eq!(var.placeholder, "{is_active}"); + assert_eq!(var.mime_type, VariableMimeType::Json); + assert_eq!(var.value, Some(json!(true))); + } + + #[test] + fn test_image_variable() { + let var = TemplateVariable::image("{logo}", "logo", "https://example.com/logo.png"); + assert_eq!(var.mime_type, VariableMimeType::Image); + assert_eq!(var.value, Some(json!("https://example.com/logo.png"))); + } + + #[test] + fn test_request_builder() { + let request = GenerateTemplateRequest::new( + "template-123", + vec![TemplateVariable::simple("{name}", "name", "Test")], + ) + .with_name("Test Document") + .with_description("A test document") + .with_output_format(OutputFormat::Pdf); + + assert_eq!(request.template_id, "template-123"); + assert_eq!(request.name, Some("Test Document".to_string())); + assert_eq!(request.output_format, Some(OutputFormat::Pdf)); + } +} diff --git a/packages/rust-sdk/src/utils/errors.rs b/packages/rust-sdk/src/utils/errors.rs new file mode 100644 index 0000000..2b77f7b --- /dev/null +++ b/packages/rust-sdk/src/utils/errors.rs @@ -0,0 +1,36 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TurboDocxError { + #[error("Authentication error: {0}")] + Authentication(String), + + #[error("Validation error: {0}")] + Validation(String), + + #[error("Not found: {0}")] + NotFound(String), + + #[error("Rate limit exceeded: {0}")] + RateLimit(String), + + #[error("Network error: {0}")] + Network(String), + + #[error("API error (status {status}): {message}")] + Api { status: u16, message: String }, + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("HTTP request error: {0}")] + Request(#[from] reqwest::Error), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("{0}")] + Other(String), +} + +pub type Result = std::result::Result; diff --git a/packages/rust-sdk/src/utils/mod.rs b/packages/rust-sdk/src/utils/mod.rs new file mode 100644 index 0000000..07a74bb --- /dev/null +++ b/packages/rust-sdk/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod errors; + +pub use errors::{Result, TurboDocxError}; diff --git a/packages/rust-sdk/tests/turbosign_test.rs b/packages/rust-sdk/tests/turbosign_test.rs new file mode 100644 index 0000000..da95c8b --- /dev/null +++ b/packages/rust-sdk/tests/turbosign_test.rs @@ -0,0 +1,554 @@ +use turbodocx_sdk::{ + CreateSignatureReviewLinkRequest, Field, Placement, Recipient, SendSignatureRequest, + SignatureFieldType, TemplateAnchor, +}; + +// =========================================== +// Field Tests +// =========================================== + +#[test] +fn test_coordinate_based_signature_field() { + let field = Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + ); + + assert_eq!(field.field_type, SignatureFieldType::Signature); + assert_eq!(field.page, Some(1)); + assert_eq!(field.x, Some(100.0)); + assert_eq!(field.y, Some(500.0)); + assert_eq!(field.width, Some(200.0)); + assert_eq!(field.height, Some(50.0)); + assert_eq!(field.recipient_email, "john@example.com"); + assert!(field.template.is_none()); +} + +#[test] +fn test_anchor_based_signature_field() { + let field = Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "john@example.com", + ); + + assert_eq!(field.field_type, SignatureFieldType::Signature); + assert_eq!(field.recipient_email, "john@example.com"); + assert!(field.template.is_some()); + + let template = field.template.unwrap(); + assert_eq!(template.anchor, Some("{SignHere}".to_string())); + assert_eq!(template.placement, Some(Placement::Replace)); +} + +#[test] +fn test_field_types() { + let types = vec![ + SignatureFieldType::Signature, + SignatureFieldType::Initial, + SignatureFieldType::Date, + SignatureFieldType::Text, + SignatureFieldType::FullName, + SignatureFieldType::Title, + SignatureFieldType::Company, + SignatureFieldType::FirstName, + SignatureFieldType::LastName, + SignatureFieldType::Email, + SignatureFieldType::Checkbox, + ]; + + for field_type in types { + let field = Field::coordinate_based( + field_type.clone(), + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "test@example.com", + ); + assert_eq!(field.field_type, field_type); + } +} + +#[test] +fn test_checkbox_field_with_default_value() { + let mut field = Field::coordinate_based( + SignatureFieldType::Checkbox, + 1, + 100.0, + 600.0, + 20.0, + 20.0, + "john@example.com", + ); + + field.default_value = Some("true".to_string()); + + assert_eq!(field.field_type, SignatureFieldType::Checkbox); + assert_eq!(field.default_value, Some("true".to_string())); +} + +#[test] +fn test_field_with_optional_properties() { + let mut field = Field::coordinate_based( + SignatureFieldType::Text, + 1, + 100.0, + 500.0, + 300.0, + 100.0, + "john@example.com", + ); + + field.is_multiline = Some(true); + field.is_readonly = Some(false); + field.required = Some(true); + field.background_color = Some("#FFFF00".to_string()); + + assert_eq!(field.is_multiline, Some(true)); + assert_eq!(field.is_readonly, Some(false)); + assert_eq!(field.required, Some(true)); + assert_eq!(field.background_color, Some("#FFFF00".to_string())); +} + +#[test] +fn test_template_anchor_with_search_text() { + let mut field = Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "john@example.com", + ); + + // Modify template to use search text instead + field.template = Some(TemplateAnchor { + anchor: None, + search_text: Some("Sign here:".to_string()), + placement: Some(Placement::After), + size: Some(turbodocx_sdk::FieldSize { + width: 200.0, + height: 50.0, + }), + offset: Some(turbodocx_sdk::FieldOffset { x: 10.0, y: 0.0 }), + case_sensitive: Some(false), + use_regex: Some(false), + }); + + let template = field.template.unwrap(); + assert_eq!(template.search_text, Some("Sign here:".to_string())); + assert_eq!(template.placement, Some(Placement::After)); + assert!(template.size.is_some()); + assert!(template.offset.is_some()); +} + +#[test] +fn test_placement_variants() { + let placements = vec![ + Placement::Replace, + Placement::Before, + Placement::After, + Placement::Above, + Placement::Below, + ]; + + for placement in placements { + let template = TemplateAnchor { + anchor: Some("{Tag}".to_string()), + search_text: None, + placement: Some(placement.clone()), + size: None, + offset: None, + case_sensitive: None, + use_regex: None, + }; + + assert_eq!(template.placement, Some(placement)); + } +} + +// =========================================== +// Recipient Tests +// =========================================== + +#[test] +fn test_recipient_creation() { + let recipient = Recipient::new("John Doe", "john@example.com", 1); + + assert_eq!(recipient.name, "John Doe"); + assert_eq!(recipient.email, "john@example.com"); + assert_eq!(recipient.signing_order, 1); +} + +#[test] +fn test_multiple_recipients_with_signing_order() { + let recipients = vec![ + Recipient::new("John Doe", "john@example.com", 1), + Recipient::new("Jane Smith", "jane@example.com", 2), + Recipient::new("Bob Johnson", "bob@example.com", 3), + ]; + + assert_eq!(recipients.len(), 3); + assert_eq!(recipients[0].signing_order, 1); + assert_eq!(recipients[1].signing_order, 2); + assert_eq!(recipients[2].signing_order, 3); +} + +// =========================================== +// CreateSignatureReviewLinkRequest Tests +// =========================================== + +#[test] +fn test_review_link_request_with_file_link() { + let request = CreateSignatureReviewLinkRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![Recipient::new("John Doe", "john@example.com", 1)], + fields: vec![Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + )], + document_name: Some("Service Agreement".to_string()), + document_description: Some("Annual service contract".to_string()), + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + assert_eq!( + request.file_link, + Some("https://example.com/contract.pdf".to_string()) + ); + assert_eq!(request.recipients.len(), 1); + assert_eq!(request.fields.len(), 1); + assert_eq!(request.document_name, Some("Service Agreement".to_string())); +} + +#[test] +fn test_review_link_request_with_deliverable_id() { + let request = CreateSignatureReviewLinkRequest { + file_link: None, + file: None, + file_name: None, + deliverable_id: Some("deliverable-uuid".to_string()), + template_id: None, + recipients: vec![Recipient::new("John Doe", "john@example.com", 1)], + fields: vec![Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "john@example.com", + )], + document_name: Some("Generated Document".to_string()), + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + assert_eq!( + request.deliverable_id, + Some("deliverable-uuid".to_string()) + ); + assert!(request.file_link.is_none()); + assert!(request.file.is_none()); +} + +#[test] +fn test_review_link_request_with_template_id() { + let request = CreateSignatureReviewLinkRequest { + file_link: None, + file: None, + file_name: None, + deliverable_id: None, + template_id: Some("template-uuid".to_string()), + recipients: vec![Recipient::new("John Doe", "john@example.com", 1)], + fields: vec![Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + )], + document_name: None, + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + assert_eq!(request.template_id, Some("template-uuid".to_string())); +} + +#[test] +fn test_review_link_request_with_sender_info() { + let request = CreateSignatureReviewLinkRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![Recipient::new("John Doe", "john@example.com", 1)], + fields: vec![Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + )], + document_name: Some("Contract".to_string()), + document_description: None, + sender_name: Some("Support Team".to_string()), + sender_email: Some("support@company.com".to_string()), + cc_emails: Some(vec!["manager@company.com".to_string()]), + }; + + assert_eq!(request.sender_name, Some("Support Team".to_string())); + assert_eq!( + request.sender_email, + Some("support@company.com".to_string()) + ); + assert_eq!( + request.cc_emails, + Some(vec!["manager@company.com".to_string()]) + ); +} + +#[test] +fn test_review_link_request_with_multiple_recipients_and_fields() { + let request = CreateSignatureReviewLinkRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![ + Recipient::new("John Doe", "john@example.com", 1), + Recipient::new("Jane Smith", "jane@example.com", 2), + ], + fields: vec![ + Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + ), + Field::coordinate_based( + SignatureFieldType::Date, + 1, + 320.0, + 500.0, + 100.0, + 30.0, + "john@example.com", + ), + Field::coordinate_based( + SignatureFieldType::Signature, + 2, + 100.0, + 500.0, + 200.0, + 50.0, + "jane@example.com", + ), + ], + document_name: Some("Multi-Party Agreement".to_string()), + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + assert_eq!(request.recipients.len(), 2); + assert_eq!(request.fields.len(), 3); +} + +// =========================================== +// SendSignatureRequest Tests +// =========================================== + +#[test] +fn test_send_signature_request_with_file_link() { + let request = SendSignatureRequest { + file_link: Some("https://example.com/contract.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![Recipient::new("Alice Johnson", "alice@example.com", 1)], + fields: vec![Field::anchor_based( + SignatureFieldType::Signature, + "{ClientSignature}", + "alice@example.com", + )], + document_name: Some("Engagement Letter".to_string()), + document_description: None, + sender_name: Some("Support Team".to_string()), + sender_email: Some("support@company.com".to_string()), + cc_emails: Some(vec!["manager@company.com".to_string()]), + }; + + assert_eq!( + request.file_link, + Some("https://example.com/contract.pdf".to_string()) + ); + assert_eq!(request.recipients.len(), 1); + assert_eq!(request.fields.len(), 1); +} + +#[test] +fn test_send_signature_request_with_deliverable_id() { + let request = SendSignatureRequest { + file_link: None, + file: None, + file_name: None, + deliverable_id: Some("deliverable-uuid".to_string()), + template_id: None, + recipients: vec![Recipient::new("Bob Smith", "bob@example.com", 1)], + fields: vec![ + Field::anchor_based( + SignatureFieldType::Signature, + "{SignHere}", + "bob@example.com", + ), + Field::anchor_based(SignatureFieldType::Date, "{Date}", "bob@example.com"), + ], + document_name: Some("Contract".to_string()), + document_description: Some("Employment contract".to_string()), + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + assert_eq!( + request.deliverable_id, + Some("deliverable-uuid".to_string()) + ); + assert_eq!(request.fields.len(), 2); +} + +// =========================================== +// Serialization Tests +// =========================================== + +#[test] +fn test_recipient_serialization() { + let recipient = Recipient::new("John Doe", "john@example.com", 1); + let json = serde_json::to_string(&recipient).unwrap(); + + assert!(json.contains("John Doe")); + assert!(json.contains("john@example.com")); + assert!(json.contains("\"signingOrder\":1")); +} + +#[test] +fn test_field_serialization() { + let field = Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "john@example.com", + ); + + let json = serde_json::to_string(&field).unwrap(); + + assert!(json.contains("\"type\":\"signature\"")); + assert!(json.contains("\"page\":1")); + assert!(json.contains("\"recipientEmail\":\"john@example.com\"")); +} + +#[test] +fn test_signature_field_type_serialization() { + let types = vec![ + (SignatureFieldType::Signature, "signature"), + (SignatureFieldType::Initial, "initial"), + (SignatureFieldType::Date, "date"), + (SignatureFieldType::Text, "text"), + (SignatureFieldType::FullName, "full_name"), + (SignatureFieldType::Title, "title"), + (SignatureFieldType::Company, "company"), + (SignatureFieldType::FirstName, "first_name"), + (SignatureFieldType::LastName, "last_name"), + (SignatureFieldType::Email, "email"), + (SignatureFieldType::Checkbox, "checkbox"), + ]; + + for (field_type, expected_json) in types { + let json = serde_json::to_string(&field_type).unwrap(); + assert_eq!(json, format!("\"{}\"", expected_json)); + } +} + +#[test] +fn test_placement_serialization() { + let placements = vec![ + (Placement::Replace, "replace"), + (Placement::Before, "before"), + (Placement::After, "after"), + (Placement::Above, "above"), + (Placement::Below, "below"), + ]; + + for (placement, expected_json) in placements { + let json = serde_json::to_string(&placement).unwrap(); + assert_eq!(json, format!("\"{}\"", expected_json)); + } +} + +#[test] +fn test_request_serialization_omits_none_fields() { + let request = CreateSignatureReviewLinkRequest { + file_link: Some("https://example.com/test.pdf".to_string()), + file: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![Recipient::new("Test User", "test@example.com", 1)], + fields: vec![Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 500.0, + 200.0, + 50.0, + "test@example.com", + )], + document_name: None, + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + let json = serde_json::to_string(&request).unwrap(); + + // Should not include null/None fields + assert!(!json.contains("\"file\":")); + assert!(!json.contains("\"deliverableId\":")); + assert!(!json.contains("\"documentName\":")); + + // Should include present fields + assert!(json.contains("\"fileLink\":")); + assert!(json.contains("\"recipients\":")); +} diff --git a/packages/rust-sdk/tests/turbotemplate_test.rs b/packages/rust-sdk/tests/turbotemplate_test.rs new file mode 100644 index 0000000..9a4a424 --- /dev/null +++ b/packages/rust-sdk/tests/turbotemplate_test.rs @@ -0,0 +1,140 @@ +use serde_json::json; +use turbodocx_sdk::{GenerateTemplateRequest, OutputFormat, TemplateVariable, VariableMimeType}; + +#[test] +fn test_simple_variable() { + let var = TemplateVariable::simple("{name}", "name", "John Doe"); + assert_eq!(var.placeholder, "{name}"); + assert_eq!(var.name, "name"); + assert_eq!(var.mime_type, VariableMimeType::Text); + assert_eq!(var.value, Some(json!("John Doe"))); +} + +#[test] +fn test_html_variable() { + let var = TemplateVariable::html("{content}", "content", "

    Hello

    "); + assert_eq!(var.mime_type, VariableMimeType::Html); + assert_eq!(var.allow_rich_text_injection, Some(true)); +} + +#[test] +fn test_advanced_engine_variable() { + let data = json!({ + "firstName": "John", + "lastName": "Doe" + }); + let var = TemplateVariable::advanced_engine("{user}", "user", data.clone()).unwrap(); + assert_eq!(var.mime_type, VariableMimeType::Json); + assert_eq!(var.uses_advanced_templating_engine, Some(true)); + assert_eq!(var.value, Some(data)); +} + +#[test] +fn test_loop_variable() { + let items = vec![json!({"name": "Item 1"}), json!({"name": "Item 2"})]; + let var = TemplateVariable::loop_var("{items}", "items", items.clone()).unwrap(); + assert_eq!(var.mime_type, VariableMimeType::Json); + assert_eq!(var.value, Some(json!(items))); +} + +#[test] +fn test_conditional_variable() { + let var = TemplateVariable::conditional("{is_active}", "is_active", true); + assert_eq!(var.mime_type, VariableMimeType::Json); + assert_eq!(var.value, Some(json!(true))); + assert_eq!(var.uses_advanced_templating_engine, Some(true)); +} + +#[test] +fn test_image_variable() { + let var = TemplateVariable::image("{logo}", "logo", "https://example.com/logo.png"); + assert_eq!(var.mime_type, VariableMimeType::Image); + assert_eq!(var.value, Some(json!("https://example.com/logo.png"))); +} + +#[test] +fn test_request_builder() { + let request = GenerateTemplateRequest::new( + "template-123", + vec![TemplateVariable::simple("{name}", "name", "Test")], + ) + .with_name("Test Document") + .with_description("A test document") + .with_output_format(OutputFormat::Pdf); + + assert_eq!(request.template_id, "template-123"); + assert_eq!(request.name, Some("Test Document".to_string())); + assert_eq!(request.description, Some("A test document".to_string())); + assert_eq!(request.output_format, Some(OutputFormat::Pdf)); + assert_eq!(request.variables.len(), 1); +} + +#[test] +fn test_request_serialization() { + let request = GenerateTemplateRequest::new( + "template-123", + vec![ + TemplateVariable::simple("{customer_name}", "customer_name", "John Doe"), + TemplateVariable::simple("{order_total}", "order_total", 1500), + ], + ) + .with_name("Invoice"); + + let json = serde_json::to_string(&request).unwrap(); + assert!(json.contains("template-123")); + assert!(json.contains("Invoice")); + assert!(json.contains("customer_name")); + assert!(json.contains("John Doe")); +} + +#[test] +fn test_nested_object_variable() { + let user = json!({ + "firstName": "John", + "lastName": "Doe", + "address": { + "street": "123 Main St", + "city": "San Francisco" + } + }); + + let var = TemplateVariable::advanced_engine("{user}", "user", user.clone()).unwrap(); + assert_eq!(var.value, Some(user)); +} + +#[test] +fn test_variable_with_numbers() { + let var1 = TemplateVariable::simple("{quantity}", "quantity", 42); + assert_eq!(var1.value, Some(json!(42))); + + let var2 = TemplateVariable::simple("{price}", "price", 99.99); + assert_eq!(var2.value, Some(json!(99.99))); +} + +#[test] +fn test_variable_with_boolean() { + let var = TemplateVariable::simple("{is_active}", "is_active", true); + assert_eq!(var.value, Some(json!(true))); +} + +#[test] +fn test_output_format_serialization() { + let format = OutputFormat::Docx; + let json = serde_json::to_string(&format).unwrap(); + assert_eq!(json, r#""docx""#); + + let format = OutputFormat::Pdf; + let json = serde_json::to_string(&format).unwrap(); + assert_eq!(json, r#""pdf""#); +} + +#[test] +fn test_mime_type_serialization() { + let mime = VariableMimeType::Text; + let json = serde_json::to_string(&mime).unwrap(); + assert_eq!(json, r#""text""#); + + let mime = VariableMimeType::Json; + let json = serde_json::to_string(&mime).unwrap(); + assert_eq!(json, r#""json""#); +} From afba7a533ade491f9b5c3d6521a3accec567ffd1 Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 10:49:48 +0000 Subject: [PATCH 32/39] chore: add ci pipeline for rust Signed-off-by: Kushal --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++ .github/workflows/publish-rust.yml | 50 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 .github/workflows/publish-rust.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c541c40..cde5acb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,3 +118,47 @@ jobs: - name: Run tests run: mvn test -B + + test-rust: + name: Test Rust SDK + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/rust-sdk + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache target directory + uses: actions/cache@v4 + with: + path: packages/rust-sdk/target + key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Check formatting + run: cargo fmt -- --check + + - name: Run clippy + run: cargo clippy -- -D warnings + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose diff --git a/.github/workflows/publish-rust.yml b/.github/workflows/publish-rust.yml new file mode 100644 index 0000000..06ade2d --- /dev/null +++ b/.github/workflows/publish-rust.yml @@ -0,0 +1,50 @@ +name: Publish Rust SDK + +on: + push: + tags: + - 'rust-v*' + +jobs: + publish: + name: Publish to crates.io + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/rust-sdk + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Check formatting + run: cargo fmt -- --check + + - name: Run clippy + run: cargo clippy -- -D warnings + + - name: Build + run: cargo build --release --verbose + + - name: Run tests + run: cargo test --verbose + + - name: Publish to crates.io + run: cargo publish --token ${{ secrets.CARGO_TOKEN }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} From 1248db4b5f8df5dc4652b8b246b91724b03792de Mon Sep 17 00:00:00 2001 From: Kushal Date: Sun, 25 Jan 2026 10:51:51 +0000 Subject: [PATCH 33/39] chore: fix formatting issues during ci Signed-off-by: Kushal --- packages/rust-sdk/examples/sign.rs | 19 +++++++------------ packages/rust-sdk/src/lib.rs | 10 +++++----- packages/rust-sdk/tests/turbosign_test.rs | 10 ++-------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/rust-sdk/examples/sign.rs b/packages/rust-sdk/examples/sign.rs index ec86586..66a85df 100644 --- a/packages/rust-sdk/examples/sign.rs +++ b/packages/rust-sdk/examples/sign.rs @@ -21,11 +21,11 @@ async fn main() -> Result<(), Box> { fields: vec![ Field::coordinate_based( SignatureFieldType::Signature, - 1, // page - 100.0, // x - 500.0, // y - 200.0, // width - 50.0, // height + 1, // page + 100.0, // x + 500.0, // y + 200.0, // width + 50.0, // height "john@example.com", ), Field::coordinate_based( @@ -77,11 +77,7 @@ async fn main() -> Result<(), Box> { "{ClientSignature}", "alice@example.com", ), - Field::anchor_based( - SignatureFieldType::Date, - "{SignDate}", - "alice@example.com", - ), + Field::anchor_based(SignatureFieldType::Date, "{SignDate}", "alice@example.com"), Field::anchor_based( SignatureFieldType::FullName, "{ClientName}", @@ -119,8 +115,7 @@ async fn main() -> Result<(), Box> { println!("✓ Sent to {} recipients", response.recipient_count); println!("\n=== Example 6: Void Document ==="); - let response = - TurboSign::void_document(document_id, Some("Contract terms changed")).await?; + let response = TurboSign::void_document(document_id, Some("Contract terms changed")).await?; println!("✓ {}", response.message); println!("\n=== Example 7: Download Signed Document ==="); diff --git a/packages/rust-sdk/src/lib.rs b/packages/rust-sdk/src/lib.rs index 52d1408..ad77465 100644 --- a/packages/rust-sdk/src/lib.rs +++ b/packages/rust-sdk/src/lib.rs @@ -68,6 +68,10 @@ pub use types::{ Field, FieldOffset, FieldSize, + // Template types + GenerateTemplateRequest, + GenerateTemplateResponse, + OutputFormat, Placement, Recipient, RecipientStatus, @@ -76,12 +80,8 @@ pub use types::{ SendSignatureResponse, SignatureFieldType, TemplateAnchor, - VoidDocumentResponse, - // Template types - GenerateTemplateRequest, - GenerateTemplateResponse, - OutputFormat, TemplateVariable, VariableMimeType, + VoidDocumentResponse, }; pub use utils::{Result, TurboDocxError}; diff --git a/packages/rust-sdk/tests/turbosign_test.rs b/packages/rust-sdk/tests/turbosign_test.rs index da95c8b..2566430 100644 --- a/packages/rust-sdk/tests/turbosign_test.rs +++ b/packages/rust-sdk/tests/turbosign_test.rs @@ -257,10 +257,7 @@ fn test_review_link_request_with_deliverable_id() { cc_emails: None, }; - assert_eq!( - request.deliverable_id, - Some("deliverable-uuid".to_string()) - ); + assert_eq!(request.deliverable_id, Some("deliverable-uuid".to_string())); assert!(request.file_link.is_none()); assert!(request.file.is_none()); } @@ -438,10 +435,7 @@ fn test_send_signature_request_with_deliverable_id() { cc_emails: None, }; - assert_eq!( - request.deliverable_id, - Some("deliverable-uuid".to_string()) - ); + assert_eq!(request.deliverable_id, Some("deliverable-uuid".to_string())); assert_eq!(request.fields.len(), 2); } From 6c83f4b63029ef9f20a045b6e869b4ce8a8e0df6 Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 29 Jan 2026 18:18:36 +0000 Subject: [PATCH 34/39] tests: add manual test suite for go Signed-off-by: Kushal --- packages/go-sdk/cmd/manual/main.go | 193 ++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) diff --git a/packages/go-sdk/cmd/manual/main.go b/packages/go-sdk/cmd/manual/main.go index 078a044..61be1e2 100644 --- a/packages/go-sdk/cmd/manual/main.go +++ b/packages/go-sdk/cmd/manual/main.go @@ -2,7 +2,9 @@ // +build manual /* -TurboSign Go SDK - Manual Test Suite +TurboDocx Go SDK - Manual Test Suite + +Tests for both TurboSign (digital signatures) and TurboTemplate (document generation) Run: go run -tags manual manual_runner.go @@ -29,6 +31,7 @@ const ( testPDFPath = "/path/to/your/test-document.pdf" // Replace with path to your test PDF/DOCX testEmail = "test-recipient@example.com" // Replace with a real email to receive notifications fileURL = "https://example.com/sample-document.pdf" // Replace with publicly accessible PDF URL + templateID = "your-template-uuid-here" // Replace with your template UUID ) var client *turbodocx.Client @@ -218,13 +221,166 @@ func testGetAuditTrail(ctx context.Context, documentID string) error { return nil } +// ============================================= +// TURBOTEMPLATE TEST FUNCTIONS +// ============================================= + +// Test 8: Simple Variable Substitution +// +// Template usage: "Dear {customer_name}, your order total is ${order_total}." +func testSimpleVariables(ctx context.Context) error { + fmt.Println("\n--- Test 8: Simple Variable Substitution ---") + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: templateID, + Variables: []turbodocx.TemplateVariable{ + {Placeholder: "{customer_name}", Name: "customer_name", Value: "John Doe", MimeType: "text"}, + {Placeholder: "{order_total}", Name: "order_total", Value: 1500, MimeType: "text"}, + {Placeholder: "{order_date}", Name: "order_date", Value: "2024-01-01", MimeType: "text"}, + }, + Name: stringPtr("Simple Substitution Document"), + Description: stringPtr("Basic variable substitution example"), + OutputFormat: stringPtr("pdf"), + }) + if err != nil { + return err + } + + prettyPrint(result) + return nil +} + +// Test 9: Nested Objects with Dot Notation +// +// Template usage: "Name: {user.name}, Company: {user.profile.company}" +func testNestedObjects(ctx context.Context) error { + fmt.Println("\n--- Test 9: Nested Objects with Dot Notation ---") + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: templateID, + Variables: []turbodocx.TemplateVariable{ + { + Placeholder: "{user}", + Name: "user", + Value: map[string]interface{}{ + "name": "John Doe", + "email": "john@example.com", + "profile": map[string]interface{}{ + "company": "Acme Corp", + "title": "Software Engineer", + "location": "San Francisco, CA", + }, + }, + MimeType: "json", + UsesAdvancedTemplatingEngine: boolPtr(true), + }, + }, + Name: stringPtr("Nested Objects Document"), + Description: stringPtr("Nested object with dot notation example"), + OutputFormat: stringPtr("pdf"), + }) + if err != nil { + return err + } + + prettyPrint(result) + return nil +} + +// Test 10: Array Loops +// +// Template usage: +// {#items} +// - {name}: {quantity} x ${price} +// {/items} +func testArrayLoops(ctx context.Context) error { + fmt.Println("\n--- Test 10: Array Loops ---") + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: templateID, + Variables: []turbodocx.TemplateVariable{ + { + Placeholder: "{items}", + Name: "items", + Value: []map[string]interface{}{ + {"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}, + {"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}, + {"name": "Item C", "quantity": 10, "price": 50, "sku": "SKU-003"}, + }, + MimeType: "json", + UsesAdvancedTemplatingEngine: boolPtr(true), + }, + }, + Name: stringPtr("Array Loops Document"), + Description: stringPtr("Array loop iteration example"), + OutputFormat: stringPtr("pdf"), + }) + if err != nil { + return err + } + + prettyPrint(result) + return nil +} + +// Test 11: Conditionals +// +// Template usage: +// {#if is_premium} +// Premium Member Discount: {discount * 100}% +// {/if} +func testConditionals(ctx context.Context) error { + fmt.Println("\n--- Test 11: Conditionals ---") + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: templateID, + Variables: []turbodocx.TemplateVariable{ + {Placeholder: "{is_premium}", Name: "is_premium", Value: true, MimeType: "json", UsesAdvancedTemplatingEngine: boolPtr(true)}, + {Placeholder: "{discount}", Name: "discount", Value: 0.2, MimeType: "json", UsesAdvancedTemplatingEngine: boolPtr(true)}, + }, + Name: stringPtr("Conditionals Document"), + Description: stringPtr("Boolean conditional example"), + OutputFormat: stringPtr("pdf"), + }) + if err != nil { + return err + } + + prettyPrint(result) + return nil +} + +// Test 12: Images +// +// Template usage: Insert {logo} at the top of the document +func testImages(ctx context.Context) error { + fmt.Println("\n--- Test 12: Images ---") + + result, err := client.TurboTemplate.Generate(ctx, &turbodocx.GenerateTemplateRequest{ + TemplateID: templateID, + Variables: []turbodocx.TemplateVariable{ + {Placeholder: "{title}", Name: "title", Value: "Quarterly Report", MimeType: "text"}, + {Placeholder: "{logo}", Name: "logo", Value: "https://example.com/logo.png", MimeType: "image"}, + }, + Name: stringPtr("Document with Images"), + Description: stringPtr("Using image variables"), + OutputFormat: stringPtr("pdf"), + }) + if err != nil { + return err + } + + prettyPrint(result) + return nil +} + // ============================================= // MAIN TEST RUNNER // ============================================= func main() { fmt.Println("==============================================") - fmt.Println("TurboSign Go SDK - Manual Test Suite") + fmt.Println("TurboDocx Go SDK - Manual Test Suite") fmt.Println("==============================================") // Check if test PDF exists @@ -245,6 +401,8 @@ func main() { // Uncomment and run tests as needed: _ = pdfBytes // Suppress unused variable warning + // ===== TurboSign Tests ===== + // Test 1: Prepare for Review (uses fileLink, doesn't need pdfBytes) // _, err = testCreateSignatureReviewLink(ctx) // if err != nil { handleError(err); return } @@ -273,6 +431,28 @@ func main() { // err = testGetAuditTrail(ctx, "document-uuid-here") // if err != nil { handleError(err); return } + // ===== TurboTemplate Tests ===== + + // Test 8: Simple Variable Substitution + // err = testSimpleVariables(ctx) + // if err != nil { handleError(err); return } + + // Test 9: Nested Objects with Dot Notation + // err = testNestedObjects(ctx) + // if err != nil { handleError(err); return } + + // Test 10: Array Loops + // err = testArrayLoops(ctx) + // if err != nil { handleError(err); return } + + // Test 11: Conditionals + // err = testConditionals(ctx) + // if err != nil { handleError(err); return } + + // Test 12: Images + // err = testImages(ctx) + // if err != nil { handleError(err); return } + _ = ctx // Suppress unused variable warning fmt.Println("\n==============================================") @@ -280,6 +460,15 @@ func main() { fmt.Println("==============================================") } +// Helper functions +func stringPtr(s string) *string { + return &s +} + +func boolPtr(b bool) *bool { + return &b +} + func handleError(err error) { fmt.Println("\n==============================================") fmt.Println("TEST FAILED") From 2d89e6408bf1b00589192332b519652151ac882f Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 29 Jan 2026 18:18:51 +0000 Subject: [PATCH 35/39] tests: add manual test suite for java Signed-off-by: Kushal --- .../main/java/com/turbodocx/ManualTest.java | 221 +++++++++++++++++- 1 file changed, 219 insertions(+), 2 deletions(-) diff --git a/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java b/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java index 3951197..06c2921 100644 --- a/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java +++ b/packages/java-sdk/src/main/java/com/turbodocx/ManualTest.java @@ -1,7 +1,9 @@ package com.turbodocx; /* - * TurboSign Java SDK - Manual Test Suite + * TurboDocx Java SDK - Manual Test Suite + * + * Tests for both TurboSign (digital signatures) and TurboTemplate (document generation) * * Run: mvn exec:java -Dexec.mainClass="com.turbodocx.ManualTest" * @@ -32,13 +34,14 @@ public class ManualTest { private static final String TEST_PDF_PATH = "/path/to/your/test-document.pdf"; // Replace with path to your test PDF/DOCX private static final String TEST_EMAIL = "test-recipient@example.com"; // Replace with a real email to receive notifications private static final String FILE_URL = "https://example.com/sample-document.pdf"; // Replace with publicly accessible PDF URL + private static final String TEMPLATE_ID = "your-template-uuid-here"; // Replace with your template UUID private static TurboDocxClient client; private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); public static void main(String[] args) { System.out.println("=============================================="); - System.out.println("TurboSign Java SDK - Manual Test Suite"); + System.out.println("TurboDocx Java SDK - Manual Test Suite"); System.out.println("=============================================="); // Check if test PDF exists @@ -60,6 +63,8 @@ public static void main(String[] args) { try { // Uncomment and run tests as needed: + // ===== TurboSign Tests ===== + // Test 1: Prepare for Review // String reviewDocId = testPrepareForReview(); @@ -81,6 +86,23 @@ public static void main(String[] args) { // Test 7: Get Audit Trail (replace with actual document ID) // testGetAuditTrail("document-uuid-here"); + // ===== TurboTemplate Tests ===== + + // Test 8: Simple Variable Substitution + // String simpleDocId = testSimpleVariables(); + + // Test 9: Nested Objects with Dot Notation + // String nestedDocId = testNestedObjects(); + + // Test 10: Array Loops + // String loopsDocId = testArrayLoops(); + + // Test 11: Conditionals + // String conditionalsDocId = testConditionals(); + + // Test 12: Images + // String imagesDocId = testImages(); + System.out.println("\n=============================================="); System.out.println("All tests completed successfully!"); System.out.println("=============================================="); @@ -237,4 +259,199 @@ private static void testGetAuditTrail(String documentId) throws IOException { AuditTrailResponse result = client.turboSign().getAuditTrail(documentId); System.out.println("Result: " + gson.toJson(result)); } + + // ============================================= + // TURBOTEMPLATE TEST FUNCTIONS + // ============================================= + + /** + * Test 8: Simple Variable Substitution + * + * Template usage: "Dear {customer_name}, your order total is ${order_total}." + */ + private static String testSimpleVariables() throws IOException { + System.out.println("\n--- Test 8: Simple Variable Substitution ---"); + + GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId(TEMPLATE_ID) + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{customer_name}") + .name("customer_name") + .value("John Doe") + .mimeType("text") + .build(), + TemplateVariable.builder() + .placeholder("{order_total}") + .name("order_total") + .value(1500) + .mimeType("text") + .build(), + TemplateVariable.builder() + .placeholder("{order_date}") + .name("order_date") + .value("2024-01-01") + .mimeType("text") + .build() + )) + .name("Simple Substitution Document") + .description("Basic variable substitution example") + .outputFormat("pdf") + .build() + ); + + System.out.println("Result: " + gson.toJson(result)); + return result.getDeliverableId(); + } + + /** + * Test 9: Nested Objects with Dot Notation + * + * Template usage: "Name: {user.name}, Company: {user.profile.company}" + */ + private static String testNestedObjects() throws IOException { + System.out.println("\n--- Test 9: Nested Objects with Dot Notation ---"); + + GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId(TEMPLATE_ID) + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{user}") + .name("user") + .value(java.util.Map.of( + "name", "John Doe", + "email", "john@example.com", + "profile", java.util.Map.of( + "company", "Acme Corp", + "title", "Software Engineer", + "location", "San Francisco, CA" + ) + )) + .mimeType("json") + .usesAdvancedTemplatingEngine(true) + .build() + )) + .name("Nested Objects Document") + .description("Nested object with dot notation example") + .outputFormat("pdf") + .build() + ); + + System.out.println("Result: " + gson.toJson(result)); + return result.getDeliverableId(); + } + + /** + * Test 10: Array Loops + * + * Template usage: + * {#items} + * - {name}: {quantity} x ${price} + * {/items} + */ + private static String testArrayLoops() throws IOException { + System.out.println("\n--- Test 10: Array Loops ---"); + + GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId(TEMPLATE_ID) + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{items}") + .name("items") + .value(Arrays.asList( + java.util.Map.of("name", "Item A", "quantity", 5, "price", 100, "sku", "SKU-001"), + java.util.Map.of("name", "Item B", "quantity", 3, "price", 200, "sku", "SKU-002"), + java.util.Map.of("name", "Item C", "quantity", 10, "price", 50, "sku", "SKU-003") + )) + .mimeType("json") + .usesAdvancedTemplatingEngine(true) + .build() + )) + .name("Array Loops Document") + .description("Array loop iteration example") + .outputFormat("pdf") + .build() + ); + + System.out.println("Result: " + gson.toJson(result)); + return result.getDeliverableId(); + } + + /** + * Test 11: Conditionals + * + * Template usage: + * {#if is_premium} + * Premium Member Discount: {discount * 100}% + * {/if} + */ + private static String testConditionals() throws IOException { + System.out.println("\n--- Test 11: Conditionals ---"); + + GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId(TEMPLATE_ID) + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{is_premium}") + .name("is_premium") + .value(true) + .mimeType("json") + .usesAdvancedTemplatingEngine(true) + .build(), + TemplateVariable.builder() + .placeholder("{discount}") + .name("discount") + .value(0.2) + .mimeType("json") + .usesAdvancedTemplatingEngine(true) + .build() + )) + .name("Conditionals Document") + .description("Boolean conditional example") + .outputFormat("pdf") + .build() + ); + + System.out.println("Result: " + gson.toJson(result)); + return result.getDeliverableId(); + } + + /** + * Test 12: Images + * + * Template usage: Insert {logo} at the top of the document + */ + private static String testImages() throws IOException { + System.out.println("\n--- Test 12: Images ---"); + + GenerateTemplateResponse result = client.turboTemplate().generate( + GenerateTemplateRequest.builder() + .templateId(TEMPLATE_ID) + .variables(Arrays.asList( + TemplateVariable.builder() + .placeholder("{title}") + .name("title") + .value("Quarterly Report") + .mimeType("text") + .build(), + TemplateVariable.builder() + .placeholder("{logo}") + .name("logo") + .value("https://example.com/logo.png") + .mimeType("image") + .build() + )) + .name("Document with Images") + .description("Using image variables") + .outputFormat("pdf") + .build() + ); + + System.out.println("Result: " + gson.toJson(result)); + return result.getDeliverableId(); + } } From 223fae6e3990c9d46a8c9f6b80a5b785823a5a2c Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 29 Jan 2026 18:18:58 +0000 Subject: [PATCH 36/39] tests: add manual test suite for js Signed-off-by: Kushal --- packages/js-sdk/manual-test.ts | 183 ++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 3 deletions(-) diff --git a/packages/js-sdk/manual-test.ts b/packages/js-sdk/manual-test.ts index 9071e24..19d4aa1 100644 --- a/packages/js-sdk/manual-test.ts +++ b/packages/js-sdk/manual-test.ts @@ -1,12 +1,14 @@ /** - * TurboSign JS SDK - Manual Test Suite + * TurboDocx JS SDK - Manual Test Suite + * + * Tests for both TurboSign (digital signatures) and TurboTemplate (document generation) * * Run: npx ts-node manual-test.ts * * Make sure to configure the values below before running. */ -import { TurboSign } from "./src"; +import { TurboSign, TurboTemplate } from "./src"; import * as fs from "fs"; // ============================================= @@ -19,8 +21,9 @@ const ORG_ID = "your-org-id-here"; // Replace with your organization UUID const TEST_PDF_PATH = "./test-document.pdf"; // Replace with path to your test PDF/DOCX const TEST_EMAIL = "recipient@example.com"; // Replace with a real email to receive notifications const FILE_URL = "https://example.com/your-document.pdf"; // Replace with publicly accessible PDF URL +const TEMPLATE_ID = "your-template-uuid-here"; // Replace with your template UUID -// Initialize client +// Initialize TurboSign client TurboSign.configure({ apiKey: API_KEY, baseUrl: BASE_URL, @@ -29,6 +32,13 @@ TurboSign.configure({ senderName: "Your Company Name", // Sender name shown in emails }); +// Initialize TurboTemplate client +TurboTemplate.configure({ + apiKey: API_KEY, + baseUrl: BASE_URL, + orgId: ORG_ID, +}); + // ============================================= // TEST FUNCTIONS // ============================================= @@ -156,6 +166,154 @@ async function testGetAuditTrail(documentId: string) { return result; } +// ============================================= +// TURBOTEMPLATE TEST FUNCTIONS +// ============================================= + +/** + * Test 8: Simple Variable Substitution + * + * Template usage: "Dear {customer_name}, your order total is ${order_total}." + */ +async function testSimpleVariables() { + console.log("\n--- Test 8: Simple Variable Substitution ---"); + + const result = await TurboTemplate.generate({ + templateId: TEMPLATE_ID, + variables: [ + { placeholder: "{customer_name}", name: "customer_name", value: "John Doe", mimeType: "text" }, + { placeholder: "{order_total}", name: "order_total", value: 1500, mimeType: "text" }, + { placeholder: "{order_date}", name: "order_date", value: "2024-01-01", mimeType: "text" }, + ], + name: "Simple Substitution Document", + description: "Basic variable substitution example", + outputFormat: "pdf", + }); + + console.log("Result:", JSON.stringify(result, null, 2)); + return result.deliverableId; +} + +/** + * Test 9: Nested Objects with Dot Notation + * + * Template usage: "Name: {user.name}, Company: {user.profile.company}" + */ +async function testNestedObjects() { + console.log("\n--- Test 9: Nested Objects with Dot Notation ---"); + + const result = await TurboTemplate.generate({ + templateId: TEMPLATE_ID, + variables: [ + { + placeholder: "{user}", + name: "user", + value: { + name: "John Doe", + email: "john@example.com", + profile: { + company: "Acme Corp", + title: "Software Engineer", + location: "San Francisco, CA" + } + }, + mimeType: "json", + usesAdvancedTemplatingEngine: true, + }, + ], + name: "Nested Objects Document", + description: "Nested object with dot notation example", + outputFormat: "pdf", + }); + + console.log("Result:", JSON.stringify(result, null, 2)); + return result.deliverableId; +} + +/** + * Test 10: Array Loops + * + * Template usage: + * {#items} + * - {name}: {quantity} x ${price} + * {/items} + */ +async function testArrayLoops() { + console.log("\n--- Test 10: Array Loops ---"); + + const result = await TurboTemplate.generate({ + templateId: TEMPLATE_ID, + variables: [ + { + placeholder: "{items}", + name: "items", + value: [ + { name: "Item A", quantity: 5, price: 100, sku: "SKU-001" }, + { name: "Item B", quantity: 3, price: 200, sku: "SKU-002" }, + { name: "Item C", quantity: 10, price: 50, sku: "SKU-003" }, + ], + mimeType: "json", + usesAdvancedTemplatingEngine: true, + }, + ], + name: "Array Loops Document", + description: "Array loop iteration example", + outputFormat: "pdf", + }); + + console.log("Result:", JSON.stringify(result, null, 2)); + return result.deliverableId; +} + +/** + * Test 11: Conditionals + * + * Template usage: + * {#if is_premium} + * Premium Member Discount: {discount * 100}% + * {/if} + */ +async function testConditionals() { + console.log("\n--- Test 11: Conditionals ---"); + + const result = await TurboTemplate.generate({ + templateId: TEMPLATE_ID, + variables: [ + { placeholder: "{is_premium}", name: "is_premium", value: true, mimeType: "json", usesAdvancedTemplatingEngine: true }, + { placeholder: "{discount}", name: "discount", value: 0.2, mimeType: "json", usesAdvancedTemplatingEngine: true }, + ], + name: "Conditionals Document", + description: "Boolean conditional example", + outputFormat: "pdf", + }); + + console.log("Result:", JSON.stringify(result, null, 2)); + return result.deliverableId; +} + +/** + * Test 12: Images + * + * Template usage: Insert {logo} at the top of the document + */ +async function testImages() { + console.log("\n--- Test 12: Images ---"); + + const result = await TurboTemplate.generate({ + templateId: TEMPLATE_ID, + variables: [ + { placeholder: "{title}", name: "title", value: "Quarterly Report", mimeType: "text" }, + { placeholder: "{logo}", name: "logo", value: "https://example.com/logo.png", mimeType: "image" }, + ], + name: "Document with Images", + description: "Using image variables", + outputFormat: "pdf", + }); + + console.log("Result:", JSON.stringify(result, null, 2)); + return result.deliverableId; +} + // ============================================= // MAIN TEST RUNNER // ============================================= @@ -175,6 +333,8 @@ async function runAllTests() { try { // Uncomment and run tests as needed: + // ===== TurboSign Tests ===== + // Test 1: Create Signature Review Link // const reviewDocId = await testCreateSignatureReviewLink(); @@ -196,6 +356,23 @@ async function runAllTests() { // Test 7: Get Audit Trail (replace with actual document ID) // await testGetAuditTrail("document-uuid-here"); + // ===== TurboTemplate Tests ===== + + // Test 8: Simple Variable Substitution + // const simpleDocId = await testSimpleVariables(); + + // Test 9: Nested Objects with Dot Notation + // const nestedDocId = await testNestedObjects(); + + // Test 10: Array Loops + // const loopsDocId = await testArrayLoops(); + + // Test 11: Conditionals + // const conditionalsDocId = await testConditionals(); + + // Test 12: Images + // const imagesDocId = await testImages(); + console.log("\n=============================================="); console.log("All tests completed successfully!"); console.log("=============================================="); From bb51eb4e2628ee2f2b304a7cb9d093c322fd5bec Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 29 Jan 2026 18:19:03 +0000 Subject: [PATCH 37/39] tests: add manual test suite for php Signed-off-by: Kushal --- packages/php-sdk/manual_test.php | 240 ++++++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 3 deletions(-) diff --git a/packages/php-sdk/manual_test.php b/packages/php-sdk/manual_test.php index cdda633..8190340 100644 --- a/packages/php-sdk/manual_test.php +++ b/packages/php-sdk/manual_test.php @@ -1,7 +1,9 @@ deliverableId; +} + +/** + * Test 9: Nested Objects with Dot Notation + * + * Template usage: "Name: {user.name}, Company: {user.profile.company}" + */ +function testNestedObjects(): string +{ + echo "\n--- Test 9: Nested Objects with Dot Notation ---\n"; + + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: TEMPLATE_ID, + variables: [ + new TemplateVariable( + placeholder: '{user}', + name: 'user', + value: [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'profile' => [ + 'company' => 'Acme Corp', + 'title' => 'Software Engineer', + 'location' => 'San Francisco, CA', + ], + ], + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true + ), + ], + name: 'Nested Objects Document', + description: 'Nested object with dot notation example', + outputFormat: OutputFormat::PDF + ) + ); + + echo "Result: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + return $result->deliverableId; +} + +/** + * Test 10: Array Loops + * + * Template usage: + * {#items} + * - {name}: {quantity} x ${price} + * {/items} + */ +function testArrayLoops(): string +{ + echo "\n--- Test 10: Array Loops ---\n"; + + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: TEMPLATE_ID, + variables: [ + new TemplateVariable( + placeholder: '{items}', + name: 'items', + value: [ + ['name' => 'Item A', 'quantity' => 5, 'price' => 100, 'sku' => 'SKU-001'], + ['name' => 'Item B', 'quantity' => 3, 'price' => 200, 'sku' => 'SKU-002'], + ['name' => 'Item C', 'quantity' => 10, 'price' => 50, 'sku' => 'SKU-003'], + ], + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true + ), + ], + name: 'Array Loops Document', + description: 'Array loop iteration example', + outputFormat: OutputFormat::PDF + ) + ); + + echo "Result: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + return $result->deliverableId; +} + +/** + * Test 11: Conditionals + * + * Template usage: + * {#if is_premium} + * Premium Member Discount: {discount * 100}% + * {/if} + */ +function testConditionals(): string +{ + echo "\n--- Test 11: Conditionals ---\n"; + + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: TEMPLATE_ID, + variables: [ + new TemplateVariable( + placeholder: '{is_premium}', + name: 'is_premium', + value: true, + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true + ), + new TemplateVariable( + placeholder: '{discount}', + name: 'discount', + value: 0.2, + mimeType: VariableMimeType::JSON, + usesAdvancedTemplatingEngine: true + ), + ], + name: 'Conditionals Document', + description: 'Boolean conditional example', + outputFormat: OutputFormat::PDF + ) + ); + + echo "Result: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + return $result->deliverableId; +} + +/** + * Test 12: Images + * + * Template usage: Insert {logo} at the top of the document + */ +function testImages(): string +{ + echo "\n--- Test 12: Images ---\n"; + + $result = TurboTemplate::generate( + new GenerateTemplateRequest( + templateId: TEMPLATE_ID, + variables: [ + new TemplateVariable( + placeholder: '{title}', + name: 'title', + value: 'Quarterly Report', + mimeType: VariableMimeType::TEXT + ), + new TemplateVariable( + placeholder: '{logo}', + name: 'logo', + value: 'https://example.com/logo.png', + mimeType: VariableMimeType::IMAGE + ), + ], + name: 'Document with Images', + description: 'Using image variables', + outputFormat: OutputFormat::PDF + ) + ); + + echo "Result: " . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + return $result->deliverableId; +} + // ============================================= // MAIN TEST RUNNER // ============================================= @@ -214,7 +429,7 @@ function testGetAuditTrail(string $documentId): void function main(): void { echo "==============================================\n"; - echo "TurboSign PHP SDK - Manual Test Suite\n"; + echo "TurboDocx PHP SDK - Manual Test Suite\n"; echo "==============================================\n"; // Check if test PDF exists @@ -227,6 +442,8 @@ function main(): void try { // Uncomment and run tests as needed: + // ===== TurboSign Tests ===== + // Test 1: Prepare for Review (uses fileLink, doesn't need PDF file) // $reviewDocId = testCreateSignatureReviewLink(); @@ -248,6 +465,23 @@ function main(): void // Test 7: Get Audit Trail (replace with actual document ID) // testGetAuditTrail('document-uuid-here'); + // ===== TurboTemplate Tests ===== + + // Test 8: Simple Variable Substitution + // $simpleDocId = testSimpleVariables(); + + // Test 9: Nested Objects with Dot Notation + // $nestedDocId = testNestedObjects(); + + // Test 10: Array Loops + // $loopsDocId = testArrayLoops(); + + // Test 11: Conditionals + // $conditionalsDocId = testConditionals(); + + // Test 12: Images + // $imagesDocId = testImages(); + echo "\n==============================================\n"; echo "All tests completed successfully!\n"; echo "==============================================\n"; From 5b5ab5af241f2a2e971e9a768ef8fe01b38dac0d Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 29 Jan 2026 18:19:12 +0000 Subject: [PATCH 38/39] tests: add manual test suite for python Signed-off-by: Kushal --- packages/py-sdk/manual_test.py | 183 ++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 3 deletions(-) diff --git a/packages/py-sdk/manual_test.py b/packages/py-sdk/manual_test.py index 9de233d..d1ae6c9 100644 --- a/packages/py-sdk/manual_test.py +++ b/packages/py-sdk/manual_test.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """ -TurboSign Python SDK - Manual Test Suite +TurboDocx Python SDK - Manual Test Suite + +Tests for both TurboSign (digital signatures) and TurboTemplate (document generation) Run: python manual_test.py @@ -12,7 +14,7 @@ import os import sys -from turbodocx_sdk import TurboSign +from turbodocx_sdk import TurboSign, TurboTemplate # ============================================= # CONFIGURE THESE VALUES BEFORE RUNNING @@ -23,6 +25,7 @@ TEST_PDF_PATH = "/path/to/your/test-document.pdf" # Replace with path to your test PDF/DOCX TEST_EMAIL = "test-recipient@example.com" # Replace with a real email to receive notifications +TEMPLATE_ID = "your-template-uuid-here" # Replace with your template UUID # Configure TurboSign TurboSign.configure( @@ -33,6 +36,13 @@ sender_name="Your Company Name" # Sender name shown in emails ) +# Configure TurboTemplate +TurboTemplate.configure( + api_key=API_KEY, + base_url=BASE_URL, + org_id=ORG_ID +) + # ============================================= # TEST FUNCTIONS @@ -177,13 +187,161 @@ async def test_get_audit_trail(document_id: str): return result +# ============================================= +# TURBOTEMPLATE TEST FUNCTIONS +# ============================================= + +async def test_simple_variables(): + """ + Test 8: Simple Variable Substitution + + Template usage: "Dear {customer_name}, your order total is ${order_total}." + """ + print("\n--- Test 8: Simple Variable Substitution ---") + + result = await TurboTemplate.generate( + template_id=TEMPLATE_ID, + variables=[ + {"placeholder": "{customer_name}", "name": "customer_name", "value": "John Doe", "mimeType": "text"}, + {"placeholder": "{order_total}", "name": "order_total", "value": 1500, "mimeType": "text"}, + {"placeholder": "{order_date}", "name": "order_date", "value": "2024-01-01", "mimeType": "text"}, + ], + name="Simple Substitution Document", + description="Basic variable substitution example", + output_format="pdf", + ) + + print("Result:", json.dumps(result, indent=2)) + return result.get("deliverableId") + + +async def test_nested_objects(): + """ + Test 9: Nested Objects with Dot Notation + + Template usage: "Name: {user.name}, Company: {user.profile.company}" + """ + print("\n--- Test 9: Nested Objects with Dot Notation ---") + + result = await TurboTemplate.generate( + template_id=TEMPLATE_ID, + variables=[ + { + "placeholder": "{user}", + "name": "user", + "value": { + "name": "John Doe", + "email": "john@example.com", + "profile": { + "company": "Acme Corp", + "title": "Software Engineer", + "location": "San Francisco, CA" + } + }, + "mimeType": "json", + "usesAdvancedTemplatingEngine": True, + }, + ], + name="Nested Objects Document", + description="Nested object with dot notation example", + output_format="pdf", + ) + + print("Result:", json.dumps(result, indent=2)) + return result.get("deliverableId") + + +async def test_array_loops(): + """ + Test 10: Array Loops + + Template usage: + {#items} + - {name}: {quantity} x ${price} + {/items} + """ + print("\n--- Test 10: Array Loops ---") + + result = await TurboTemplate.generate( + template_id=TEMPLATE_ID, + variables=[ + { + "placeholder": "{items}", + "name": "items", + "value": [ + {"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}, + {"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}, + {"name": "Item C", "quantity": 10, "price": 50, "sku": "SKU-003"}, + ], + "mimeType": "json", + "usesAdvancedTemplatingEngine": True, + }, + ], + name="Array Loops Document", + description="Array loop iteration example", + output_format="pdf", + ) + + print("Result:", json.dumps(result, indent=2)) + return result.get("deliverableId") + + +async def test_conditionals(): + """ + Test 11: Conditionals + + Template usage: + {#if is_premium} + Premium Member Discount: {discount * 100}% + {/if} + """ + print("\n--- Test 11: Conditionals ---") + + result = await TurboTemplate.generate( + template_id=TEMPLATE_ID, + variables=[ + {"placeholder": "{is_premium}", "name": "is_premium", "value": True, "mimeType": "json", "usesAdvancedTemplatingEngine": True}, + {"placeholder": "{discount}", "name": "discount", "value": 0.2, "mimeType": "json", "usesAdvancedTemplatingEngine": True}, + ], + name="Conditionals Document", + description="Boolean conditional example", + output_format="pdf", + ) + + print("Result:", json.dumps(result, indent=2)) + return result.get("deliverableId") + + +async def test_images(): + """ + Test 12: Images + + Template usage: Insert {logo} at the top of the document + """ + print("\n--- Test 12: Images ---") + + result = await TurboTemplate.generate( + template_id=TEMPLATE_ID, + variables=[ + {"placeholder": "{title}", "name": "title", "value": "Quarterly Report", "mimeType": "text"}, + {"placeholder": "{logo}", "name": "logo", "value": "https://example.com/logo.png", "mimeType": "image"}, + ], + name="Document with Images", + description="Using image variables", + output_format="pdf", + ) + + print("Result:", json.dumps(result, indent=2)) + return result.get("deliverableId") + + # ============================================= # MAIN TEST RUNNER # ============================================= async def run_all_tests(): print("==============================================") - print("TurboSign Python SDK - Manual Test Suite") + print("TurboDocx Python SDK - Manual Test Suite") print("==============================================") # Check if test PDF exists @@ -195,6 +353,8 @@ async def run_all_tests(): try: # Uncomment and run tests as needed: + # ===== TurboSign Tests ===== + # Test 1: Prepare for Review # review_doc_id = await test_create_signature_review_link() @@ -216,6 +376,23 @@ async def run_all_tests(): # Test 7: Get Audit Trail (replace with actual document ID) # await test_get_audit_trail("document-uuid-here") + # ===== TurboTemplate Tests ===== + + # Test 8: Simple Variable Substitution + # simple_doc_id = await test_simple_variables() + + # Test 9: Nested Objects with Dot Notation + # nested_doc_id = await test_nested_objects() + + # Test 10: Array Loops + # loops_doc_id = await test_array_loops() + + # Test 11: Conditionals + # conditionals_doc_id = await test_conditionals() + + # Test 12: Images + # images_doc_id = await test_images() + print("\n==============================================") print("All tests completed successfully!") print("==============================================") From 8e9a982658c6c883ed16558f6618257e80f37664 Mon Sep 17 00:00:00 2001 From: Kushal Date: Thu, 29 Jan 2026 18:19:21 +0000 Subject: [PATCH 39/39] tests: add manual test suite for rust Signed-off-by: Kushal --- packages/rust-sdk/Cargo.toml | 4 + packages/rust-sdk/manual-test.rs | 394 +++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 packages/rust-sdk/manual-test.rs diff --git a/packages/rust-sdk/Cargo.toml b/packages/rust-sdk/Cargo.toml index dc05a57..4c0cf44 100644 --- a/packages/rust-sdk/Cargo.toml +++ b/packages/rust-sdk/Cargo.toml @@ -19,3 +19,7 @@ once_cell = "1.19" [dev-dependencies] tokio-test = "0.4" + +[[bin]] +name = "manual-test" +path = "manual-test.rs" diff --git a/packages/rust-sdk/manual-test.rs b/packages/rust-sdk/manual-test.rs new file mode 100644 index 0000000..c05b643 --- /dev/null +++ b/packages/rust-sdk/manual-test.rs @@ -0,0 +1,394 @@ +/** + * TurboDocx Rust SDK - Manual Test Suite + * + * Tests for both TurboSign (digital signatures) and TurboTemplate (document generation) + * + * Run: cargo run --bin manual-test + * + * Make sure to configure the values below before running. + * + * Note: Add this to Cargo.toml under [[bin]]: + * [[bin]] + * name = "manual-test" + * path = "manual-test.rs" + */ + +use serde_json::json; +use std::fs; +use turbodocx_sdk::{ + http::HttpClientConfig, CreateSignatureReviewLinkRequest, Field, GenerateTemplateRequest, + OutputFormat, Recipient, SendSignatureRequest, SignatureFieldType, TemplateVariable, TurboSign, + TurboTemplate, +}; + +// ============================================= +// CONFIGURE THESE VALUES BEFORE RUNNING +// ============================================= +const API_KEY: &str = "your-api-key-here"; // Replace with your actual TurboDocx API key +const BASE_URL: &str = "https://api.turbodocx.com"; // Replace with your API URL +const ORG_ID: &str = "your-org-id-here"; // Replace with your organization UUID + +const TEST_PDF_PATH: &str = "./test-document.pdf"; // Replace with path to your test PDF/DOCX +const TEST_EMAIL: &str = "recipient@example.com"; // Replace with a real email to receive notifications +const TEMPLATE_ID: &str = "your-template-uuid-here"; // Replace with your template UUID + +// ============================================= +// TURBOSIGN TEST FUNCTIONS +// ============================================= + +/// Test 1: Create Signature Review Link +async fn test_create_signature_review_link() -> Result> { + println!("\n--- Test 1: createSignatureReviewLink ---"); + + let pdf_bytes = fs::read(TEST_PDF_PATH)?; + + let request = CreateSignatureReviewLinkRequest { + file: Some(pdf_bytes), + file_link: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![Recipient::new("Test User", TEST_EMAIL, 1)], + fields: vec![ + Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 550.0, + 200.0, + 50.0, + TEST_EMAIL, + ), + Field::coordinate_based( + SignatureFieldType::Checkbox, + 1, + 320.0, + 550.0, + 50.0, + 50.0, + TEST_EMAIL, + ), + ], + document_name: Some("Review Test Document".to_string()), + document_description: None, + sender_name: None, + sender_email: None, + cc_emails: None, + }; + + let result = TurboSign::create_signature_review_link(request).await?; + println!("Result: {:#?}", result); + Ok(result.document_id) +} + +/// Test 2: Send Signature +async fn test_send_signature() -> Result> { + println!("\n--- Test 2: sendSignature ---"); + + let pdf_bytes = fs::read(TEST_PDF_PATH)?; + + let request = SendSignatureRequest { + file: Some(pdf_bytes), + file_link: None, + file_name: None, + deliverable_id: None, + template_id: None, + recipients: vec![Recipient::new("Signer One", TEST_EMAIL, 1)], + fields: vec![ + Field::coordinate_based( + SignatureFieldType::Signature, + 1, + 100.0, + 550.0, + 200.0, + 50.0, + TEST_EMAIL, + ), + Field::coordinate_based( + SignatureFieldType::Checkbox, + 1, + 320.0, + 550.0, + 50.0, + 50.0, + TEST_EMAIL, + ), + ], + document_name: Some("Signing Test Document".to_string()), + document_description: Some("Sample contract for testing".to_string()), + sender_name: None, + sender_email: None, + cc_emails: Some(vec!["cc@example.com".to_string()]), + }; + + let result = TurboSign::send_signature(request).await?; + println!("Result: {:#?}", result); + Ok(result.document_id) +} + +/// Test 3: Get Status +async fn test_get_status(document_id: &str) -> Result<(), Box> { + println!("\n--- Test 3: getStatus ---"); + + let result = TurboSign::get_status(document_id).await?; + println!("Result: {:#?}", result); + Ok(()) +} + +/// Test 4: Download +async fn test_download(document_id: &str) -> Result<(), Box> { + println!("\n--- Test 4: download ---"); + + let download_url = TurboSign::download(document_id).await?; + println!("Download URL: {}", download_url); + Ok(()) +} + +/// Test 5: Resend Emails +async fn test_resend( + document_id: &str, + recipient_ids: Vec<&str>, +) -> Result<(), Box> { + println!("\n--- Test 5: resendEmails ---"); + + let result = TurboSign::resend_emails(document_id, recipient_ids).await?; + println!("Result: {:#?}", result); + Ok(()) +} + +/// Test 6: Void Document +async fn test_void(document_id: &str) -> Result<(), Box> { + println!("\n--- Test 6: voidDocument ---"); + + let result = TurboSign::void_document(document_id, Some("Testing void functionality")).await?; + println!("Result: {:#?}", result); + Ok(()) +} + +/// Test 7: Get Audit Trail +async fn test_get_audit_trail(document_id: &str) -> Result<(), Box> { + println!("\n--- Test 7: getAuditTrail ---"); + + let result = TurboSign::get_audit_trail(document_id).await?; + println!("Result: {:#?}", result); + Ok(()) +} + +// ============================================= +// TURBOTEMPLATE TEST FUNCTIONS +// ============================================= + +/// Test 8: Simple Variable Substitution +/// +/// Template usage: "Dear {customer_name}, your order total is ${order_total}." +async fn test_simple_variables() -> Result> { + println!("\n--- Test 8: Simple Variable Substitution ---"); + + let request = GenerateTemplateRequest::new( + TEMPLATE_ID, + vec![ + TemplateVariable::simple("{customer_name}", "customer_name", "John Doe"), + TemplateVariable::simple("{order_total}", "order_total", 1500), + TemplateVariable::simple("{order_date}", "order_date", "2024-01-01"), + ], + ) + .with_name("Simple Substitution Document") + .with_description("Basic variable substitution example") + .with_output_format(OutputFormat::Pdf); + + let result = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", result.deliverable_id); + Ok(result.deliverable_id.unwrap_or_default()) +} + +/// Test 9: Nested Objects with Dot Notation +/// +/// Template usage: "Name: {user.name}, Company: {user.profile.company}" +async fn test_nested_objects() -> Result> { + println!("\n--- Test 9: Nested Objects with Dot Notation ---"); + + let user_data = json!({ + "name": "John Doe", + "email": "john@example.com", + "profile": { + "company": "Acme Corp", + "title": "Software Engineer", + "location": "San Francisco, CA" + } + }); + + let request = GenerateTemplateRequest::new( + TEMPLATE_ID, + vec![TemplateVariable::advanced_engine( + "{user}", "user", user_data, + )?], + ) + .with_name("Nested Objects Document") + .with_description("Nested object with dot notation example") + .with_output_format(OutputFormat::Pdf); + + let result = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", result.deliverable_id); + Ok(result.deliverable_id.unwrap_or_default()) +} + +/// Test 10: Array Loops +/// +/// Template usage: +/// {#items} +/// - {name}: {quantity} x ${price} +/// {/items} +async fn test_array_loops() -> Result> { + println!("\n--- Test 10: Array Loops ---"); + + let items = vec![ + json!({"name": "Item A", "quantity": 5, "price": 100, "sku": "SKU-001"}), + json!({"name": "Item B", "quantity": 3, "price": 200, "sku": "SKU-002"}), + json!({"name": "Item C", "quantity": 10, "price": 50, "sku": "SKU-003"}), + ]; + + let request = GenerateTemplateRequest::new( + TEMPLATE_ID, + vec![TemplateVariable::loop_var("{items}", "items", items)?], + ) + .with_name("Array Loops Document") + .with_description("Array loop iteration example") + .with_output_format(OutputFormat::Pdf); + + let result = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", result.deliverable_id); + Ok(result.deliverable_id.unwrap_or_default()) +} + +/// Test 11: Conditionals +/// +/// Template usage: +/// {#if is_premium} +/// Premium Member Discount: {discount * 100}% +/// {/if} +async fn test_conditionals() -> Result> { + println!("\n--- Test 11: Conditionals ---"); + + let request = GenerateTemplateRequest::new( + TEMPLATE_ID, + vec![ + TemplateVariable::conditional("{is_premium}", "is_premium", true), + TemplateVariable::conditional("{discount}", "discount", 0.2), + ], + ) + .with_name("Conditionals Document") + .with_description("Boolean conditional example") + .with_output_format(OutputFormat::Pdf); + + let result = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", result.deliverable_id); + Ok(result.deliverable_id.unwrap_or_default()) +} + +/// Test 12: Images +/// +/// Template usage: Insert {logo} at the top of the document +async fn test_images() -> Result> { + println!("\n--- Test 12: Images ---"); + + let request = GenerateTemplateRequest::new( + TEMPLATE_ID, + vec![ + TemplateVariable::simple("{title}", "title", "Quarterly Report"), + TemplateVariable::image("{logo}", "logo", "https://example.com/logo.png"), + ], + ) + .with_name("Document with Images") + .with_description("Using image variables") + .with_output_format(OutputFormat::Pdf); + + let result = TurboTemplate::generate(request).await?; + println!("✓ Deliverable ID: {:?}", result.deliverable_id); + Ok(result.deliverable_id.unwrap_or_default()) +} + +// ============================================= +// MAIN TEST RUNNER +// ============================================= + +#[tokio::main] +async fn main() { + println!("=============================================="); + println!("TurboDocx Rust SDK - Manual Test Suite"); + println!("=============================================="); + + // Check if test PDF exists + if !std::path::Path::new(TEST_PDF_PATH).exists() { + eprintln!("\nError: Test PDF not found at {}", TEST_PDF_PATH); + eprintln!("Please add a test PDF file and try again."); + std::process::exit(1); + } + + // Configure TurboSign + if let Err(e) = TurboSign::configure( + HttpClientConfig::new(API_KEY) + .with_org_id(ORG_ID) + .with_base_url(BASE_URL) + .with_sender_email("sender@example.com") + .with_sender_name("Your Company Name"), + ) { + eprintln!("Failed to configure TurboSign: {}", e); + std::process::exit(1); + } + + // Configure TurboTemplate + if let Err(e) = TurboTemplate::configure( + HttpClientConfig::new(API_KEY) + .with_org_id(ORG_ID) + .with_base_url(BASE_URL), + ) { + eprintln!("Failed to configure TurboTemplate: {}", e); + std::process::exit(1); + } + + // Uncomment and run tests as needed: + + // ===== TurboSign Tests ===== + + // Test 1: Create Signature Review Link + // let review_doc_id = test_create_signature_review_link().await.unwrap(); + + // Test 2: Send Signature (creates a new document) + // let sign_doc_id = test_send_signature().await.unwrap(); + + // Test 3: Get Status (replace with actual document ID) + // test_get_status("document-uuid-here").await.unwrap(); + + // Test 4: Download (replace with actual document ID) + // test_download("document-uuid-here").await.unwrap(); + + // Test 5: Resend (replace with actual document ID and recipient ID) + // test_resend("document-uuid-here", vec!["recipient-uuid-here"]).await.unwrap(); + + // Test 6: Void (do this last as it cancels the document) + // test_void("document-uuid-here").await.unwrap(); + + // Test 7: Get Audit Trail (replace with actual document ID) + // test_get_audit_trail("document-uuid-here").await.unwrap(); + + // ===== TurboTemplate Tests ===== + + // Test 8: Simple Variable Substitution + // let simple_doc_id = test_simple_variables().await.unwrap(); + + // Test 9: Nested Objects with Dot Notation + // let nested_doc_id = test_nested_objects().await.unwrap(); + + // Test 10: Array Loops + // let loops_doc_id = test_array_loops().await.unwrap(); + + // Test 11: Conditionals + // let conditionals_doc_id = test_conditionals().await.unwrap(); + + // Test 12: Images + // let images_doc_id = test_images().await.unwrap(); + + println!("\n=============================================="); + println!("All tests completed successfully!"); + println!("=============================================="); +}