INTRODUCTION
Modern applications increasingly require extensibility and automation capabilities that allow users to customize behavior, automate repetitive tasks, and integrate with external systems. While many applications provide graphical user interfaces for manual operations, power users and system administrators often need programmatic access to the same functionality. Scripting systems bridge this gap by exposing application internals through a controlled, secure interface that maintains the integrity of the application while providing powerful automation capabilities.
This article presents a comprehensive architectural approach to exposing application functionality to scripting systems. We explore the design patterns, security considerations, and implementation strategies necessary to create a production-ready scripting interface. The approach is applicable to any type of application - from document management systems to CAD software, from financial applications to content management systems.
We use a Document Management System as our running example throughout this article. The example demonstrates all architectural concepts with complete, working code in Python. However, the principles and patterns presented are language-agnostic and can be applied to applications written in any programming language.
THE FUNDAMENTAL CHALLENGE
Applications have internal functionality that performs operations, manages state, and enforces business rules. This functionality is typically accessed through user interface components like buttons, menus, and dialogs. When we want to expose this functionality to scripts, we face several challenges:
Encapsulation: Application internals should remain encapsulated and not be directly accessible to scripts. Direct access would create tight coupling, making the application difficult to maintain and evolve.
Security: Scripts should not be able to bypass security checks or access functionality beyond their authorization level. Malicious or poorly written scripts could corrupt data or compromise system integrity.
Transactionality: Operations should support undo and redo, allowing users to reverse script actions. This requires maintaining state and implementing proper rollback mechanisms.
Type Safety: Scripts use dynamic types while applications often use static types. We need proper conversion and validation at the boundary between scripts and application code.
Versioning: As the application evolves, the scripting interface must remain stable or provide clear migration paths for existing scripts.
Error Handling: Scripts need meaningful error messages when operations fail, without exposing internal implementation details that could be security risks.
The solution to these challenges is a layered architecture that provides controlled access to application functionality through well-defined interfaces.
ARCHITECTURAL OVERVIEW
The architecture for exposing application functionality consists of five major layers, each with specific responsibilities:
┌─────────────────────────────────────────────────────────────┐
│ SCRIPT LAYER │
│ User-written scripts in the scripting language │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ SCRIPT INTERFACE LAYER │
│ Built-in functions callable from scripts │
│ Type conversion between script and application types │└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ COMMAND LAYER │
│ Command objects implementing the Command pattern │
│ Execute, Undo, Redo, Validate operations │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ COMMAND PROCESSOR LAYER │
│ Authorization checking │
│ Command execution coordination │
│ Undo/Redo stack management │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ Core application functionality │
│ Business logic and data management │
└─────────────────────────────────────────────────────────────┘
Script Layer: Contains user-written scripts in the scripting language. Scripts call built-in functions to access application functionality.
Script Interface Layer: Provides built-in functions that scripts can call. Handles type conversion between script types and application types. Returns results in script-compatible formats.
Command Layer: Implements the Command pattern for all operations. Each command encapsulates an operation with execute, undo, and validation methods.
Command Processor Layer: Coordinates command execution, enforces authorization policies, manages undo/redo stacks, and provides event notifications.
Application Layer: Contains the core application functionality, business logic, and data management. This layer is unaware of scripting and operates independently.
This layered architecture provides clear separation of concerns, making the system maintainable and testable. Each layer has well-defined interfaces and can evolve independently.
THE APPLICATION LAYER
We begin with the application layer, which contains the core functionality that we want to expose to scripts. For our example, we implement a Document Management System with user management, document operations, and workflow capabilities.
The application layer should be designed without any knowledge of scripting. It provides a clean API that can be used by any client - whether a graphical user interface, web service, or scripting system.
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import datetime
from enum import Enum, auto
class DocumentStatus(Enum):
"""Status of a document in the system."""
DRAFT = auto()
PENDING_REVIEW = auto()
APPROVED = auto()
PUBLISHED = auto()
ARCHIVED = auto()
class UserRole(Enum):
"""User roles in the system."""
VIEWER = auto()
EDITOR = auto()
REVIEWER = auto()
ADMIN = auto()
@dataclass
class Document:
"""Represents a document in the system."""
document_id: str
title: str
content: str
author_id: str
status: DocumentStatus
version: int = 1
created_at: datetime = None
modified_at: datetime = None
tags: List[str] = None
class DocumentManagementSystem:
"""
Core application - Document Management System.
This represents the internal application functionality.
"""
def __init__(self):
self.documents: Dict[str, Document] = {}
self.users: Dict[str, User] = {}
self.workflow_tasks: Dict[str, WorkflowTask] = {}
self.current_user: Optional[User] = None
def create_document(self, title: str, content: str,
tags: List[str] = None) -> Document:
"""Create a new document."""
# Implementation creates document and returns it
pass
def update_document(self, document_id: str, title: str = None,
content: str = None) -> bool:
"""Update an existing document."""
# Implementation updates document
pass
def change_document_status(self, document_id: str,
new_status: DocumentStatus) -> bool:
"""Change a document's status."""
# Implementation changes status
pass
def search_documents(self, query: str) -> List[Document]:
"""Search documents by query."""
# Implementation searches and returns results
pass
The application layer provides methods that perform operations and return results. These methods enforce business rules, validate inputs, and maintain data integrity. They have no knowledge of commands, scripts, or authorization - those concerns are handled in higher layers.
THE COMMAND PATTERN FOR APPLICATION OPERATIONS
The Command pattern is central to our architecture. Each application operation is wrapped in a command object that implements a standard interface. Commands encapsulate all information needed to perform an operation, undo it, and validate it.
The Command interface defines the contract that all commands must implement:
from abc import ABC, abstractmethod
class Command(ABC):
"""Abstract base class for all commands."""
@abstractmethod
def execute(self) -> bool:
"""
Execute the command.
Returns True if successful, False otherwise.
"""
pass
@abstractmethod
def undo(self) -> bool:
"""
Undo the effects of the command.
Returns True if successful, False otherwise.
"""
pass
@abstractmethod
def validate(self) -> bool:
"""
Validate that the command can be executed.
Returns True if validation passes, False otherwise.
"""
pass
@abstractmethod
def get_description(self) -> str:
"""Get a human-readable description of the command."""
pass
@abstractmethod
def get_required_authorization(self) -> AuthorizationLevel:
"""Get the authorization level required to execute this command."""
pass
Each application operation gets a corresponding command class. For example, creating a document is wrapped in a CreateDocumentCommand:
class CreateDocumentCommand(Command):
"""Command to create a new document in the application."""
def __init__(self, app: DocumentManagementSystem, title: str,
content: str, tags: List[str] = None):
self.app = app
self.title = title
self.content = content
self.tags = tags or []
self.created_document: Optional[Document] = None
def execute(self) -> bool:
"""Execute the document creation."""
try:
self.created_document = self.app.create_document(
self.title,
self.content,
self.tags
)
return True
except Exception as e:
return False
def undo(self) -> bool:
"""Undo by deleting the created document."""
if not self.created_document:
return False
return self.app.delete_document(self.created_document.document_id)
def validate(self) -> bool:
"""Validate that we have required information."""
return bool(self.title and self.content)
def get_description(self) -> str:
return f"Create document: {self.title}"
def get_required_authorization(self) -> AuthorizationLevel:
return AuthorizationLevel.USER
This pattern provides several benefits:
Undo/Redo Support: Commands store the information needed to reverse their effects. The undo method can restore previous state.
Validation: Commands can validate inputs before execution, preventing invalid operations from being attempted.
Authorization: Each command specifies its required authorization level, enabling centralized security enforcement.
Logging and Auditing: Commands provide descriptions that can be logged for audit trails.
Transactionality: Multiple commands can be grouped into composite commands that execute as a unit.
COMMAND PROCESSOR - COORDINATING EXECUTION
The Command Processor coordinates command execution and enforces cross-cutting concerns like authorization and undo/redo management. It sits between the script interface and the commands, providing a controlled execution environment.
class CommandProcessor:
"""
Processes commands and manages undo/redo stacks.
Coordinates command execution and enforces authorization.
"""
def __init__(self, auth_context: AuthorizationContext,
max_stack_size: int = 100):
self.undo_stack: List[Command] = []
self.redo_stack: List[Command] = []
self.auth_context = auth_context
self.max_stack_size = max_stack_size
def execute_command(self, command: Command) -> bool:
"""Execute a command with authorization checking."""
# Check authorization
if not self._check_authorization(command):
return False
# Validate the command
if not command.validate():
return False
# Execute the command
success = command.execute()
if success and command.is_undoable():
# Add to undo stack
self.undo_stack.append(command)
# Limit stack size
if len(self.undo_stack) > self.max_stack_size:
self.undo_stack.pop(0)
# Clear redo stack
self.redo_stack.clear()
return success
def undo(self) -> bool:
"""Undo the most recently executed command."""
if not self.undo_stack:
return False
command = self.undo_stack.pop()
success = command.undo()
if success:
self.redo_stack.append(command)
else:
self.undo_stack.append(command)
return success
def redo(self) -> bool:
"""Redo the most recently undone command."""
if not self.redo_stack:
return False
command = self.redo_stack.pop()
success = command.execute()
if success:
self.undo_stack.append(command)
else:
self.redo_stack.append(command)
return success
The Command Processor provides several critical services:
Authorization Enforcement: Before executing any command, the processor checks whether the current user has sufficient authorization. This ensures that scripts cannot bypass security restrictions.
Undo/Redo Management: The processor maintains stacks of executed and undone commands, enabling users to reverse script actions.
Validation: Commands are validated before execution, preventing invalid operations from being attempted.
Stack Size Limits: The processor limits the size of undo/redo stacks to prevent memory exhaustion.
AUTHORIZATION AND SECURITY
Security is paramount when exposing application functionality to scripts. The authorization system uses hierarchical levels where higher levels include all permissions of lower levels:
class AuthorizationLevel(Enum):
"""Authorization levels for command execution."""
GUEST = 0
USER = 10
POWER_USER = 20
ADMINISTRATOR = 30
SYSTEM = 40
def is_sufficient_for(self, required: 'AuthorizationLevel') -> bool:
"""Check if this level is sufficient for a required level."""
return self.value >= required.value
class AuthorizationContext:
"""Represents the current authorization context."""
def __init__(self, user_id: str, level: AuthorizationLevel):
self.user_id = user_id
self.level = level
def get_authorization_level(self) -> AuthorizationLevel:
return self.level
Each command specifies its required authorization level through the get_required_authorization() method. The Command Processor checks this before execution:
def _check_authorization(self, command: Command) -> bool:
"""Check if current authorization allows executing a command."""
required = command.get_required_authorization()
current = self.auth_context.get_authorization_level()
return current.is_sufficient_for(required)
This approach provides several security benefits:
Centralized Enforcement: Authorization is checked in one place (the Command Processor), making it impossible to bypass.
Declarative Security: Each command declares its requirements, making security policies explicit and auditable.
Hierarchical Permissions: The level system makes it easy to grant broad permissions without listing every individual operation.
Context Awareness: The authorization context can include additional information like user roles, organizational units, or time-based restrictions.
THE SCRIPT INTERFACE LAYER
The Script Interface Layer provides the bridge between scripts and commands. It exposes built-in functions that scripts can call, handles type conversion, and creates appropriate command objects.
Scripts use a simple, high-level API while the interface layer handles all the complexity of command creation, type conversion, and error handling.
class ApplicationScriptInterface:
"""
Provides script-accessible interface to application functionality.
All methods return RuntimeValue objects for use in scripts.
"""
def __init__(self, app: DocumentManagementSystem,
command_processor: CommandProcessor):
self.app = app
self.command_processor = command_processor
def create_document(self, *args) -> RuntimeValue:
"""
Create a new document.
Args:
args[0]: Title (RuntimeValue)
args[1]: Content (RuntimeValue)
args[2]: Tags (optional, RuntimeValue)
Returns:
RuntimeValue containing document ID
"""
if len(args) < 2:
raise RuntimeException(
"create_document requires at least 2 arguments: title, content"
)
# Convert script types to application types
title = str(args[0].value)
content = str(args[1].value)
tags = []
if len(args) >= 3:
tags_str = str(args[2].value)
tags = [tag.strip() for tag in tags_str.split(',')]
# Create and execute command
cmd = CreateDocumentCommand(self.app, title, content, tags)
success = self.command_processor.execute_command(cmd)
if success:
# Convert result to script type
return RuntimeValue(cmd.get_document_id(), ValueType.STRING)
else:
raise RuntimeException("Failed to create document")
def get_document(self, *args) -> RuntimeValue:
"""
Get document information.
Args:
args[0]: Document ID (RuntimeValue)
Returns:
RuntimeValue struct containing document information
"""
if len(args) != 1:
raise RuntimeException(
"get_document requires 1 argument: document_id"
)
document_id = str(args[0].value)
doc = self.app.get_document(document_id)
if not doc:
raise RuntimeException(f"Document not found: {document_id}")
# Convert document to script struct
doc_struct = {
'id': RuntimeValue(doc.document_id, ValueType.STRING),
'title': RuntimeValue(doc.title, ValueType.STRING),
'content': RuntimeValue(doc.content, ValueType.STRING),
'status': RuntimeValue(doc.status.name, ValueType.STRING),
'version': RuntimeValue(doc.version, ValueType.NUMBER),
}
return RuntimeValue(doc_struct, ValueType.STRUCT)
The interface layer performs several critical functions:
Type Conversion: Scripts use dynamic types (RuntimeValue objects) while the application uses static types. The interface converts between these representations.
Parameter Validation: The interface validates that scripts provide the correct number and types of arguments.
Command Creation: The interface creates appropriate command objects based on script calls.
Error Handling: The interface catches application exceptions and converts them to script-friendly error messages.
Result Formatting: Application results are converted to script-compatible types before being returned.
REGISTERING INTERFACE FUNCTIONS WITH THE RUNTIME
For scripts to call interface functions, they must be registered with the scripting runtime environment. This registration makes the functions available as built-in functions in the scripting language:
def register_with_runtime(self, runtime_env: RuntimeEnvironment):
"""Register all interface functions with the runtime environment."""
# Document operations
runtime_env.functions['create_document'] = {
'type': 'builtin',
'implementation': self.create_document
}
runtime_env.functions['get_document'] = {
'type': 'builtin',
'implementation': self.get_document
}
runtime_env.functions['update_document'] = {
'type': 'builtin',
'implementation': self.update_document
}
runtime_env.functions['search_documents'] = {
'type': 'builtin',
'implementation': self.search_documents
}
# User operations
runtime_env.functions['create_user'] = {
'type': 'builtin',
'implementation': self.create_user
}
runtime_env.functions['get_user'] = {
'type': 'builtin',
'implementation': self.get_user
}
# Workflow operations
runtime_env.functions['create_workflow_task'] = {
'type': 'builtin',
'implementation': self.create_workflow_task
}
# Statistics and reporting
runtime_env.functions['get_statistics'] = {
'type': 'builtin',
'implementation': self.get_statistics
}
Once registered, these functions become part of the scripting language and can be called naturally from scripts.
EXAMPLE SCRIPTS USING THE INTERFACE
With the interface layer in place, scripts can access application functionality through simple function calls. Here are examples demonstrating various use cases:
Example 1: Automated Document Creation
# Create multiple documents automatically
var doc_count = 0
print("Creating documents...")
# Create Project Proposal
var doc1_id = create_document("Project Proposal",
"This is the project proposal document.",
"proposal,project")
print("Created:", doc1_id)
doc_count = doc_count + 1
# Create Technical Specification
var doc2_id = create_document("Technical Specification",
"This document contains technical specifications.",
"technical,specification")
print("Created:", doc2_id)
doc_count = doc_count + 1
# Create User Manual
var doc3_id = create_document("User Manual",
"This is the user manual for the system.",
"manual,documentation")
print("Created:", doc3_id)
doc_count = doc_count + 1
print("Total documents created:", doc_count)
This script demonstrates basic document creation. The create_document function is a built-in function provided by the interface layer. It accepts title, content, and tags, creates a CreateDocumentCommand, executes it through the Command Processor, and returns the document ID.
Example 2: Document Workflow Automation
# Automate document workflow
print("Starting document workflow automation...")
# Create a document
var workflow_doc = create_document("Workflow Test Document",
"This document will go through the workflow.",
"workflow,test")
print("Created workflow document:", workflow_doc)
# Change status to pending review
var status_changed = change_document_status(workflow_doc, "PENDING_REVIEW")
print("Status changed to PENDING_REVIEW:", status_changed)
# Get current user
var current_user = get_current_user()
print("Current user:", current_user)
# Create a review task
var task_id = create_workflow_task(workflow_doc, "review", current_user)
print("Created review task:", task_id)
# Complete the task
var task_completed = complete_workflow_task(task_id)
print("Task completed:", task_completed)
# Approve and publish
var approved = change_document_status(workflow_doc, "APPROVED")
print("Document approved:", approved)
var published = change_document_status(workflow_doc, "PUBLISHED")
print("Document published:", published)
print("Workflow automation completed successfully!")
This script demonstrates workflow automation. It creates a document, changes its status through various workflow states, creates tasks, and completes them. Each operation is a separate command that can be undone if needed.
Example 3: Reporting and Statistics
# Generate system statistics report
print("=== SYSTEM STATISTICS REPORT ===")
# Get overall statistics
var stats = get_statistics()
print("Total Documents:", stats.total_documents)
print("Documents Published:", stats.documents_published)
print("Active Workflows:", stats.active_workflows)
# Get document counts by status
var status_counts = get_documents_by_status()
print("Documents by Status:")
print(" DRAFT:", status_counts.DRAFT)
print(" PENDING_REVIEW:", status_counts.PENDING_REVIEW)
print(" APPROVED:", status_counts.APPROVED)
print(" PUBLISHED:", status_counts.PUBLISHED)
# Get current user's document count
var current_user = get_current_user()
var user_doc_count = get_user_document_count(current_user)
print("Documents created by current user:", user_doc_count)
print("=== END OF REPORT ===")
This script demonstrates querying application state. The get_statistics and get_documents_by_status functions return struct objects that scripts can access using dot notation. These are read-only operations that don't create commands.
Example 4: Batch Processing
# Batch process documents
print("Starting batch document processing...")
var batch_size = 5
var i = 1
while i <= batch_size do
var title = concat("Batch Document ", to_string(i))
var content = concat("This is batch document number ", to_string(i))
var doc_id = create_document(title, content, "batch,automated")
print("Created:", doc_id)
i = i + 1
endwhile
print("Created", batch_size, "documents in batch")
# Search for batch documents
var batch_docs = search_documents("Batch Document")
print("Found batch documents:", batch_docs)
This script demonstrates batch operations using loops. It creates multiple documents programmatically, showing how scripts can automate repetitive tasks that would be tedious through a graphical interface.
Example 5: Conditional Processing
# Process documents based on conditions
print("Processing documents with conditional logic...")
# Create a document
var doc_id = create_document("Conditional Test",
"Testing conditional processing",
"test,conditional")
# Get document information
var doc = get_document(doc_id)
print("Document status:", doc.status)
# Conditional processing based on status
if doc.status == "DRAFT" then
print("Document is in DRAFT status")
print("Moving to PENDING_REVIEW...")
var changed = change_document_status(doc_id, "PENDING_REVIEW")
if changed then
print("Status changed successfully")
else
print("Failed to change status")
endif
else
print("Document is not in DRAFT status")
endif
# Get updated document
var updated_doc = get_document(doc_id)
print("Updated status:", updated_doc.status)
This script demonstrates conditional logic based on application state. Scripts can query document properties and make decisions based on those properties, enabling sophisticated automation workflows.
DESIGN PATTERNS FOR DIFFERENT OPERATION TYPES
Different types of operations require different approaches in the command layer. Understanding these patterns helps in designing a comprehensive scripting interface.
Pattern 1: Create Operations
Create operations add new entities to the application. They must:
- Store enough information to delete the created entity for undo
- Return an identifier for the created entity
- Validate that required information is provided
class CreateDocumentCommand(Command):
def __init__(self, app, title, content, tags):
self.app = app
self.title = title
self.content = content
self.tags = tags
self.created_document = None
def execute(self):
self.created_document = self.app.create_document(
self.title, self.content, self.tags
)
return True
def undo(self):
return self.app.delete_document(
self.created_document.document_id
)
def get_document_id(self):
return self.created_document.document_id
Pattern 2: Update Operations
Update operations modify existing entities. They must:
- Store the previous state for undo
- Validate that the entity exists
- Handle partial updates (some fields may not change)
class UpdateDocumentCommand(Command):
def __init__(self, app, document_id, title=None, content=None):
self.app = app
self.document_id = document_id
self.new_title = title
self.new_content = content
self.old_version = None
def execute(self):
# Store old version for undo
doc = self.app.get_document(self.document_id)
self.old_version = copy.deepcopy(doc)
# Perform update
return self.app.update_document(
self.document_id,
self.new_title,
self.new_content
)
def undo(self):
# Restore old version
self.app.documents[self.document_id] = self.old_version
return True
Pattern 3: Delete Operations
Delete operations remove entities. They must:
- Store the deleted entity for undo
- Handle cascading deletes of related entities
- Validate that the entity exists before deletion
class DeleteDocumentCommand(Command):
def __init__(self, app, document_id):
self.app = app
self.document_id = document_id
self.deleted_document = None
def execute(self):
# Store document for undo
self.deleted_document = self.app.get_document(self.document_id)
# Perform deletion
return self.app.delete_document(self.document_id)
def undo(self):
# Restore deleted document
self.app.documents[self.document_id] = self.deleted_document
return True
Pattern 4: State Change Operations
State change operations modify the state of entities. They must:
- Store the previous state for undo
- Validate state transitions (not all transitions may be valid)
- Trigger side effects (notifications, workflow actions, etc.)
class ChangeDocumentStatusCommand(Command):
def __init__(self, app, document_id, new_status):
self.app = app
self.document_id = document_id
self.new_status = new_status
self.old_status = None
def execute(self):
doc = self.app.get_document(self.document_id)
self.old_status = doc.status
return self.app.change_document_status(
self.document_id,
self.new_status
)
def undo(self):
return self.app.change_document_status(
self.document_id,
self.old_status
)
def get_required_authorization(self):
# Different statuses require different authorization
if self.new_status in [DocumentStatus.APPROVED,
DocumentStatus.PUBLISHED]:
return AuthorizationLevel.ADMINISTRATOR
return AuthorizationLevel.POWER_USER
Pattern 5: Query Operations
Query operations retrieve information without modifying state. They:
- Don't need undo support (they don't change anything)
- Don't go through the command processor
- Are called directly by the interface layer
def get_document(self, *args) -> RuntimeValue:
"""Query operation - no command needed."""
document_id = str(args[0].value)
doc = self.app.get_document(document_id)
if not doc:
raise RuntimeException(f"Document not found: {document_id}")
# Convert to script type and return
return self._convert_document_to_struct(doc)
Pattern 6: Composite Operations
Composite operations execute multiple sub-operations as a unit. They:
- Create and execute multiple commands
- Implement all-or-nothing semantics (undo all if any fails)
- Provide a single undo operation for the entire group
class PublishDocumentWorkflowCommand(Command):
"""Composite command for complete publish workflow."""
def __init__(self, app, document_id):
self.app = app
self.document_id = document_id
self.sub_commands = []
def execute(self):
# Create sub-commands
self.sub_commands = [
ChangeDocumentStatusCommand(
self.app, self.document_id, DocumentStatus.PENDING_REVIEW
),
CreateWorkflowTaskCommand(
self.app, self.document_id, "review", "reviewer_id"
),
# More sub-commands...
]
# Execute all sub-commands
for cmd in self.sub_commands:
if not cmd.execute():
# Rollback on failure
self._rollback()
return False
return True
def undo(self):
# Undo in reverse order
for cmd in reversed(self.sub_commands):
cmd.undo()
return True
def _rollback(self):
"""Rollback any executed sub-commands."""
for cmd in reversed(self.sub_commands):
if cmd.executed:
cmd.undo()
HANDLING COMPLEX DATA TYPES
Applications often work with complex data structures that need to be exposed to scripts. The interface layer must convert between application types and script types.
Structures and Objects
Application objects are converted to script structs (dictionaries of RuntimeValue objects):
def _convert_document_to_struct(self, doc: Document) -> RuntimeValue:
"""Convert Document object to script struct."""
doc_struct = {
'id': RuntimeValue(doc.document_id, ValueType.STRING),
'title': RuntimeValue(doc.title, ValueType.STRING),
'content': RuntimeValue(doc.content, ValueType.STRING),
'author_id': RuntimeValue(doc.author_id, ValueType.STRING),
'status': RuntimeValue(doc.status.name, ValueType.STRING),
'version': RuntimeValue(doc.version, ValueType.NUMBER),
'created_at': RuntimeValue(
doc.created_at.isoformat(),
ValueType.STRING
),
'tags': RuntimeValue(','.join(doc.tags), ValueType.STRING),
}
return RuntimeValue(doc_struct, ValueType.STRUCT)
Scripts can then access struct members using dot notation:
var doc = get_document(doc_id)
print("Title:", doc.title)
print("Status:", doc.status)
print("Version:", doc.version)
Collections
Collections are typically converted to comma-separated strings or arrays:
def search_documents(self, *args) -> RuntimeValue:
"""Return search results as comma-separated IDs."""
query = str(args[0].value)
results = self.app.search_documents(query=query)
# Convert to comma-separated string
doc_ids = ','.join([doc.document_id for doc in results])
return RuntimeValue(doc_ids, ValueType.STRING)
For more complex scenarios, you might return an array type if your scripting language supports it.
Enumerations
Enumerations are converted to strings:
# In the interface
'status': RuntimeValue(doc.status.name, ValueType.STRING)
# In scripts
if doc.status == "DRAFT" then
# Do something
endif
Dates and Times
Dates are typically converted to ISO format strings:
'created_at': RuntimeValue(doc.created_at.isoformat(), ValueType.STRING)
Scripts can then use string comparison or parsing functions to work with dates.
ERROR HANDLING AND VALIDATION
Proper error handling is critical for a good scripting experience. Scripts need clear, actionable error messages when operations fail.
Validation Errors
Validation errors occur when scripts provide invalid arguments:
def create_document(self, *args) -> RuntimeValue:
# Check argument count
if len(args) < 2:
raise RuntimeException(
"create_document requires at least 2 arguments: title, content"
)
# Validate argument types
title = str(args[0].value)
if not title or len(title) == 0:
raise RuntimeException(
"Document title cannot be empty"
)
content = str(args[1].value)
if not content or len(content) == 0:
raise RuntimeException(
"Document content cannot be empty"
)
Authorization Errors
Authorization errors occur when scripts attempt operations they don't have permission for:
def execute_command(self, command: Command) -> bool:
# Check authorization
if not self._check_authorization(command):
raise AuthorizationException(
f"Insufficient authorization for {command.get_description()}"
)
Application Errors
Application errors occur when operations fail due to business rule violations or system issues:
def change_document_status(self, *args) -> RuntimeValue:
document_id = str(args[0].value)
status_str = str(args[1].value).upper()
try:
new_status = DocumentStatus[status_str]
except KeyError:
raise RuntimeException(
f"Invalid status: {status_str}. "
f"Valid statuses are: DRAFT, PENDING_REVIEW, APPROVED, PUBLISHED, ARCHIVED"
)
cmd = ChangeDocumentStatusCommand(self.app, document_id, new_status)
success = self.command_processor.execute_command(cmd)
if not success:
raise RuntimeException(
f"Failed to change document status. "
f"The status transition may not be allowed."
)
VERSIONING AND BACKWARD COMPATIBILITY
As your application evolves, the scripting interface must evolve with it. However, existing scripts must continue to work. Several strategies help maintain backward compatibility:
Version Namespacing
Provide different versions of functions:
# Version 1
runtime_env.functions['create_document'] = {
'type': 'builtin',
'implementation': self.create_document_v1
}
# Version 2 with additional parameters
runtime_env.functions['create_document_v2'] = {
'type': 'builtin',
'implementation': self.create_document_v2
}
Optional Parameters
Use optional parameters for new functionality:
def create_document(self, *args) -> RuntimeValue:
# Required parameters
title = str(args[0].value)
content = str(args[1].value)
# Optional parameters (maintain backward compatibility)
tags = []
if len(args) >= 3:
tags_str = str(args[2].value)
tags = [tag.strip() for tag in tags_str.split(',')]
metadata = {}
if len(args) >= 4:
# New parameter added in version 2
metadata_str = str(args[3].value)
metadata = self._parse_metadata(metadata_str)
Deprecation Warnings
Warn users when they use deprecated functionality:
def old_function(self, *args) -> RuntimeValue:
print("WARNING: old_function is deprecated. Use new_function instead.")
# Still execute the operation for compatibility
return self.new_function(*args)
Interface Versioning
Provide completely separate interfaces for major versions:
class ApplicationScriptInterfaceV1:
"""Version 1 of the scripting interface."""
pass
class ApplicationScriptInterfaceV2:
"""Version 2 with breaking changes."""
pass
# Scripts specify which version they want
interface = ApplicationScriptInterfaceV2(app, command_processor)
PERFORMANCE CONSIDERATIONS
When exposing application functionality to scripts, performance becomes important since scripts may execute many operations in loops.
Command Pooling
Reuse command objects when possible:
class CommandPool:
"""Pool of reusable command objects."""
def __init__(self):
self.pools = {}
def get_command(self, command_class, *args):
"""Get a command from the pool or create new one."""
pool_key = command_class.__name__
if pool_key not in self.pools:
self.pools[pool_key] = []
pool = self.pools[pool_key]
if pool:
cmd = pool.pop()
cmd.reset(*args)
return cmd
else:
return command_class(*args)
def return_command(self, command):
"""Return a command to the pool."""
pool_key = command.__class__.__name__
self.pools[pool_key].append(command)
Batch Operations
Provide batch versions of operations:
def create_documents_batch(self, *args) -> RuntimeValue:
"""Create multiple documents in a single operation."""
# args[0] is array of document data
documents_data = args[0].value
created_ids = []
for doc_data in documents_data:
cmd = CreateDocumentCommand(
self.app,
doc_data['title'],
doc_data['content'],
doc_data.get('tags', [])
)
if self.command_processor.execute_command(cmd):
created_ids.append(cmd.get_document_id())
return RuntimeValue(','.join(created_ids), ValueType.STRING)
Lazy Loading
Don't load data until it's actually accessed:
class LazyDocument:
"""Lazy-loading wrapper for document data."""
def __init__(self, app, document_id):
self.app = app
self.document_id = document_id
self._document = None
@property
def document(self):
if self._document is None:
self._document = self.app.get_document(self.document_id)
return self._document
Caching
Cache frequently accessed data:
class CachedApplicationInterface:
"""Interface with caching for read operations."""
def __init__(self, app, command_processor):
self.app = app
self.command_processor = command_processor
self.document_cache = {}
self.cache_timeout = 60 # seconds
def get_document(self, *args) -> RuntimeValue:
document_id = str(args[0].value)
# Check cache
if document_id in self.document_cache:
cached_doc, timestamp = self.document_cache[document_id]
if time.time() - timestamp < self.cache_timeout:
return cached_doc
# Load from application
doc = self.app.get_document(document_id)
result = self._convert_document_to_struct(doc)
# Cache result
self.document_cache[document_id] = (result, time.time())
return result
TESTING THE SCRIPTING INTERFACE
Comprehensive testing ensures that the scripting interface works correctly and maintains backward compatibility.
Unit Tests for Commands
Test each command in isolation:
def test_create_document_command():
"""Test CreateDocumentCommand execution and undo."""
app = DocumentManagementSystem()
cmd = CreateDocumentCommand(
app,
"Test Document",
"Test content",
["test", "example"]
)
# Test execution
assert cmd.validate()
assert cmd.execute()
assert cmd.get_document_id() is not None
# Verify document was created
doc = app.get_document(cmd.get_document_id())
assert doc is not None
assert doc.title == "Test Document"
assert doc.content == "Test content"
assert "test" in doc.tags
# Test undo
assert cmd.undo()
assert app.get_document(cmd.get_document_id()) is None
Integration Tests for Interface Functions
Test interface functions with the full stack:
def test_create_document_interface():
"""Test create_document interface function."""
app = DocumentManagementSystem()
auth_context = AuthorizationContext("test", AuthorizationLevel.USER)
command_processor = CommandProcessor(auth_context)
interface = ApplicationScriptInterface(app, command_processor)
# Create RuntimeValue arguments
title = RuntimeValue("Test Doc", ValueType.STRING)
content = RuntimeValue("Test content", ValueType.STRING)
tags = RuntimeValue("test,example", ValueType.STRING)
# Call interface function
result = interface.create_document(title, content, tags)
# Verify result
assert result.value_type == ValueType.STRING
assert len(result.value) > 0
# Verify document was created
doc = app.get_document(result.value)
assert doc is not None
assert doc.title == "Test Doc"
End-to-End Script Tests
Test complete scripts:
def test_document_workflow_script():
"""Test complete workflow automation script."""
# Setup
app = DocumentManagementSystem()
auth_context = AuthorizationContext("admin", AuthorizationLevel.ADMINISTRATOR)
command_processor = CommandProcessor(auth_context)
runtime_env = RuntimeEnvironment(command_processor)
evaluator = Evaluator(runtime_env)
interface = ApplicationScriptInterface(app, command_processor)
interface.register_with_runtime(runtime_env)
# Script to test
script = """
var doc_id = create_document("Test", "Content", "test")
var changed = change_document_status(doc_id, "PENDING_REVIEW")
var doc = get_document(doc_id)
"""
# Execute script
lexer = Lexer(script)
tokens = lexer.tokenize()
parser = Parser(tokens)
ast = parser.parse()
success = evaluator.evaluate(ast)
assert success
# Verify results
doc_id = runtime_env.get_variable("doc_id").value
doc = app.get_document(doc_id)
assert doc.status == DocumentStatus.PENDING_REVIEW
Backward Compatibility Tests
Ensure old scripts still work:
def test_backward_compatibility():
"""Test that version 1 scripts still work with version 2 interface."""
# Version 1 script (without new parameters)
v1_script = """
var doc_id = create_document("Title", "Content")
"""
# Should still work with version 2 interface
# that added optional parameters
success = execute_script(v1_script)
assert success
DOCUMENTATION AND DISCOVERABILITY
Good documentation is essential for users to effectively use the scripting interface.
Function Documentation
Document each interface function with clear descriptions, parameters, return values, and examples:
def create_document(self, *args) -> RuntimeValue:
"""
Create a new document in the system.
Parameters:
title (string): Document title (required)
content (string): Document content (required)
tags (string): Comma-separated list of tags (optional)
Returns:
string: ID of the created document
Example:
var doc_id = create_document("My Document", "Content here", "tag1,tag2")
print("Created document:", doc_id)
Raises:
RuntimeException: If title or content is empty
AuthorizationException: If user lacks USER authorization
"""
Auto-Generated Documentation
Generate documentation from code:
class DocumentationGenerator:
"""Generates documentation for script interface functions."""
def generate_function_docs(self, interface):
"""Generate documentation for all interface functions."""
docs = []
for func_name, func_info in interface.registered_functions.items():
func = func_info['implementation']
doc = {
'name': func_name,
'description': func.__doc__,
'signature': self._extract_signature(func),
'examples': self._extract_examples(func.__doc__)
}
docs.append(doc)
return docs
Interactive Help
Provide help functions in scripts:
def help_function(self, *args) -> RuntimeValue:
"""
Get help for a function.
Usage:
help("function_name")
"""
if len(args) == 0:
# List all available functions
functions = list(self.registered_functions.keys())
return RuntimeValue('\n'.join(functions), ValueType.STRING)
func_name = str(args[0].value)
if func_name in self.registered_functions:
func = self.registered_functions[func_name]['implementation']
return RuntimeValue(func.__doc__, ValueType.STRING)
else:
return RuntimeValue(f"Function '{func_name}' not found", ValueType.STRING)
Scripts can then use:
# List all functions
help()
# Get help for specific function
help("create_document")
SECURITY BEST PRACTICES
Security must be considered at every layer of the architecture.
Principle of Least Privilege
Grant only the minimum necessary permissions:
# Different authorization levels for different operations
class CreateDocumentCommand(Command):
def get_required_authorization(self):
return AuthorizationLevel.USER
class DeleteDocumentCommand(Command):
def get_required_authorization(self):
return AuthorizationLevel.POWER_USER
class ChangeSystemSettingsCommand(Command):
def get_required_authorization(self):
return AuthorizationLevel.ADMINISTRATOR
Input Validation
Validate all inputs at the interface layer:
def create_document(self, *args) -> RuntimeValue:
title = str(args[0].value)
# Validate length
if len(title) > 1000:
raise RuntimeException("Title exceeds maximum length of 1000 characters")
# Validate characters
if not self._is_valid_title(title):
raise RuntimeException("Title contains invalid characters")
# Validate against injection attacks
if self._contains_sql_injection(title):
raise RuntimeException("Title contains potentially dangerous content")
Audit Logging
Log all script operations for security auditing:
class AuditLogger:
"""Logs all security-relevant operations."""
def log_command_execution(self, user_id, command, success):
"""Log command execution."""
entry = {
'timestamp': datetime.now(),
'user_id': user_id,
'command': command.get_description(),
'success': success,
'authorization_level': command.get_required_authorization()
}
self._write_to_audit_log(entry)
Resource Limits
Prevent resource exhaustion:
class ResourceLimiter:
"""Enforces resource usage limits."""
def __init__(self):
self.max_execution_time = 300 # seconds
self.max_commands_per_script = 10000
self.max_memory_usage = 100 * 1024 * 1024 # 100 MB
def check_limits(self, execution_context):
"""Check if resource limits are exceeded."""
if execution_context.execution_time > self.max_execution_time:
raise RuntimeException("Script execution time limit exceeded")
if execution_context.command_count > self.max_commands_per_script:
raise RuntimeException("Script command limit exceeded")
Sandboxing
Restrict what scripts can access:
class Sandbox:
"""Provides sandboxing for script execution."""
def __init__(self):
self.allowed_functions = set()
self.blocked_functions = {'delete_all_documents', 'drop_database'}
def is_function_allowed(self, func_name):
"""Check if a function is allowed in sandbox."""
if func_name in self.blocked_functions:
return False
if self.allowed_functions and func_name not in self.allowed_functions:
return False
return True
ADVANCED TOPICS
Event-Driven Script Execution
Scripts can respond to application events:
class EventDrivenScriptHandler:
"""Executes scripts in response to application events."""
def __init__(self, event_system, script_manager, runtime_env):
self.event_system = event_system
self.script_manager = script_manager
self.runtime_env = runtime_env
def register_script_for_event(self, event_type, script_id):
"""Register a script to execute when event occurs."""
def handler(event):
script = self.script_manager.get_script(script_id)
if script and script.metadata.enabled:
self._execute_script_with_event_data(script, event)
self.event_system.subscribe(event_type, handler)
def _execute_script_with_event_data(self, script, event):
"""Execute script with event data available."""
# Set event data as variables
self.runtime_env.set_variable(
'event_type',
RuntimeValue(event.event_type, ValueType.STRING)
)
for key, value in event.data.items():
self.runtime_env.set_variable(
f'event_{key}',
self._convert_to_runtime_value(value)
)
# Execute script
self._execute_script(script)
Scheduled Script Execution
Scripts can be scheduled to run periodically:
class ScriptScheduler:
"""Schedules scripts for periodic execution."""
def __init__(self, script_manager, runtime_env):
self.script_manager = script_manager
self.runtime_env = runtime_env
self.scheduled_scripts = {}
def schedule_script(self, script_id, cron_expression):
"""Schedule a script using cron expression."""
self.scheduled_scripts[script_id] = {
'cron': cron_expression,
'next_run': self._calculate_next_run(cron_expression)
}
def run_scheduled_scripts(self):
"""Run any scripts that are due."""
now = datetime.now()
for script_id, schedule in self.scheduled_scripts.items():
if now >= schedule['next_run']:
script = self.script_manager.get_script(script_id)
if script:
self._execute_script(script)
schedule['next_run'] = self._calculate_next_run(
schedule['cron']
)
Script Debugging Support
Provide debugging capabilities:
class ScriptDebugger:
"""Provides debugging support for scripts."""
def __init__(self, runtime_env):
self.runtime_env = runtime_env
self.breakpoints = set()
self.watch_variables = set()
def set_breakpoint(self, line_number):
"""Set a breakpoint at a line number."""
self.breakpoints.add(line_number)
def add_watch(self, variable_name):
"""Watch a variable for changes."""
self.watch_variables.add(variable_name)
def on_line_executed(self, line_number):
"""Called when a line is executed."""
if line_number in self.breakpoints:
self._pause_execution()
self._show_debug_info()
def _show_debug_info(self):
"""Show current state for debugging."""
print("=== Debug Info ===")
print(f"Call stack depth: {len(self.runtime_env.call_stack)}")
for var_name in self.watch_variables:
value = self.runtime_env.get_variable(var_name)
print(f"{var_name} = {value}")
COMPLETE WORKING EXAMPLE
Let's demonstrate the complete system with a realistic scenario:
def main():
"""Complete demonstration of script access to application functionality."""
# Initialize the application
app = DocumentManagementSystem()
# Initialize scripting system
auth_context = AuthorizationContext("admin", AuthorizationLevel.ADMINISTRATOR)
command_processor = CommandProcessor(auth_context)
runtime_env = RuntimeEnvironment(command_processor)
evaluator = Evaluator(runtime_env)
# Create and register application interface
app_interface = ApplicationScriptInterface(app, command_processor)
app_interface.register_with_runtime(runtime_env)
print("System initialized\n")
# Example 1: Document Creation and Workflow
print("=" * 60)
print("Example 1: Document Creation and Workflow Automation")
print("=" * 60)
workflow_script = """
# Create a new document
var doc_id = create_document(
"Quarterly Report",
"This is the Q4 2024 quarterly report.",
"report,quarterly,2024"
)
print("Created document:", doc_id)
# Get document details
var doc = get_document(doc_id)
print("Document title:", doc.title)
print("Document status:", doc.status)
# Move through workflow
var changed = change_document_status(doc_id, "PENDING_REVIEW")
print("Changed to PENDING_REVIEW:", changed)
# Create review task
var current_user = get_current_user()
var task_id = create_workflow_task(doc_id, "review", current_user)
print("Created review task:", task_id)
# Complete review and approve
var completed = complete_workflow_task(task_id)
var approved = change_document_status(doc_id, "APPROVED")
var published = change_document_status(doc_id, "PUBLISHED")
print("Document published successfully!")
"""
execute_script(workflow_script, runtime_env, evaluator)
# Example 2: Batch Processing
print("\n" + "=" * 60)
print("Example 2: Batch Document Processing")
print("=" * 60)
batch_script = """
# Create multiple documents in a batch
print("Creating batch documents...")
var count = 0
var i = 1
while i <= 5 do
var title = concat("Document ", to_string(i))
var content = concat("Content for document ", to_string(i))
var doc_id = create_document(title, content, "batch,automated")
print(" Created:", title)
count = count + 1
i = i + 1
endwhile
print("Created", count, "documents")
# Search for batch documents
var results = search_documents("Document")
print("Search found:", results)
"""
execute_script(batch_script, runtime_env, evaluator)
# Example 3: Reporting
print("\n" + "=" * 60)
print("Example 3: System Statistics Report")
print("=" * 60)
report_script = """
print("=== SYSTEM STATISTICS ===")
var stats = get_statistics()
print("Total Documents:", stats.total_documents)
print("Documents Published:", stats.documents_published)
print("Active Workflows:", stats.active_workflows)
var status_counts = get_documents_by_status()
print("\nDocuments by Status:")
print(" DRAFT:", status_counts.DRAFT)
print(" PENDING_REVIEW:", status_counts.PENDING_REVIEW)
print(" APPROVED:", status_counts.APPROVED)
print(" PUBLISHED:", status_counts.PUBLISHED)
print("\n=== END REPORT ===")
"""
execute_script(report_script, runtime_env, evaluator)
# Demonstrate undo/redo
print("\n" + "=" * 60)
print("Demonstrating Undo/Redo")
print("=" * 60)
print(f"Can undo: {command_processor.can_undo()}")
if command_processor.can_undo():
print(f"Last operation: {command_processor.get_undo_description()}")
command_processor.undo()
print("Operation undone")
command_processor.redo()
print("Operation redone")
print("\nDemonstration complete!")
def execute_script(script_code, runtime_env, evaluator):
"""Execute a script with error handling."""
try:
from lexer import Lexer
from parser import Parser
from semantic_analyzer import SemanticAnalyzer
lexer = Lexer(script_code)
tokens = lexer.tokenize()
parser = Parser(tokens)
ast = parser.parse()
analyzer = SemanticAnalyzer()
if not analyzer.analyze(ast):
print("Semantic errors:")
for error in analyzer.get_errors():
print(f" {error}")
return
evaluator.evaluate(ast)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
CONCLUSION
Exposing application functionality to scripting systems requires careful architectural design that balances power, security, and maintainability. The layered architecture presented in this article provides a proven approach that:
Maintains Encapsulation: Application internals remain hidden behind well-defined interfaces. Scripts interact with commands and interface functions, not directly with application code.
Enforces Security: Authorization is checked centrally before any operation executes. Commands declare their requirements, and the Command Processor enforces them consistently.
Supports Undo/Redo: The Command pattern naturally supports reversible operations, giving users confidence to experiment with scripts.
Enables Evolution: The interface layer can evolve independently of the application layer. New functionality can be added without breaking existing scripts.
Provides Type Safety: Conversion between script types and application types happens in one place, ensuring consistency and preventing type-related errors.
Facilitates Testing: Each layer can be tested independently. Commands can be unit tested, interface functions can be integration tested, and complete scripts can be end-to-end tested.
This architecture is applicable to any application domain - CAD systems, financial applications, content management systems, scientific software, and more. The key is to identify your application's operations, wrap them in commands, provide a script-friendly interface, and coordinate execution through a command processor.
By following these patterns and principles, you can create a powerful, secure, and maintainable scripting system that enhances your application's value and enables users to automate their workflows effectively.