quantum-framework 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
quantum/core/parser.py
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Parser - Convert .q files (XML) to Quantum AST
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from xml.etree import ElementTree as ET
|
|
9
|
+
from typing import Union, Optional, List
|
|
10
|
+
|
|
11
|
+
# Fix imports
|
|
12
|
+
|
|
13
|
+
from quantum.core.html_compat import normalise_html
|
|
14
|
+
from quantum.core.ast_nodes import (
|
|
15
|
+
QuantumNode, ComponentNode, ApplicationNode, JobNode,
|
|
16
|
+
QuantumParam, QuantumReturn, QuantumRoute,
|
|
17
|
+
DispatchEventNode, OnEventNode, QueryNode, QueryParamNode,
|
|
18
|
+
InvokeHeaderNode, DataNode, ColumnNode, FieldNode, TransformNode,
|
|
19
|
+
FilterNode, SortNode, LimitNode, ComputeNode, HeaderNode,
|
|
20
|
+
HTMLNode, TextNode, DocTypeNode, CommentNode, HTML_VOID_ELEMENTS,
|
|
21
|
+
ImportNode, SlotNode, ComponentCallNode,
|
|
22
|
+
ActionNode, RedirectNode, FlashNode, FileNode, MailNode, TransactionNode,
|
|
23
|
+
LLMNode, LLMMessageNode,
|
|
24
|
+
ScheduleNode, ThreadNode, # Job Execution System
|
|
25
|
+
# Message Queue System
|
|
26
|
+
MessageNode, MessageHeaderNode, SubscribeNode, QueueNode,
|
|
27
|
+
MessageAckNode, MessageNackNode,
|
|
28
|
+
# Python Scripting System
|
|
29
|
+
PythonNode, PyImportNode, PyClassNode, PyDecoratorNode, PyExprNode
|
|
30
|
+
)
|
|
31
|
+
# Import feature AST nodes from their respective modules
|
|
32
|
+
from quantum.core.features.conditionals.src.ast_node import IfNode
|
|
33
|
+
from quantum.core.features.loops.src.ast_node import LoopNode
|
|
34
|
+
from quantum.core.features.state_management.src.ast_node import SetNode, PersistNode
|
|
35
|
+
from quantum.core.features.functions.src.ast_node import FunctionNode, RestConfig
|
|
36
|
+
from quantum.core.features.invocation.src.ast_node import InvokeNode
|
|
37
|
+
from quantum.core.features.logging.src import LogNode, parse_log
|
|
38
|
+
from quantum.core.features.dump.src import DumpNode, parse_dump
|
|
39
|
+
from quantum.core.features.knowledge_base.src import KnowledgeNode, KnowledgeSourceNode, parse_knowledge
|
|
40
|
+
from quantum.core.features.agents.src import (
|
|
41
|
+
AgentNode, AgentInstructionNode, AgentToolNode, AgentToolParamNode, AgentExecuteNode,
|
|
42
|
+
AgentTeamNode, AgentHandoffNode
|
|
43
|
+
)
|
|
44
|
+
from quantum.core.features.websocket.src import (
|
|
45
|
+
WebSocketNode, WebSocketHandlerNode, WebSocketSendNode, WebSocketCloseNode
|
|
46
|
+
)
|
|
47
|
+
from quantum.core.features.game_engine_2d.src.parser import GameParser, GameParseError
|
|
48
|
+
from quantum.core.features.game_engine_2d.src.ast_nodes import (
|
|
49
|
+
SceneNode, BehaviorNode, PrefabNode, PersistentNode, EnemyNode,
|
|
50
|
+
)
|
|
51
|
+
from quantum.core.features.terminal_engine.src.parser import TerminalParser, TerminalParseError
|
|
52
|
+
from quantum.core.features.terminal_engine.src.ast_nodes import (
|
|
53
|
+
ScreenNode as TerminalScreenNode, KeybindingNode as TerminalKeybindingNode,
|
|
54
|
+
ServiceNode as TerminalServiceNode, CssNode as TerminalCssNode,
|
|
55
|
+
)
|
|
56
|
+
from quantum.core.features.testing_engine.src.parser import TestingParser, TestingParseError
|
|
57
|
+
from quantum.core.features.testing_engine.src.ast_nodes import (
|
|
58
|
+
QTestSuiteNode as TestingTestSuiteNode,
|
|
59
|
+
BrowserConfigNode as TestingBrowserConfigNode,
|
|
60
|
+
FixtureNode_Testing as TestingFixtureNode,
|
|
61
|
+
MockNode_Testing as TestingMockNode,
|
|
62
|
+
AuthNode as TestingAuthNode,
|
|
63
|
+
)
|
|
64
|
+
from quantum.core.features.ui_engine.src.parser import UIParser, UIParseError
|
|
65
|
+
from quantum.core.features.ui_engine.src.ast_nodes import UIWindowNode
|
|
66
|
+
from quantum.core.features.theming.src import UIThemeNode
|
|
67
|
+
from quantum.core.parser_registry import ParserRegistry
|
|
68
|
+
import logging
|
|
69
|
+
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _create_parser_registry(parser: 'QuantumParser') -> ParserRegistry:
|
|
74
|
+
"""
|
|
75
|
+
Create and populate the parser registry with all modular parsers.
|
|
76
|
+
|
|
77
|
+
This factory function creates all modular parsers and registers them.
|
|
78
|
+
The registry can then dispatch parsing to the appropriate parser
|
|
79
|
+
based on tag name.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
parser: QuantumParser instance for parser initialization
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Populated ParserRegistry
|
|
86
|
+
"""
|
|
87
|
+
from quantum.core.parsers import (
|
|
88
|
+
# Control flow
|
|
89
|
+
IfParser, LoopParser, SetParser,
|
|
90
|
+
# Data
|
|
91
|
+
QueryParser, InvokeParser, DataParser, TransactionParser,
|
|
92
|
+
# Services
|
|
93
|
+
LogParser, DumpParser, FileParser, MailParser,
|
|
94
|
+
# AI
|
|
95
|
+
LLMParser, AgentParser, TeamParser, KnowledgeParser,
|
|
96
|
+
# Messaging
|
|
97
|
+
WebSocketParser, WebSocketSendParser, WebSocketCloseParser,
|
|
98
|
+
MessageParser, SubscribeParser, QueueParser,
|
|
99
|
+
# Jobs
|
|
100
|
+
ScheduleParser, ThreadParser, JobParser,
|
|
101
|
+
# Scripting
|
|
102
|
+
PythonParser, PyImportParser, PyClassParser,
|
|
103
|
+
# HTML
|
|
104
|
+
HTMLParser, ComponentCallParser,
|
|
105
|
+
)
|
|
106
|
+
# New modular parsers (Phase 2 - Operacao Guilhotina)
|
|
107
|
+
from quantum.core.parsers.forms import ActionParser, RedirectParser, FlashParser
|
|
108
|
+
from quantum.core.parsers.composition import ImportParser, SlotParser
|
|
109
|
+
from quantum.core.parsers.functions import FunctionParser, ReturnParser, ParamParser
|
|
110
|
+
from quantum.core.parsers.events import DispatchEventParser
|
|
111
|
+
from quantum.core.parsers.persistence import PersistParser
|
|
112
|
+
from quantum.core.parsers.routing import RouteParser
|
|
113
|
+
from quantum.core.parsers.messaging import MessageAckParser, MessageNackParser
|
|
114
|
+
from quantum.core.parsers.scripting import PyDecoratorParser
|
|
115
|
+
|
|
116
|
+
registry = ParserRegistry()
|
|
117
|
+
|
|
118
|
+
# Register all tag-based parsers
|
|
119
|
+
parsers = [
|
|
120
|
+
# Control flow
|
|
121
|
+
IfParser(parser),
|
|
122
|
+
LoopParser(parser),
|
|
123
|
+
SetParser(parser),
|
|
124
|
+
# Data
|
|
125
|
+
QueryParser(parser),
|
|
126
|
+
InvokeParser(parser),
|
|
127
|
+
DataParser(parser),
|
|
128
|
+
TransactionParser(parser),
|
|
129
|
+
# Services
|
|
130
|
+
LogParser(parser),
|
|
131
|
+
DumpParser(parser),
|
|
132
|
+
FileParser(parser),
|
|
133
|
+
MailParser(parser),
|
|
134
|
+
# AI
|
|
135
|
+
LLMParser(parser),
|
|
136
|
+
AgentParser(parser),
|
|
137
|
+
TeamParser(parser),
|
|
138
|
+
KnowledgeParser(parser),
|
|
139
|
+
# Messaging
|
|
140
|
+
WebSocketParser(parser),
|
|
141
|
+
WebSocketSendParser(parser),
|
|
142
|
+
WebSocketCloseParser(parser),
|
|
143
|
+
MessageParser(parser),
|
|
144
|
+
SubscribeParser(parser),
|
|
145
|
+
QueueParser(parser),
|
|
146
|
+
MessageAckParser(parser),
|
|
147
|
+
MessageNackParser(parser),
|
|
148
|
+
# Jobs
|
|
149
|
+
ScheduleParser(parser),
|
|
150
|
+
ThreadParser(parser),
|
|
151
|
+
JobParser(parser),
|
|
152
|
+
# Scripting
|
|
153
|
+
PythonParser(parser),
|
|
154
|
+
PyImportParser(parser),
|
|
155
|
+
PyClassParser(parser),
|
|
156
|
+
PyDecoratorParser(parser),
|
|
157
|
+
# HTML (registers for common HTML tags)
|
|
158
|
+
HTMLParser(parser),
|
|
159
|
+
# Forms & Actions
|
|
160
|
+
ActionParser(parser),
|
|
161
|
+
RedirectParser(parser),
|
|
162
|
+
FlashParser(parser),
|
|
163
|
+
# Component Composition
|
|
164
|
+
ImportParser(parser),
|
|
165
|
+
SlotParser(parser),
|
|
166
|
+
# Functions
|
|
167
|
+
FunctionParser(parser),
|
|
168
|
+
# Note: ReturnParser, ParamParser, RouteParser are not registered here
|
|
169
|
+
# They are internal parsers used by FunctionParser and other parsers
|
|
170
|
+
# Events
|
|
171
|
+
DispatchEventParser(parser),
|
|
172
|
+
# Persistence
|
|
173
|
+
PersistParser(parser),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
registry.register_all(parsers)
|
|
177
|
+
|
|
178
|
+
# Store special parsers for fallback handling
|
|
179
|
+
# These are used when no specific tag parser matches
|
|
180
|
+
registry.html_parser = HTMLParser(parser)
|
|
181
|
+
registry.component_call_parser = ComponentCallParser(parser)
|
|
182
|
+
|
|
183
|
+
logger.debug(f"Initialized parser registry with {registry.parser_count} parsers")
|
|
184
|
+
|
|
185
|
+
return registry
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class QuantumParseError(Exception):
|
|
189
|
+
"""Quantum parsing error"""
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _explain_xml_error(error, source: str) -> str:
|
|
194
|
+
"""Turn a raw ElementTree error into something actionable.
|
|
195
|
+
|
|
196
|
+
A .q file is XML, so a bare `<` or `&` inside SQL or JavaScript breaks
|
|
197
|
+
parsing with a message ("not well-formed (invalid token)") that says
|
|
198
|
+
nothing about the actual cause. This has bitten the same way at least
|
|
199
|
+
twice (a JS `for` loop in q:llm, a SQL `<` in q:schedule), so name it.
|
|
200
|
+
"""
|
|
201
|
+
message = f"XML parse error: {error}"
|
|
202
|
+
|
|
203
|
+
line_no = getattr(error, 'position', (None, None))[0]
|
|
204
|
+
offending = ''
|
|
205
|
+
if line_no:
|
|
206
|
+
lines = source.splitlines()
|
|
207
|
+
if 0 < line_no <= len(lines):
|
|
208
|
+
offending = lines[line_no - 1].strip()
|
|
209
|
+
|
|
210
|
+
if offending:
|
|
211
|
+
message += f"\n at line {line_no}: {offending}"
|
|
212
|
+
|
|
213
|
+
if offending and ('<' in offending or '&' in offending):
|
|
214
|
+
message += (
|
|
215
|
+
"\n Hint: a .q file is XML, so a bare '<' or '&' is invalid even "
|
|
216
|
+
"inside SQL or JavaScript.\n"
|
|
217
|
+
" Write '<' / '&', or wrap the body in <![CDATA[ ... ]]>."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return message
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class QuantumParser:
|
|
224
|
+
"""Main parser for .q files"""
|
|
225
|
+
|
|
226
|
+
def __init__(self, use_cache: bool = True, use_modular_parsers: bool = True):
|
|
227
|
+
self.quantum_ns = {'q': 'https://quantum.lang/ns'}
|
|
228
|
+
self._use_cache = use_cache
|
|
229
|
+
self._source_path: Optional[str] = None # Path of the file being parsed
|
|
230
|
+
self._ast_cache = None
|
|
231
|
+
if use_cache:
|
|
232
|
+
try:
|
|
233
|
+
from quantum.runtime.ast_cache import get_ast_cache
|
|
234
|
+
self._ast_cache = get_ast_cache()
|
|
235
|
+
except ImportError:
|
|
236
|
+
self._use_cache = False
|
|
237
|
+
|
|
238
|
+
# === NEW: Parser Registry for modular dispatch ===
|
|
239
|
+
self._use_modular_parsers = use_modular_parsers
|
|
240
|
+
self._parser_registry: ParserRegistry = None
|
|
241
|
+
if use_modular_parsers:
|
|
242
|
+
self._parser_registry = _create_parser_registry(self)
|
|
243
|
+
else:
|
|
244
|
+
import warnings
|
|
245
|
+
warnings.warn(
|
|
246
|
+
"use_modular_parsers=False is deprecated and will be removed in v2.0. "
|
|
247
|
+
"The modular ParserRegistry is now the default.",
|
|
248
|
+
DeprecationWarning,
|
|
249
|
+
stacklevel=2
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def parser_registry(self) -> ParserRegistry:
|
|
254
|
+
"""Access to parser registry (may be None if not using modular parsers)"""
|
|
255
|
+
return self._parser_registry
|
|
256
|
+
|
|
257
|
+
# Prefixo -> URI que o framework injeta sozinho.
|
|
258
|
+
_NAMESPACES = {
|
|
259
|
+
'q': 'https://quantum.lang/ns',
|
|
260
|
+
'qg': 'https://quantum.lang/game',
|
|
261
|
+
'qt': 'https://quantum.lang/terminal',
|
|
262
|
+
'qtest': 'https://quantum.lang/testing',
|
|
263
|
+
'ui': 'https://quantum.lang/ui',
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# O primeiro elemento do documento, seja ele qual for.
|
|
267
|
+
_FIRST_ELEMENT = re.compile(r'<([A-Za-z_][\w.:-]*)((?:\s[^<>]*?)?)(/?)>')
|
|
268
|
+
|
|
269
|
+
def _inject_namespace(self, content: str) -> str:
|
|
270
|
+
"""Declara os namespaces do framework que o autor nao declarou.
|
|
271
|
+
|
|
272
|
+
Isto permite escrever `<q:component name="Foo">` sem cerimonia, em
|
|
273
|
+
vez de `<q:component name="Foo" xmlns:q="https://quantum.lang/ns">`.
|
|
274
|
+
|
|
275
|
+
Duas coisas estavam erradas:
|
|
276
|
+
|
|
277
|
+
1. **A deteccao era por SUBSTRING.** `if 'xmlns:q' in content` casa
|
|
278
|
+
com `xmlns:qt=`, `xmlns:qg=` e `xmlns:qtest=`. Um arquivo que
|
|
279
|
+
declarava so o namespace da sua engine caia no ramo "ja tem
|
|
280
|
+
namespace", e la dentro toda injecao era um
|
|
281
|
+
`content.replace('xmlns:q="https://quantum.lang/ns"', ...)` que
|
|
282
|
+
nao achava nada. Resultado: NENHUM namespace era declarado e o
|
|
283
|
+
arquivo morria com "unbound prefix" — e a mensagem apontava para
|
|
284
|
+
a linha 1, coluna 0, que nao diz nada a ninguem.
|
|
285
|
+
|
|
286
|
+
2. **So tres raizes eram reconhecidas** (`q:component`,
|
|
287
|
+
`q:application`, `q:job`). Um arquivo cuja raiz e `<q:behavior>`
|
|
288
|
+
ou `<qg:scene>` — as cenas e comportamentos que o scene-include
|
|
289
|
+
carrega, 16 arquivos entregues no repositorio — nao casava com o
|
|
290
|
+
padrao e tambem nao recebia namespace nenhum.
|
|
291
|
+
|
|
292
|
+
Agora a deteccao e pelo prefixo EXATO (`xmlns:q=`) e a injecao vai no
|
|
293
|
+
primeiro elemento, qualquer que seja ele. Uma raiz que o framework
|
|
294
|
+
nao suporta passa a falhar em `_parse_root_element`, com o nome do
|
|
295
|
+
elemento na mensagem, em vez de "unbound prefix".
|
|
296
|
+
"""
|
|
297
|
+
faltando = {}
|
|
298
|
+
for prefixo, uri in self._NAMESPACES.items():
|
|
299
|
+
if f'xmlns:{prefixo}=' in content:
|
|
300
|
+
continue # o autor declarou; respeitar a escolha dele
|
|
301
|
+
# So o que o documento realmente usa.
|
|
302
|
+
if f'<{prefixo}:' in content or f'</{prefixo}:' in content:
|
|
303
|
+
faltando[prefixo] = uri
|
|
304
|
+
|
|
305
|
+
# `type="game"` implica qg: mesmo antes de qualquer tag qg: aparecer,
|
|
306
|
+
# e assim por diante — comportamento que ja existia.
|
|
307
|
+
for atributo, prefixo in (('type="game"', 'qg'),
|
|
308
|
+
('type="terminal"', 'qt'),
|
|
309
|
+
('type="testing"', 'qtest'),
|
|
310
|
+
('type="ui"', 'ui')):
|
|
311
|
+
if atributo in content and f'xmlns:{prefixo}=' not in content:
|
|
312
|
+
faltando[prefixo] = self._NAMESPACES[prefixo]
|
|
313
|
+
|
|
314
|
+
if not faltando:
|
|
315
|
+
return content
|
|
316
|
+
|
|
317
|
+
declaracoes = ''.join(f' xmlns:{p}="{u}"' for p, u in
|
|
318
|
+
sorted(faltando.items()))
|
|
319
|
+
|
|
320
|
+
def declarar(match):
|
|
321
|
+
nome, attrs, fecho = match.groups()
|
|
322
|
+
return f'<{nome}{attrs}{declaracoes}{fecho}>'
|
|
323
|
+
|
|
324
|
+
# Apenas no primeiro ELEMENTO. O prologo — declaracao XML, doctype e
|
|
325
|
+
# comentarios — e pulado inteiro: examples/python-scripting.q abre
|
|
326
|
+
# com um comentario que menciona `<cfscript>`, e casar ali injetaria
|
|
327
|
+
# os namespaces dentro do comentario, deixando o componente de
|
|
328
|
+
# verdade sem nenhum.
|
|
329
|
+
deslocamento = self._offset_do_primeiro_elemento(content)
|
|
330
|
+
substituido = self._FIRST_ELEMENT.sub(
|
|
331
|
+
declarar, content[deslocamento:], count=1)
|
|
332
|
+
return content[:deslocamento] + substituido
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _offset_do_primeiro_elemento(content: str) -> int:
|
|
336
|
+
"""Onde comeca o primeiro elemento de verdade."""
|
|
337
|
+
i = 0
|
|
338
|
+
n = len(content)
|
|
339
|
+
while i < n:
|
|
340
|
+
while i < n and content[i].isspace():
|
|
341
|
+
i += 1
|
|
342
|
+
if content.startswith('<?', i):
|
|
343
|
+
fim = content.find('?>', i)
|
|
344
|
+
if fim == -1:
|
|
345
|
+
return i
|
|
346
|
+
i = fim + 2
|
|
347
|
+
elif content.startswith('<!--', i):
|
|
348
|
+
fim = content.find('-->', i)
|
|
349
|
+
if fim == -1:
|
|
350
|
+
return i
|
|
351
|
+
i = fim + 3
|
|
352
|
+
elif content.startswith('<!', i): # DOCTYPE
|
|
353
|
+
fim = content.find('>', i)
|
|
354
|
+
if fim == -1:
|
|
355
|
+
return i
|
|
356
|
+
i = fim + 1
|
|
357
|
+
else:
|
|
358
|
+
return i
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
def parse(self, source: str) -> QuantumNode:
|
|
362
|
+
"""Parse Quantum XML from a string."""
|
|
363
|
+
try:
|
|
364
|
+
# Accept the HTML people actually write: boolean attributes
|
|
365
|
+
# (<input required />) and bare ampersands (Forms & Actions) are
|
|
366
|
+
# valid HTML and invalid XML. Ten of the forty-eight components
|
|
367
|
+
# shipped with this framework — login.q among them — failed to
|
|
368
|
+
# parse for exactly those two reasons and returned HTTP 400.
|
|
369
|
+
content = normalise_html(source)
|
|
370
|
+
content = self._inject_namespace(content)
|
|
371
|
+
root = ET.fromstring(content)
|
|
372
|
+
return self._parse_root_element(root, Path("<string>"))
|
|
373
|
+
except ET.ParseError as e:
|
|
374
|
+
raise QuantumParseError(_explain_xml_error(e, source))
|
|
375
|
+
except QuantumParseError:
|
|
376
|
+
raise
|
|
377
|
+
except Exception as e:
|
|
378
|
+
raise QuantumParseError(f"Unexpected error: {e}")
|
|
379
|
+
|
|
380
|
+
def parse_file(self, file_path: str, use_cache: bool = None) -> QuantumNode:
|
|
381
|
+
"""Parse .q file and return AST
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
file_path: Path to the .q file
|
|
385
|
+
use_cache: Override cache behavior (None = use instance default)
|
|
386
|
+
"""
|
|
387
|
+
path = Path(file_path)
|
|
388
|
+
|
|
389
|
+
if not path.exists():
|
|
390
|
+
raise QuantumParseError(f"File not found: {file_path}")
|
|
391
|
+
|
|
392
|
+
if not path.suffix == '.q':
|
|
393
|
+
raise QuantumParseError(f"Invalid extension: {path.suffix}, expected .q")
|
|
394
|
+
|
|
395
|
+
# Determine if we should use cache
|
|
396
|
+
should_cache = use_cache if use_cache is not None else self._use_cache
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
# Try cache first (Phase 2 optimization)
|
|
400
|
+
if should_cache and self._ast_cache is not None:
|
|
401
|
+
self._source_path = str(path.resolve())
|
|
402
|
+
return self._ast_cache.get_or_parse(file_path, self)
|
|
403
|
+
|
|
404
|
+
# Fallback to direct parsing
|
|
405
|
+
self._source_path = str(path.resolve())
|
|
406
|
+
source = Path(file_path).read_text(encoding="utf-8")
|
|
407
|
+
return self.parse(source)
|
|
408
|
+
except QuantumParseError:
|
|
409
|
+
raise
|
|
410
|
+
except Exception as e:
|
|
411
|
+
raise QuantumParseError(f"Unexpected error: {e}")
|
|
412
|
+
|
|
413
|
+
def invalidate_cache(self, file_path: str = None):
|
|
414
|
+
"""Invalidate cached AST for a file or all files
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
file_path: Specific file to invalidate, or None for all
|
|
418
|
+
"""
|
|
419
|
+
if self._ast_cache is not None:
|
|
420
|
+
self._ast_cache.invalidate(file_path)
|
|
421
|
+
|
|
422
|
+
def _parse_root_element(self, root: ET.Element, path: Path) -> QuantumNode:
|
|
423
|
+
"""Parse root element based on type"""
|
|
424
|
+
root_type = self._get_element_name(root)
|
|
425
|
+
|
|
426
|
+
if root_type == 'component':
|
|
427
|
+
return self._parse_component(root, path)
|
|
428
|
+
elif root_type == 'application':
|
|
429
|
+
return self._parse_application(root, path)
|
|
430
|
+
elif root_type == 'job':
|
|
431
|
+
return self._parse_job(root, path)
|
|
432
|
+
else:
|
|
433
|
+
raise QuantumParseError(f"Unknown root element: {root_type}")
|
|
434
|
+
|
|
435
|
+
def _get_element_name(self, element: ET.Element) -> str:
|
|
436
|
+
"""Extract element name removing namespace"""
|
|
437
|
+
return element.tag.split('}')[-1] if '}' in element.tag else element.tag.split(':')[-1]
|
|
438
|
+
|
|
439
|
+
def _find_element(self, parent: ET.Element, tag_name: str) -> Optional[ET.Element]:
|
|
440
|
+
"""Find element considering namespace"""
|
|
441
|
+
# Try with namespace
|
|
442
|
+
element = parent.find(f"q:{tag_name}", self.quantum_ns)
|
|
443
|
+
if element is not None:
|
|
444
|
+
return element
|
|
445
|
+
|
|
446
|
+
# Try without namespace (fallback)
|
|
447
|
+
return parent.find(tag_name)
|
|
448
|
+
|
|
449
|
+
def _find_all_elements(self, parent: ET.Element, tag_name: str) -> list:
|
|
450
|
+
"""Find all elements considering namespace"""
|
|
451
|
+
# Try with namespace
|
|
452
|
+
elements = parent.findall(f"q:{tag_name}", self.quantum_ns)
|
|
453
|
+
if elements:
|
|
454
|
+
return elements
|
|
455
|
+
|
|
456
|
+
# Try without namespace (fallback)
|
|
457
|
+
return parent.findall(tag_name)
|
|
458
|
+
|
|
459
|
+
def _is_html_element(self, element: ET.Element) -> bool:
|
|
460
|
+
"""
|
|
461
|
+
Check if element is HTML (not a Quantum tag).
|
|
462
|
+
|
|
463
|
+
Returns True for:
|
|
464
|
+
- Regular HTML tags (div, span, p, etc.)
|
|
465
|
+
- DOCTYPE declarations
|
|
466
|
+
- Comments
|
|
467
|
+
|
|
468
|
+
Returns False for:
|
|
469
|
+
- Quantum tags (q:* namespace)
|
|
470
|
+
- Special Quantum tags (param, return, function, etc.)
|
|
471
|
+
"""
|
|
472
|
+
tag = element.tag
|
|
473
|
+
|
|
474
|
+
# Skip namespace declarations and special elements
|
|
475
|
+
if tag.startswith('{'):
|
|
476
|
+
# Has XML namespace - check if it's Quantum namespace
|
|
477
|
+
if '{https://quantum.lang/ns}' in tag:
|
|
478
|
+
return False
|
|
479
|
+
# Other namespaces might be HTML5 or SVG
|
|
480
|
+
return True
|
|
481
|
+
|
|
482
|
+
# Check for q: prefix (Quantum tags)
|
|
483
|
+
if tag.startswith('q:'):
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
# Quantum tags without prefix (when xmlns:q is set)
|
|
487
|
+
quantum_tags = {
|
|
488
|
+
'component', 'application', 'job', 'param', 'return', 'route',
|
|
489
|
+
'if', 'elseif', 'else', 'loop', 'set', 'function', 'dispatchEvent',
|
|
490
|
+
'onEvent', 'script', 'query', 'invoke', 'data', 'log', 'dump',
|
|
491
|
+
'import', 'slot', # Phase 2
|
|
492
|
+
'knowledge', # Knowledge Base / RAG
|
|
493
|
+
'schedule', 'thread', # Job Execution System
|
|
494
|
+
}
|
|
495
|
+
if tag in quantum_tags:
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
# Component calls (Phase 2) - Uppercase tags are components, not HTML
|
|
499
|
+
if tag and tag[0].isupper():
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
# Everything else is HTML
|
|
503
|
+
return True
|
|
504
|
+
|
|
505
|
+
def _parse_component(self, root: ET.Element, path: Path) -> ComponentNode:
|
|
506
|
+
"""Parse q:component"""
|
|
507
|
+
name = root.get('name', path.stem)
|
|
508
|
+
component_type = root.get('type', 'pure')
|
|
509
|
+
|
|
510
|
+
component = ComponentNode(name, component_type)
|
|
511
|
+
|
|
512
|
+
# Parse component-level attributes
|
|
513
|
+
component.port = root.get('port')
|
|
514
|
+
component.base_path = root.get('basePath')
|
|
515
|
+
component.health_endpoint = root.get('health')
|
|
516
|
+
component.metrics_provider = root.get('metrics')
|
|
517
|
+
component.trace_provider = root.get('trace')
|
|
518
|
+
|
|
519
|
+
# Phase G: Authentication & Security attributes
|
|
520
|
+
require_auth_attr = root.get('require_auth', 'false').lower()
|
|
521
|
+
component.require_auth = require_auth_attr in ['true', '1', 'yes']
|
|
522
|
+
component.require_role = root.get('require_role')
|
|
523
|
+
component.require_permission = root.get('require_permission')
|
|
524
|
+
|
|
525
|
+
# HTML rendering & interactivity (Phase 1 & future Phase 3)
|
|
526
|
+
interactive_attr = root.get('interactive', 'false').lower()
|
|
527
|
+
component.interactive = interactive_attr in ['true', '1', 'yes']
|
|
528
|
+
|
|
529
|
+
# Parse q:param elements (component-level params)
|
|
530
|
+
for param_el in self._find_all_elements(root, 'param'):
|
|
531
|
+
param = self._parse_param(param_el)
|
|
532
|
+
component.add_param(param)
|
|
533
|
+
|
|
534
|
+
# Parse q:return elements (component-level returns)
|
|
535
|
+
for return_el in self._find_all_elements(root, 'return'):
|
|
536
|
+
return_node = self._parse_return(return_el)
|
|
537
|
+
component.add_return(return_node)
|
|
538
|
+
|
|
539
|
+
# Parse q:function elements
|
|
540
|
+
for func_el in self._find_all_elements(root, 'function'):
|
|
541
|
+
func = self._parse_function(func_el)
|
|
542
|
+
component.add_function(func)
|
|
543
|
+
|
|
544
|
+
# Parse q:onEvent elements
|
|
545
|
+
for event_el in self._find_all_elements(root, 'onEvent'):
|
|
546
|
+
event_handler = self._parse_on_event(event_el)
|
|
547
|
+
component.add_event_handler(event_handler)
|
|
548
|
+
|
|
549
|
+
# Parse q:script elements
|
|
550
|
+
for script_el in self._find_all_elements(root, 'script'):
|
|
551
|
+
script_content = script_el.text or ""
|
|
552
|
+
component.add_script(script_content.strip())
|
|
553
|
+
|
|
554
|
+
# Parse control flow statements (if, loop, set, dispatchEvent, etc)
|
|
555
|
+
self._parse_control_flow_statements(root, component)
|
|
556
|
+
|
|
557
|
+
return component
|
|
558
|
+
|
|
559
|
+
def _parse_control_flow_statements(self, parent: ET.Element, component: ComponentNode):
|
|
560
|
+
"""Parse control flow statements - delegates to modular parser registry"""
|
|
561
|
+
for child in parent:
|
|
562
|
+
child_type = self._get_element_name(child)
|
|
563
|
+
|
|
564
|
+
# Special case: inline job definition (needs path parameter)
|
|
565
|
+
if child_type == 'job':
|
|
566
|
+
from pathlib import Path as PathLib
|
|
567
|
+
job_node = self._parse_job(child, PathLib("<inline>"))
|
|
568
|
+
component.add_statement(job_node)
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
# `<q:else>` / `<q:elseif>` IRMAO de um `<q:if>` no corpo do
|
|
572
|
+
# componente: anexa ao if anterior. A forma irma nao tinha parser
|
|
573
|
+
# e sumia calada — a documentacao a ensinava e arquivos entregues
|
|
574
|
+
# a usavam (ver base.parse_statements para o porque completo).
|
|
575
|
+
if child_type in ('else', 'elseif'):
|
|
576
|
+
anterior = component.statements[-1] if component.statements else None
|
|
577
|
+
if isinstance(anterior, IfNode):
|
|
578
|
+
self._attach_else_sibling(anterior, child_type, child)
|
|
579
|
+
continue
|
|
580
|
+
raise QuantumParseError(
|
|
581
|
+
f"<q:{child_type}> has no matching <q:if> before it. "
|
|
582
|
+
f"Put it inside the if, or right after it: "
|
|
583
|
+
f"<q:if ...>...</q:if> <q:{child_type}>...</q:{child_type}>"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Delegate to modular parser registry via _parse_statement
|
|
587
|
+
node = self._parse_statement(child)
|
|
588
|
+
if node is not None:
|
|
589
|
+
component.add_statement(node)
|
|
590
|
+
# Check if this produces HTML
|
|
591
|
+
if self._is_html_element(child) or (child_type and child_type[0].isupper()):
|
|
592
|
+
component.has_html = True
|
|
593
|
+
|
|
594
|
+
def _attach_else_sibling(self, if_node: 'IfNode', kind: str, element: ET.Element):
|
|
595
|
+
"""Anexa um else/elseif IRMAO ao IfNode anterior — mesmo resultado da
|
|
596
|
+
forma aninhada que o IfParser ja monta."""
|
|
597
|
+
if kind == 'elseif':
|
|
598
|
+
condicao = element.get('condition', '')
|
|
599
|
+
corpo = []
|
|
600
|
+
for filho in element:
|
|
601
|
+
stmt = self._parse_statement(filho)
|
|
602
|
+
if stmt:
|
|
603
|
+
corpo.append(stmt)
|
|
604
|
+
if_node.add_elseif_block(condicao, corpo)
|
|
605
|
+
else:
|
|
606
|
+
for filho in element:
|
|
607
|
+
stmt = self._parse_statement(filho)
|
|
608
|
+
if stmt:
|
|
609
|
+
if_node.add_else_statement(stmt)
|
|
610
|
+
|
|
611
|
+
def _parse_statement(self, element: ET.Element) -> Optional[QuantumNode]:
|
|
612
|
+
"""Parse individual statement (return, set, dispatchEvent, etc)"""
|
|
613
|
+
element_type = self._get_element_name(element)
|
|
614
|
+
|
|
615
|
+
# === MODULAR PARSER REGISTRY (no fallback) ===
|
|
616
|
+
# PHASE 1 DIAGNOSTIC: Legacy fallback disabled to test modular coverage
|
|
617
|
+
if self._use_modular_parsers and self._parser_registry:
|
|
618
|
+
if self._parser_registry.has_parser(element_type):
|
|
619
|
+
return self._parser_registry.parse(element, element_type)
|
|
620
|
+
|
|
621
|
+
# Component calls (uppercase naming convention)
|
|
622
|
+
if element_type and element_type[0].isupper():
|
|
623
|
+
from quantum.core.parsers import ComponentCallParser
|
|
624
|
+
if ComponentCallParser.is_component_call(element_type):
|
|
625
|
+
return self._parser_registry.component_call_parser.parse(element)
|
|
626
|
+
|
|
627
|
+
# HTML elements
|
|
628
|
+
# <q:return> aninhado. Nao ha parser registrado para 'return' —
|
|
629
|
+
# o nivel de componente o coleta a parte, com _find_all_elements —
|
|
630
|
+
# entao aqui ele caia no `return None` do fim e era DESCARTADO em
|
|
631
|
+
# silencio. `<q:if ...><q:return value="X"/></q:if>` nao retornava
|
|
632
|
+
# X: o componente seguia e devolvia o return seguinte, ou nada.
|
|
633
|
+
# O mesmo valia dentro de q:loop e q:else.
|
|
634
|
+
if element_type == 'return':
|
|
635
|
+
return self._parse_return(element)
|
|
636
|
+
|
|
637
|
+
if self._is_html_element(element):
|
|
638
|
+
if self._parser_registry.html_parser.can_parse(element_type):
|
|
639
|
+
return self._parser_registry.html_parser.parse(element)
|
|
640
|
+
|
|
641
|
+
# No parser found - this is unexpected
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
# If modular parsers are disabled, raise an error
|
|
645
|
+
raise QuantumParseError("Modular parsers are disabled but legacy fallback has been removed")
|
|
646
|
+
|
|
647
|
+
def _parse_application(self, root: ET.Element, path: Path) -> ApplicationNode:
|
|
648
|
+
"""Parse q:application"""
|
|
649
|
+
app_id = root.get('id', path.stem)
|
|
650
|
+
app_type = root.get('type', 'html')
|
|
651
|
+
|
|
652
|
+
app = ApplicationNode(app_id, app_type)
|
|
653
|
+
app.engine = root.get('engine')
|
|
654
|
+
|
|
655
|
+
# Parse theme attribute for UI applications
|
|
656
|
+
theme_attr = root.get('theme')
|
|
657
|
+
if theme_attr and app_type == 'ui':
|
|
658
|
+
app.ui_theme_preset = theme_attr
|
|
659
|
+
# Create a UIThemeNode from the attribute
|
|
660
|
+
theme_node = UIThemeNode()
|
|
661
|
+
theme_node.preset = theme_attr
|
|
662
|
+
app.ui_theme = theme_node
|
|
663
|
+
|
|
664
|
+
# Parse q:route elements
|
|
665
|
+
for route_el in self._find_all_elements(root, 'route'):
|
|
666
|
+
route = self._parse_route(route_el)
|
|
667
|
+
app.add_route(route)
|
|
668
|
+
|
|
669
|
+
# q:set e q:function no topo do <q:application>, para qualquer
|
|
670
|
+
# engine. Os parsers por engine so olham os filhos do proprio
|
|
671
|
+
# namespace (qt:, qg:, ui:), entao estes eram silenciosamente
|
|
672
|
+
# descartados — inclusive a funcao para a qual um on-click apontava.
|
|
673
|
+
self._parse_application_quantum_children(root, app)
|
|
674
|
+
|
|
675
|
+
# Game Engine 2D: parse game children when type="game"
|
|
676
|
+
if app_type == 'game':
|
|
677
|
+
self._parse_game_application_children(root, app)
|
|
678
|
+
|
|
679
|
+
# Terminal Engine: parse terminal children when type="terminal"
|
|
680
|
+
if app_type == 'terminal':
|
|
681
|
+
self._parse_terminal_application_children(root, app)
|
|
682
|
+
|
|
683
|
+
# Testing Engine: parse testing children when type="testing"
|
|
684
|
+
if app_type == 'testing':
|
|
685
|
+
self._parse_testing_application_children(root, app)
|
|
686
|
+
|
|
687
|
+
# UI Engine: parse ui children when type="ui"
|
|
688
|
+
if app_type == 'ui':
|
|
689
|
+
self._parse_ui_application_children(root, app)
|
|
690
|
+
|
|
691
|
+
return app
|
|
692
|
+
|
|
693
|
+
def _parse_application_quantum_children(self, root: ET.Element,
|
|
694
|
+
app: ApplicationNode):
|
|
695
|
+
"""Coleta <q:function> e <q:set> escritos direto no q:application."""
|
|
696
|
+
for child in root:
|
|
697
|
+
local_name = self._get_element_name(child)
|
|
698
|
+
if local_name not in ('function', 'set'):
|
|
699
|
+
continue
|
|
700
|
+
tag = child.tag
|
|
701
|
+
# So o namespace q:. Um <ui:set> ou <qg:set> pertence ao seu
|
|
702
|
+
# proprio parser, e um elemento sem prefixo nao e tag do
|
|
703
|
+
# framework.
|
|
704
|
+
if not (tag.startswith('q:')
|
|
705
|
+
or tag.startswith('{https://quantum.lang/ns}')):
|
|
706
|
+
continue
|
|
707
|
+
try:
|
|
708
|
+
node = self._parse_statement(child)
|
|
709
|
+
except QuantumParseError:
|
|
710
|
+
raise
|
|
711
|
+
except Exception:
|
|
712
|
+
continue
|
|
713
|
+
if node is None:
|
|
714
|
+
continue
|
|
715
|
+
if isinstance(node, FunctionNode):
|
|
716
|
+
app.functions.append(node)
|
|
717
|
+
elif isinstance(node, SetNode):
|
|
718
|
+
app.state_vars.append(node)
|
|
719
|
+
|
|
720
|
+
def _parse_game_application_children(self, root: ET.Element, app: ApplicationNode):
|
|
721
|
+
"""Parse children of a game application (qg: elements at top level)."""
|
|
722
|
+
game_parser = GameParser(self)
|
|
723
|
+
for child in root:
|
|
724
|
+
local_name = self._get_element_name(child)
|
|
725
|
+
ns = self._get_element_game_namespace(child)
|
|
726
|
+
|
|
727
|
+
if ns == 'game':
|
|
728
|
+
# Handle scene-include specially (resolved at parse time)
|
|
729
|
+
if local_name == 'scene-include':
|
|
730
|
+
self._parse_scene_include(child, app, game_parser)
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
node = game_parser.parse_game_element(local_name, child)
|
|
734
|
+
if isinstance(node, SceneNode):
|
|
735
|
+
app.scenes.append(node)
|
|
736
|
+
elif isinstance(node, BehaviorNode):
|
|
737
|
+
app.behaviors.append(node)
|
|
738
|
+
elif isinstance(node, PrefabNode):
|
|
739
|
+
app.prefabs.append(node)
|
|
740
|
+
elif isinstance(node, EnemyNode):
|
|
741
|
+
app.enemies.append(node)
|
|
742
|
+
elif isinstance(node, PersistentNode):
|
|
743
|
+
app.persistent.append(node)
|
|
744
|
+
|
|
745
|
+
def _parse_scene_include(self, element: ET.Element, app: ApplicationNode, game_parser):
|
|
746
|
+
"""Parse <qg:scene-include src="..."> - Include a scene from external .q file.
|
|
747
|
+
|
|
748
|
+
The external file should contain a <qg:scene> element (with or without
|
|
749
|
+
a <q:application> wrapper). The scene is added to the application's scene list.
|
|
750
|
+
"""
|
|
751
|
+
src = element.get('src', '')
|
|
752
|
+
if not src:
|
|
753
|
+
raise QuantumParseError("scene-include requires a 'src' attribute")
|
|
754
|
+
|
|
755
|
+
# Resolve relative path from the source file being parsed
|
|
756
|
+
base_path = getattr(self, '_source_path', None)
|
|
757
|
+
if base_path:
|
|
758
|
+
include_path = Path(base_path).parent / src
|
|
759
|
+
else:
|
|
760
|
+
include_path = Path(src)
|
|
761
|
+
|
|
762
|
+
if not include_path.exists():
|
|
763
|
+
raise QuantumParseError(f"scene-include: file not found: {include_path}")
|
|
764
|
+
|
|
765
|
+
content = include_path.read_text(encoding='utf-8')
|
|
766
|
+
|
|
767
|
+
# O mesmo HTML tolerado em qualquer outro .q. Este caminho le um
|
|
768
|
+
# arquivo .q e ia direto para o ET.fromstring la embaixo, sem passar
|
|
769
|
+
# por aqui: um `<br>`, um `&` cru, um atributo booleano ou um `<`
|
|
770
|
+
# dentro de um valor davam "scene-include: parse error" num arquivo
|
|
771
|
+
# que o parser normal aceita sem reclamar.
|
|
772
|
+
content = normalise_html(content)
|
|
773
|
+
|
|
774
|
+
# Strip XML processing instruction if present
|
|
775
|
+
import re
|
|
776
|
+
content = re.sub(r'<\?xml[^?]*\?>\s*', '', content)
|
|
777
|
+
# Strip XML comments at the top level
|
|
778
|
+
content = re.sub(r'<!--[\s\S]*?-->\s*', '', content, count=1)
|
|
779
|
+
content = content.strip()
|
|
780
|
+
|
|
781
|
+
# Wrap in a root element with namespace declarations
|
|
782
|
+
# This handles bare <qg:scene> files that don't have <q:application>
|
|
783
|
+
if 'xmlns:qg' not in content:
|
|
784
|
+
ns_attrs = 'xmlns:qg="https://quantum.lang/game"'
|
|
785
|
+
if 'q:' in content:
|
|
786
|
+
ns_attrs += ' xmlns:q="https://quantum.lang/ns"'
|
|
787
|
+
content = f'<_root {ns_attrs}>{content}</_root>'
|
|
788
|
+
|
|
789
|
+
try:
|
|
790
|
+
include_root = ET.fromstring(content)
|
|
791
|
+
except ET.ParseError as e:
|
|
792
|
+
raise QuantumParseError(f"scene-include: parse error in {src}: {e}")
|
|
793
|
+
|
|
794
|
+
# Find scene element(s) in the included file
|
|
795
|
+
self._extract_scenes_from_include(include_root, app, game_parser)
|
|
796
|
+
|
|
797
|
+
def _extract_scenes_from_include(self, root: ET.Element, app: ApplicationNode, game_parser):
|
|
798
|
+
"""Extract SceneNode(s) from an included file's parsed XML."""
|
|
799
|
+
tag = root.tag
|
|
800
|
+
# Strip namespace
|
|
801
|
+
if '}' in tag:
|
|
802
|
+
local = tag.split('}')[-1]
|
|
803
|
+
elif ':' in tag:
|
|
804
|
+
local = tag.split(':')[-1]
|
|
805
|
+
else:
|
|
806
|
+
local = tag
|
|
807
|
+
|
|
808
|
+
if local == 'scene':
|
|
809
|
+
# Root element is the scene itself
|
|
810
|
+
node = game_parser.parse_game_element('scene', root)
|
|
811
|
+
if isinstance(node, SceneNode):
|
|
812
|
+
app.scenes.append(node)
|
|
813
|
+
else:
|
|
814
|
+
# Root is a wrapper — look for scene children
|
|
815
|
+
for child in root:
|
|
816
|
+
child_ns = self._get_element_game_namespace(child)
|
|
817
|
+
child_local = self._get_element_name(child)
|
|
818
|
+
if child_ns == 'game' and child_local == 'scene':
|
|
819
|
+
node = game_parser.parse_game_element('scene', child)
|
|
820
|
+
if isinstance(node, SceneNode):
|
|
821
|
+
app.scenes.append(node)
|
|
822
|
+
|
|
823
|
+
def _get_element_game_namespace(self, element: ET.Element) -> str:
|
|
824
|
+
"""Detect if element belongs to qg: (game) or q: (quantum) namespace."""
|
|
825
|
+
tag = element.tag
|
|
826
|
+
if '{https://quantum.lang/game}' in tag:
|
|
827
|
+
return 'game'
|
|
828
|
+
if '{https://quantum.lang/ns}' in tag:
|
|
829
|
+
return 'quantum'
|
|
830
|
+
if tag.startswith('qg:'):
|
|
831
|
+
return 'game'
|
|
832
|
+
if tag.startswith('q:'):
|
|
833
|
+
return 'quantum'
|
|
834
|
+
return 'html'
|
|
835
|
+
|
|
836
|
+
def _parse_terminal_application_children(self, root: ET.Element, app: ApplicationNode):
|
|
837
|
+
"""Parse children of a terminal application (qt: elements at top level)."""
|
|
838
|
+
terminal_parser = TerminalParser(self)
|
|
839
|
+
for child in root:
|
|
840
|
+
local_name = self._get_element_name(child)
|
|
841
|
+
ns = self._get_element_terminal_namespace(child)
|
|
842
|
+
|
|
843
|
+
if ns == 'terminal':
|
|
844
|
+
node = terminal_parser.parse_terminal_element(local_name, child)
|
|
845
|
+
if isinstance(node, TerminalScreenNode):
|
|
846
|
+
app.screens.append(node)
|
|
847
|
+
elif isinstance(node, TerminalKeybindingNode):
|
|
848
|
+
app.keybindings.append(node)
|
|
849
|
+
elif isinstance(node, TerminalServiceNode):
|
|
850
|
+
app.services.append(node)
|
|
851
|
+
elif isinstance(node, TerminalCssNode):
|
|
852
|
+
app.terminal_css += node.content + '\n'
|
|
853
|
+
|
|
854
|
+
def _get_element_terminal_namespace(self, element: ET.Element) -> str:
|
|
855
|
+
"""Detect if element belongs to qt: (terminal) or q: (quantum) namespace."""
|
|
856
|
+
tag = element.tag
|
|
857
|
+
if '{https://quantum.lang/terminal}' in tag:
|
|
858
|
+
return 'terminal'
|
|
859
|
+
if '{https://quantum.lang/ns}' in tag:
|
|
860
|
+
return 'quantum'
|
|
861
|
+
if tag.startswith('qt:'):
|
|
862
|
+
return 'terminal'
|
|
863
|
+
if tag.startswith('q:'):
|
|
864
|
+
return 'quantum'
|
|
865
|
+
return 'html'
|
|
866
|
+
|
|
867
|
+
def _parse_testing_application_children(self, root: ET.Element, app: ApplicationNode):
|
|
868
|
+
"""Parse children of a testing application (qtest: elements at top level)."""
|
|
869
|
+
testing_parser = TestingParser(self)
|
|
870
|
+
for child in root:
|
|
871
|
+
local_name = self._get_element_name(child)
|
|
872
|
+
ns = self._get_element_testing_namespace(child)
|
|
873
|
+
|
|
874
|
+
if ns == 'testing':
|
|
875
|
+
node = testing_parser.parse_testing_element(local_name, child)
|
|
876
|
+
if isinstance(node, TestingTestSuiteNode):
|
|
877
|
+
app.test_suites.append(node)
|
|
878
|
+
elif isinstance(node, TestingBrowserConfigNode):
|
|
879
|
+
app.test_config = node
|
|
880
|
+
elif isinstance(node, TestingFixtureNode):
|
|
881
|
+
app.test_fixtures.append(node)
|
|
882
|
+
elif isinstance(node, TestingMockNode):
|
|
883
|
+
app.test_mocks.append(node)
|
|
884
|
+
elif isinstance(node, TestingAuthNode):
|
|
885
|
+
app.test_auth_states.append(node)
|
|
886
|
+
|
|
887
|
+
def _get_element_testing_namespace(self, element: ET.Element) -> str:
|
|
888
|
+
"""Detect if element belongs to qtest: (testing) or q: (quantum) namespace."""
|
|
889
|
+
tag = element.tag
|
|
890
|
+
if '{https://quantum.lang/testing}' in tag:
|
|
891
|
+
return 'testing'
|
|
892
|
+
if '{https://quantum.lang/ns}' in tag:
|
|
893
|
+
return 'quantum'
|
|
894
|
+
if tag.startswith('qtest:'):
|
|
895
|
+
return 'testing'
|
|
896
|
+
if tag.startswith('q:'):
|
|
897
|
+
return 'quantum'
|
|
898
|
+
return 'html'
|
|
899
|
+
|
|
900
|
+
def _parse_ui_application_children(self, root: ET.Element, app: ApplicationNode):
|
|
901
|
+
"""Parse children of a UI application (ui: elements at top level)."""
|
|
902
|
+
ui_parser = UIParser(self)
|
|
903
|
+
for child in root:
|
|
904
|
+
local_name = self._get_element_name(child)
|
|
905
|
+
ns = self._get_element_ui_namespace(child)
|
|
906
|
+
|
|
907
|
+
if ns == 'ui':
|
|
908
|
+
node = ui_parser.parse_ui_element(local_name, child)
|
|
909
|
+
if isinstance(node, UIWindowNode):
|
|
910
|
+
app.ui_windows.append(node)
|
|
911
|
+
elif isinstance(node, UIThemeNode):
|
|
912
|
+
# ui:theme overrides any theme attribute
|
|
913
|
+
app.ui_theme = node
|
|
914
|
+
else:
|
|
915
|
+
app.ui_children.append(node)
|
|
916
|
+
elif ns == 'quantum':
|
|
917
|
+
# Allow q: tags at the top level of a UI app (e.g. q:set)
|
|
918
|
+
statement = self._parse_statement(child)
|
|
919
|
+
if statement:
|
|
920
|
+
app.ui_children.append(statement)
|
|
921
|
+
|
|
922
|
+
def _get_element_ui_namespace(self, element: ET.Element) -> str:
|
|
923
|
+
"""Detect if element belongs to ui: (UI engine) or q: (quantum) namespace."""
|
|
924
|
+
tag = element.tag
|
|
925
|
+
if '{https://quantum.lang/ui}' in tag:
|
|
926
|
+
return 'ui'
|
|
927
|
+
if '{https://quantum.lang/ns}' in tag:
|
|
928
|
+
return 'quantum'
|
|
929
|
+
if tag.startswith('ui:'):
|
|
930
|
+
return 'ui'
|
|
931
|
+
if tag.startswith('q:'):
|
|
932
|
+
return 'quantum'
|
|
933
|
+
return 'html'
|
|
934
|
+
|
|
935
|
+
def _parse_job(self, root: ET.Element, path: Path) -> JobNode:
|
|
936
|
+
"""
|
|
937
|
+
Parse q:job - Job queue for batch processing.
|
|
938
|
+
|
|
939
|
+
Examples:
|
|
940
|
+
<q:job name="sendEmail" queue="emails" priority="5">
|
|
941
|
+
<q:param name="to" type="string" />
|
|
942
|
+
<q:mail to="{to}" subject="Notification">...</q:mail>
|
|
943
|
+
</q:job>
|
|
944
|
+
|
|
945
|
+
<q:job name="processOrder" action="dispatch" delay="5m" />
|
|
946
|
+
"""
|
|
947
|
+
# Support both 'name' and legacy 'id' attribute
|
|
948
|
+
name = root.get('name') or root.get('id', path.stem)
|
|
949
|
+
queue = root.get('queue', 'default')
|
|
950
|
+
action = root.get('action', 'define')
|
|
951
|
+
|
|
952
|
+
job_node = JobNode(name, queue, action)
|
|
953
|
+
|
|
954
|
+
# Parse optional attributes
|
|
955
|
+
job_node.delay = root.get('delay')
|
|
956
|
+
job_node.timeout = root.get('timeout')
|
|
957
|
+
job_node.backoff = root.get('backoff', '30s')
|
|
958
|
+
|
|
959
|
+
priority_attr = root.get('priority')
|
|
960
|
+
if priority_attr:
|
|
961
|
+
try:
|
|
962
|
+
job_node.priority = int(priority_attr)
|
|
963
|
+
except ValueError:
|
|
964
|
+
pass
|
|
965
|
+
|
|
966
|
+
attempts_attr = root.get('attempts')
|
|
967
|
+
if attempts_attr:
|
|
968
|
+
try:
|
|
969
|
+
job_node.attempts = int(attempts_attr)
|
|
970
|
+
except ValueError:
|
|
971
|
+
pass
|
|
972
|
+
|
|
973
|
+
# Legacy support
|
|
974
|
+
job_node.schedule = root.get('schedule')
|
|
975
|
+
|
|
976
|
+
# Parse children (params and body statements)
|
|
977
|
+
for child in root:
|
|
978
|
+
child_type = self._get_element_name(child)
|
|
979
|
+
|
|
980
|
+
if child_type == 'param':
|
|
981
|
+
param = self._parse_param(child)
|
|
982
|
+
job_node.add_param(param)
|
|
983
|
+
else:
|
|
984
|
+
# Parse body statements
|
|
985
|
+
statement = self._parse_statement(child)
|
|
986
|
+
if statement:
|
|
987
|
+
job_node.add_statement(statement)
|
|
988
|
+
|
|
989
|
+
return job_node
|
|
990
|
+
|
|
991
|
+
def _parse_param(self, element: ET.Element) -> QuantumParam:
|
|
992
|
+
"""Parse q:param"""
|
|
993
|
+
param = QuantumParam(
|
|
994
|
+
name=element.get('name', ''),
|
|
995
|
+
type=element.get('type', 'string'),
|
|
996
|
+
required=element.get('required', 'false').lower() == 'true',
|
|
997
|
+
default=element.get('default'),
|
|
998
|
+
validation=element.get('validation'),
|
|
999
|
+
description=element.get('description')
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# REST-specific
|
|
1003
|
+
param.source = element.get('source', 'auto')
|
|
1004
|
+
|
|
1005
|
+
# value= is how a CALL SITE passes an argument — <q:job> dispatch,
|
|
1006
|
+
# <q:invoke>, <q:query>. This function read only default=, so every
|
|
1007
|
+
# <q:param name="to" value="a@b.c" /> inside a q:job arrived with no
|
|
1008
|
+
# value at all and each dispatched job got nulls. Declaration sites
|
|
1009
|
+
# (q:function, q:action) use default= and are unaffected.
|
|
1010
|
+
param.value = element.get('value')
|
|
1011
|
+
|
|
1012
|
+
# Validation
|
|
1013
|
+
param.validate_rule = element.get('validate')
|
|
1014
|
+
param.pattern = element.get('pattern')
|
|
1015
|
+
param.min = element.get('min')
|
|
1016
|
+
param.max = element.get('max')
|
|
1017
|
+
|
|
1018
|
+
minlength_str = element.get('minlength')
|
|
1019
|
+
if minlength_str:
|
|
1020
|
+
try:
|
|
1021
|
+
param.minlength = int(minlength_str)
|
|
1022
|
+
except ValueError:
|
|
1023
|
+
pass
|
|
1024
|
+
|
|
1025
|
+
maxlength_str = element.get('maxlength')
|
|
1026
|
+
if maxlength_str:
|
|
1027
|
+
try:
|
|
1028
|
+
param.maxlength = int(maxlength_str)
|
|
1029
|
+
except ValueError:
|
|
1030
|
+
pass
|
|
1031
|
+
|
|
1032
|
+
param.range = element.get('range')
|
|
1033
|
+
param.enum = element.get('enum')
|
|
1034
|
+
|
|
1035
|
+
# File upload
|
|
1036
|
+
param.maxsize = element.get('maxsize')
|
|
1037
|
+
param.accept = element.get('accept')
|
|
1038
|
+
|
|
1039
|
+
return param
|
|
1040
|
+
|
|
1041
|
+
def _parse_return(self, element: ET.Element) -> QuantumReturn:
|
|
1042
|
+
"""Parse q:return"""
|
|
1043
|
+
return QuantumReturn(
|
|
1044
|
+
name=element.get('name'),
|
|
1045
|
+
type=element.get('type', 'string'),
|
|
1046
|
+
value=element.get('value', ''),
|
|
1047
|
+
description=element.get('description')
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
def _parse_route(self, element: ET.Element) -> QuantumRoute:
|
|
1051
|
+
"""Parse q:route"""
|
|
1052
|
+
path = element.get('path', '/')
|
|
1053
|
+
method = element.get('method', 'GET').upper()
|
|
1054
|
+
|
|
1055
|
+
route = QuantumRoute(path, method)
|
|
1056
|
+
|
|
1057
|
+
# Parse q:return inside the route
|
|
1058
|
+
for return_el in self._find_all_elements(element, 'return'):
|
|
1059
|
+
return_node = self._parse_return(return_el)
|
|
1060
|
+
route.returns.append(return_node)
|
|
1061
|
+
|
|
1062
|
+
return route
|
|
1063
|
+
|
|
1064
|
+
def _parse_function(self, func_element: ET.Element) -> FunctionNode:
|
|
1065
|
+
"""Parse q:function statement"""
|
|
1066
|
+
name = func_element.get('name')
|
|
1067
|
+
|
|
1068
|
+
if not name:
|
|
1069
|
+
raise QuantumParseError("Function requires 'name' attribute")
|
|
1070
|
+
|
|
1071
|
+
func_node = FunctionNode(name)
|
|
1072
|
+
|
|
1073
|
+
# Core attributes
|
|
1074
|
+
func_node.return_type = func_element.get('returnType', 'any')
|
|
1075
|
+
func_node.scope = func_element.get('scope', 'component')
|
|
1076
|
+
func_node.access = func_element.get('access', 'public')
|
|
1077
|
+
func_node.description = func_element.get('description')
|
|
1078
|
+
func_node.hint = func_element.get('hint')
|
|
1079
|
+
|
|
1080
|
+
# Validation
|
|
1081
|
+
func_node.validate_params = func_element.get('validate', 'false').lower() == 'true'
|
|
1082
|
+
|
|
1083
|
+
# Performance
|
|
1084
|
+
cache_attr = func_element.get('cache')
|
|
1085
|
+
if cache_attr:
|
|
1086
|
+
if cache_attr.lower() == 'true':
|
|
1087
|
+
func_node.cache = True
|
|
1088
|
+
elif cache_attr.endswith('s'): # "60s"
|
|
1089
|
+
func_node.cache = True
|
|
1090
|
+
try:
|
|
1091
|
+
func_node.cache_ttl = int(cache_attr[:-1])
|
|
1092
|
+
except ValueError:
|
|
1093
|
+
pass
|
|
1094
|
+
|
|
1095
|
+
func_node.memoize = func_element.get('memoize', 'false').lower() == 'true'
|
|
1096
|
+
func_node.pure = func_element.get('pure', 'false').lower() == 'true'
|
|
1097
|
+
|
|
1098
|
+
# Behavior
|
|
1099
|
+
func_node.async_func = func_element.get('async', 'false').lower() == 'true'
|
|
1100
|
+
|
|
1101
|
+
retry_attr = func_element.get('retry')
|
|
1102
|
+
if retry_attr:
|
|
1103
|
+
try:
|
|
1104
|
+
func_node.retry = int(retry_attr)
|
|
1105
|
+
except ValueError:
|
|
1106
|
+
pass
|
|
1107
|
+
|
|
1108
|
+
timeout_attr = func_element.get('timeout')
|
|
1109
|
+
if timeout_attr:
|
|
1110
|
+
func_node.timeout = timeout_attr
|
|
1111
|
+
|
|
1112
|
+
# REST API (optional)
|
|
1113
|
+
endpoint = func_element.get('endpoint')
|
|
1114
|
+
if endpoint:
|
|
1115
|
+
method = func_element.get('method', 'GET')
|
|
1116
|
+
func_node.enable_rest(endpoint, method)
|
|
1117
|
+
|
|
1118
|
+
# REST-specific attributes
|
|
1119
|
+
if func_element.get('produces'):
|
|
1120
|
+
func_node.rest_config.produces = func_element.get('produces')
|
|
1121
|
+
|
|
1122
|
+
if func_element.get('consumes'):
|
|
1123
|
+
func_node.rest_config.consumes = func_element.get('consumes')
|
|
1124
|
+
|
|
1125
|
+
if func_element.get('auth'):
|
|
1126
|
+
func_node.rest_config.auth = func_element.get('auth')
|
|
1127
|
+
|
|
1128
|
+
if func_element.get('roles'):
|
|
1129
|
+
roles_str = func_element.get('roles')
|
|
1130
|
+
func_node.rest_config.roles = [r.strip() for r in roles_str.split(',')]
|
|
1131
|
+
|
|
1132
|
+
if func_element.get('rateLimit'):
|
|
1133
|
+
func_node.rest_config.rate_limit = func_element.get('rateLimit')
|
|
1134
|
+
|
|
1135
|
+
cors_attr = func_element.get('cors')
|
|
1136
|
+
if cors_attr and cors_attr.lower() == 'true':
|
|
1137
|
+
func_node.rest_config.cors = True
|
|
1138
|
+
|
|
1139
|
+
status_attr = func_element.get('status')
|
|
1140
|
+
if status_attr:
|
|
1141
|
+
try:
|
|
1142
|
+
func_node.rest_config.status = int(status_attr)
|
|
1143
|
+
except ValueError:
|
|
1144
|
+
pass
|
|
1145
|
+
|
|
1146
|
+
# Parse function params and body
|
|
1147
|
+
for child in func_element:
|
|
1148
|
+
child_type = self._get_element_name(child)
|
|
1149
|
+
|
|
1150
|
+
if child_type == 'param':
|
|
1151
|
+
param = self._parse_param(child)
|
|
1152
|
+
func_node.add_param(param)
|
|
1153
|
+
elif child_type == 'return':
|
|
1154
|
+
# ReturnParser is intentionally not in the tag registry, so
|
|
1155
|
+
# _parse_statement() returns None for <q:return> — which
|
|
1156
|
+
# silently dropped the return from every function body,
|
|
1157
|
+
# making every q:function return nothing.
|
|
1158
|
+
from quantum.core.parsers.functions.return_parser import ReturnParser
|
|
1159
|
+
func_node.add_statement(ReturnParser(self).parse(child))
|
|
1160
|
+
else:
|
|
1161
|
+
# Parse body statements
|
|
1162
|
+
statement = self._parse_statement(child)
|
|
1163
|
+
if statement:
|
|
1164
|
+
func_node.add_statement(statement)
|
|
1165
|
+
|
|
1166
|
+
return func_node
|
|
1167
|
+
|
|
1168
|
+
def _parse_dispatch_event(self, event_element: ET.Element) -> DispatchEventNode:
|
|
1169
|
+
"""Parse q:dispatchEvent statement"""
|
|
1170
|
+
event = event_element.get('event')
|
|
1171
|
+
|
|
1172
|
+
if not event:
|
|
1173
|
+
raise QuantumParseError("dispatchEvent requires 'event' attribute")
|
|
1174
|
+
|
|
1175
|
+
dispatch_node = DispatchEventNode(event)
|
|
1176
|
+
|
|
1177
|
+
# Optional attributes
|
|
1178
|
+
dispatch_node.data = event_element.get('data')
|
|
1179
|
+
dispatch_node.queue = event_element.get('queue')
|
|
1180
|
+
dispatch_node.exchange = event_element.get('exchange')
|
|
1181
|
+
dispatch_node.routing_key = event_element.get('routingKey')
|
|
1182
|
+
dispatch_node.priority = event_element.get('priority', 'normal')
|
|
1183
|
+
dispatch_node.delay = event_element.get('delay')
|
|
1184
|
+
dispatch_node.ttl = event_element.get('ttl')
|
|
1185
|
+
dispatch_node.metadata = event_element.get('metadata')
|
|
1186
|
+
|
|
1187
|
+
return dispatch_node
|
|
1188
|
+
|
|
1189
|
+
def _parse_on_event(self, event_element: ET.Element) -> OnEventNode:
|
|
1190
|
+
"""Parse q:onEvent handler"""
|
|
1191
|
+
event = event_element.get('event')
|
|
1192
|
+
|
|
1193
|
+
if not event:
|
|
1194
|
+
raise QuantumParseError("onEvent requires 'event' attribute")
|
|
1195
|
+
|
|
1196
|
+
event_node = OnEventNode(event)
|
|
1197
|
+
|
|
1198
|
+
# Optional attributes
|
|
1199
|
+
event_node.queue = event_element.get('queue')
|
|
1200
|
+
|
|
1201
|
+
max_retries_attr = event_element.get('maxRetries')
|
|
1202
|
+
if max_retries_attr:
|
|
1203
|
+
try:
|
|
1204
|
+
event_node.max_retries = int(max_retries_attr)
|
|
1205
|
+
except ValueError:
|
|
1206
|
+
pass
|
|
1207
|
+
|
|
1208
|
+
event_node.retry_delay = event_element.get('retryDelay')
|
|
1209
|
+
event_node.dead_letter = event_element.get('deadLetter')
|
|
1210
|
+
event_node.filter = event_element.get('filter')
|
|
1211
|
+
|
|
1212
|
+
concurrent_attr = event_element.get('concurrent')
|
|
1213
|
+
if concurrent_attr:
|
|
1214
|
+
try:
|
|
1215
|
+
event_node.concurrent = int(concurrent_attr)
|
|
1216
|
+
except ValueError:
|
|
1217
|
+
pass
|
|
1218
|
+
|
|
1219
|
+
prefetch_attr = event_element.get('prefetch')
|
|
1220
|
+
if prefetch_attr:
|
|
1221
|
+
try:
|
|
1222
|
+
event_node.prefetch = int(prefetch_attr)
|
|
1223
|
+
except ValueError:
|
|
1224
|
+
pass
|
|
1225
|
+
|
|
1226
|
+
event_node.timeout = event_element.get('timeout')
|
|
1227
|
+
|
|
1228
|
+
# Parse event handler body
|
|
1229
|
+
for child in event_element:
|
|
1230
|
+
statement = self._parse_statement(child)
|
|
1231
|
+
if statement:
|
|
1232
|
+
event_node.add_statement(statement)
|
|
1233
|
+
|
|
1234
|
+
return event_node
|
|
1235
|
+
|