Skip to content

feat: support legacy microflow call web service statement#334

Merged
ako merged 10 commits intomendixlabs:mainfrom
hjotha:submit/microflow-call-web-service-statement
Apr 30, 2026
Merged

feat: support legacy microflow call web service statement#334
ako merged 10 commits intomendixlabs:mainfrom
hjotha:submit/microflow-call-web-service-statement

Conversation

@hjotha
Copy link
Copy Markdown
Contributor

@hjotha hjotha commented Apr 27, 2026

Part of #332.

Summary

  • add structured call web service MDL syntax for legacy SOAP Microflows$CallWebServiceAction
  • preserve unsupported SOAP action payloads through a raw base64 BSON escape hatch
  • resolve known imported SOAP service and send/receive mapping IDs to Module.Document names during describe, while keeping raw IDs as fallback
  • add proposal docs, quick-reference entry, doctype example, LSP completions, and skill guidance

Validation

  • make build
  • ./bin/mxcli check mdl-examples/doctype-tests/call_web_service.test.mdl
  • make lint-go
  • make test
  • make test-integration

Copy link
Copy Markdown
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #334 Review — feat: support legacy microflow call web service statement

Overview

Large but well-structured PR. Adds full MDL round-trip support for legacy SOAP CallWebServiceAction, using a dual-mode design: a structured form that resolves/emits qualified Module.Document names, and a raw base64 escape hatch for opaque byte-for-byte preservation of unsupported SOAP payloads.

Full-stack checklist

Layer Status
Grammar (tokens + rule) WEB, RAW, RECEIVE added; rule in keyword list
Parser regenerated Yes
AST node CallWebServiceStmt with all fields
Visitor Index-based STRING_LITERAL parsing; both structured and raw forms
Builder addCallWebServiceAction + reference resolution helpers
Formatter formatWebServiceCallAction; ID→QName resolution; error suffix via getActionErrorHandlingType
Parser (BSON) parseMicroflowActionValue added to preserve primitive.D ordering; parseWebServiceCallActionFromD stores RawBSON
Writer (BSON) serializeWebServiceCallAction; RawBSON takes priority
Tests Visitor, builder, formatter, BSON parser/writer all covered

Issues

1. Magic value ParameterMappings: bson.A{int32(2)} needs explanation or verification

In serializeWebServiceCallAction:

{Key: "ParameterMappings", Value: bson.A{int32(2)}},

A single integer 2 in a BSON array is unusual. If this comes from actual Studio Pro-generated BSON, it needs a comment explaining what the value represents. If it is a placeholder, it may cause Studio Pro to reject the document. This should be verified against a real CallWebServiceAction exported from Studio Pro.

2. canonicalRawBSON sorts keys, breaking byte-for-byte identity in the MDL text

The function sorts BSON document keys alphabetically before encoding the base64 payload in the MDL text. This means describe → exec → describe produces a different base64 string than the original if the original had non-alphabetical key order. The write path is fine (RawBSON is preserved as-is when written to disk), but the MDL text diverges from the original, which contradicts the stated goal of "lossless round-trip". A comment explaining that canonical sorting is intentional for MDL text stability (not for disk output) would help future readers.

3. Visitor index-based STRING_LITERAL parsing is fragile

literals := callCtx.AllSTRING_LITERAL()
idx := 0
// ...advances idx conditionally for each clause

This works because the grammar enforces clause ordering (OPERATION before SEND before RECEIVE), but a grammar ambiguity fix or reorder would silently break it. Consider extracting each literal by walking the specific grammar context token (e.g., using child context rather than positional index), or at minimum add a comment that the ordering depends on the grammar production order.

4. resolveWebServiceRefForWrite name fallback skips qualified-name check

if name == ref {
    return string(unit.ID)
}

This fallback (after the GetQualifiedName check) matches on bare Name without module qualification. If two modules have a web service with the same short name, the first match wins regardless of module. The fallback exists for when hierarchy is unavailable, but a comment acknowledging the ambiguity risk would help.

5. LSP completion label for RECEIVE is "REST keyword"

RECEIVE is added under the REST keyword group in lsp_completions_gen.go. It is also used for SOAP now. Not a functional issue but might mislead users browsing completions.

Nit

In addCallWebServiceAction, ErrorHandlingType is set on both the action and the activity.BaseActivity. This is consistent with the existing pattern for other actions (e.g., RestCallAction) but is worth noting as known redundancy.

Summary

The design is sound and the test coverage is thorough. The main concern before merging is the ParameterMappings: bson.A{int32(2)} value — this needs to be verified against actual Studio Pro-generated BSON and documented if it is correct. The canonical BSON sorting and index-based visitor are acceptable with added comments.

hjotha pushed a commit to hjotha/mxcli that referenced this pull request Apr 30, 2026
The legacy SOAP writer had a misleading comment claiming the leading
`int32(2)` of an empty ParameterMappings array was a "declared count".
It is actually the Mendix storage-list-type marker — the same
constant used by every other empty list writer in this file (see
PR mendixlabs#338 for showpage and PR mendixlabs#374 for change-action items).

Addresses ako's review on PR mendixlabs#334.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
hjothamendix and others added 4 commits April 30, 2026 10:35
Symptom: legacy SOAP CallWebServiceAction activities could not be represented as executable MDL, so describe/exec round-trips either dropped the action or had to keep it as an unsupported comment.

Root cause: the microflow AST, grammar, visitor, formatter, graph builder, and MPR parser/writer had no structured representation for Microflows$CallWebServiceAction and no raw escape hatch for version-specific SOAP payload fields.

Fix: add structured call web service syntax, raw base64 BSON preservation, qualified-name resolution for imported SOAP services and send/receive mappings during describe, writer support for structured and raw actions, docs, a doctype example, LSP completions, and microflow skill guidance.

Tests: covered parser/visitor, graph builder, formatter reference resolution and raw fallback, MPR raw BSON preservation, and validated with make build, mxcli check for call_web_service.test.mdl, make lint-go, make test, and make test-integration.
…oject

The unqualified `returns Object` in the original doctype test caused
`entity '.Object' not found` when executed. Adds a synthetic
SampleSOAP.OrderResponse entity and uses it as the return type so the
script runs cleanly against a fresh Mendix project. Drops the
`$Root =` assignment from the raw-base64 case since the encoded BSON
has no OutputVariable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Symptom: review flagged several CALL WEB SERVICE implementation details whose
correctness depended on non-obvious BSON and grammar constraints.

Root cause: the code encoded those constraints directly without explaining why
empty SOAP parameter mappings use the marker array, why raw BSON is canonicalized
for MDL text, why string literals are consumed positionally, or why bare service
name fallback is ambiguous.

Fix: add focused comments at the writer, formatter, visitor, and builder sites
that depend on those constraints. There is no behavior change.

Tests: make build; focused web-service tests; mxcli check on the web-service
doctype fixture; make test.
The legacy SOAP writer had a misleading comment claiming the leading
`int32(2)` of an empty ParameterMappings array was a "declared count".
It is actually the Mendix storage-list-type marker — the same
constant used by every other empty list writer in this file (see
PR mendixlabs#338 for showpage and PR mendixlabs#374 for change-action items).

Addresses ako's review on PR mendixlabs#334.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@hjotha hjotha force-pushed the submit/microflow-call-web-service-statement branch from 6ba80eb to 9095513 Compare April 30, 2026 08:37
Symptom: the generated LSP completion entry for RECEIVE still described the
keyword as REST-only after CALL WEB SERVICE started reusing it for legacy SOAP
mapping clauses.

Root cause: completion categories come from lexer sections, and RECEIVE lives
in the REST keyword section even though the token is now shared across REST and
SOAP service syntax.

Fix: override the generated category for RECEIVE to "Service keyword" while
leaving the rest of the REST section unchanged.

Tests: make build
Tests: make test
Tests: make lint-go
@hjotha
Copy link
Copy Markdown
Contributor Author

hjotha commented Apr 30, 2026

Review follow-up status:

  • Documented the ParameterMappings: bson.A{int32(2)} storage-list marker in the writer.
  • Added comments for canonical raw-BSON output and positional STRING_LITERAL parsing.
  • Documented the ambiguous bare-name fallback in resolveWebServiceRefForWrite.
  • Updated the generated LSP completion category for RECEIVE from REST-only to Service keyword because it is shared by REST and legacy SOAP syntax.

Local validation on the updated head:

make build
make test
make lint-go

Symptom: roundtripping a legacy SOAP call with simple request parameter mappings produced a model that mx check rejected because the reconstructed action dropped SOAP-specific request metadata.

Root cause: parsed CallWebServiceAction values from map-backed BSON were treated as fully representable by the structured MDL syntax, even when they contained request/body/header fields that the syntax cannot author yet.

Fix: keep qualified service references as authored and fall back to raw BSON when a parsed SOAP action contains unsupported fields, preserving the full action payload through exec.

Tests: added parser coverage for unsupported SOAP request details, updated the builder expectation, ran make build, make test, and a targeted roundtrip with mx check.
Copy link
Copy Markdown
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Moderate: doctype example won't run in CI

mdl-examples/doctype-tests/call_web_service.test.mdl uses a .test.mdl suffix, which causes TestMxCheck_DoctypeScripts to skip it (the test suite only picks up plain .mdl files). The syntax won't be exercised by the automated doctype CI pass at all.

Fix: rename to call_web_service.mdl (or add a companion plain-.mdl file that just exercises the syntax without requiring a live project).

Relevant test filter (in roundtrip_*_test.go / TestMxCheck_DoctypeScripts):

// .test.mdl and .tests.mdl files are skipped — only plain .mdl gets CI coverage

Moderate: STRING_LITERAL (single-quoted) for Module.Document names is inconsistent with MDL conventions

Every other MDL command that references a document uses a bare qualifiedName token:

call microflow MyModule.MF_DoSomething(...)
show page MyModule.HomePage
describe page MyModule.HomePage
grant execute on microflow MyModule.MF_DoSomething to MyModule.User

This PR introduces a new pattern where Module.Document identifiers are embedded inside STRING_LITERAL tokens (single-quoted strings):

call web service 'SampleSOAP.OrderService'
    operation 'SampleSOAP.Operation_SendOrder'
    send mapping 'SampleSOAP.RequestMapping'
    receive mapping 'SampleSOAP.ResponseMapping'

The DESCRIBE formatter calls mdlQuote(resolveWebServiceReference(...)), which always produces '...' single-quoted output. The skill and quick-reference docs faithfully reproduce this single-quote style.

Concerns:

  1. Inconsistency — readers and LLMs working from examples elsewhere in the docs will expect bare Module.Document identifiers, not single-quoted strings.
  2. Roundtrip brittlenessmdlQuote() escapes apostrophes as '' (Mendix expression style). A module or document name containing an apostrophe would serialize incorrectly.
  3. Future friction — if the grammar is ever extended to accept qualifiedName here (for consistency), the roundtrip format changes and existing scripts break.

Recommended fix (or at minimum, document the design choice explicitly):

Option A — change the grammar to accept qualifiedName for the structured form and keep STRING_LITERAL only for the raw legacy ID fallback:

callWebServiceStatement
    : (VARIABLE EQUALS)? CALL WEB SERVICE
      (RAW STRING_LITERAL
      | qualifiedName                            -- service as Module.Document
        (OPERATION qualifiedName)?
        (SEND MAPPING qualifiedName)?
        (RECEIVE MAPPING qualifiedName)?
        (TIMEOUT expression)?)
      onErrorClause?
    ;

Option B — if STRING_LITERAL is intentional (e.g. because service names can contain characters invalid in identifiers), add a comment in the grammar and a note in the skill explaining why this departs from the convention.


Positive notes

  • The visitor's positional STRING_LITERAL walk is correct — it uses keyword token presence (OPERATION(), SEND(), RECEIVE()) to advance the index, which safely handles optional clauses without ambiguity.
  • resolveWebServiceReference / resolveWebServiceMappingReference correctly look up module-qualified names from imported service metadata, with raw-ID fallback for unresolved cases.
  • The raw BSON base64 escape hatch for unsupported SOAP payload types is a pragmatic choice that avoids data loss on roundtrip.

Symptom: the call web service doctype example used a skipped .test.mdl suffix, and the grammar/docs did not explain why structured SOAP references are quoted strings.

Root cause: legacy SOAP support intentionally accepts both Module.Document text and raw dangling IDs, but that design choice was only implicit in examples.

Fix: rename the doctype example to a checked .mdl fixture and document the STRING_LITERAL reference choice in the grammar, skill, proposal, and quick reference.

Tests: ran make build, mxcli check on the doctype fixture, and make test.
@hjotha
Copy link
Copy Markdown
Contributor Author

hjotha commented Apr 30, 2026

Review follow-up status:

  • Renamed the doctype example to mdl-examples/doctype-tests/call_web_service.mdl so it is picked up by the doctype CI scan.
  • Documented why structured SOAP references remain quoted string literals instead of bare qualifiedName tokens: unresolved legacy IDs and SOAP document names can contain text that is not a valid MDL identifier, so quoted references are the stable authoring/fallback form.
  • Added comments for the storage-list marker in ParameterMappings, canonical raw BSON text stability, and the shared RECEIVE completion category.

Local validation on the updated branch:

make build
./bin/mxcli check mdl-examples/doctype-tests/call_web_service.mdl
make test

The current build-and-test failure is the repository nanoflow integration baseline fixed separately by #412; it is not specific to this PR.

@ako ako added this to the v0.8.0 milestone Apr 30, 2026
Copy link
Copy Markdown
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on STRING_LITERAL justification

The updated remark says: "SOAP document names can contain text that is not a valid MDL identifier, so quoted references are the stable authoring/fallback form."

But qualifiedName already handles that case — identifierOrKeyword accepts QUOTED_IDENTIFIER (double-quoted "..." or backtick form):

qualifiedName
    : identifierOrKeyword (DOT identifierOrKeyword)*
    ;

identifierOrKeyword
    : IDENTIFIER
    | QUOTED_IDENTIFIER   // "double-quoted" or `backtick` — already in the grammar
    | keyword
    ;

So a service name with characters that aren't valid bare identifiers could be expressed as:

call web service "My Module"."My SOAP Service"
    operation "My Module"."My_Operation"

The only case where STRING_LITERAL is genuinely necessary is a raw unresolved GUID/ID like {f3a7c8b2-...} — but that's already handled by the separate RAW STRING_LITERAL path.

For the structured (resolved) form, Mendix module and document names are always valid Mendix identifiers (alphanumeric + underscore), so they would work as bare qualifiedName without quoting. The "invalid identifier characters" argument doesn't apply to the structured path, only to the raw fallback that already has its own grammar alternative.

Symptom: review noted that structured `call web service` references were
always emitted as string literals even when they were normal `Module.Document`
references.

Root cause: the grammar only accepted STRING_LITERAL references, so the
formatter had to quote resolved SOAP services, operations, and mappings.

Fix: add a webServiceReference rule that accepts qualifiedName for resolved
references and STRING_LITERAL only as a dangling raw-ID fallback. Formatter now
emits bare qualified refs when possible and quoted refs only when necessary.
Docs, examples, proposal, and skill guidance were updated to match.

Tests: regenerate the parser, run focused web-service visitor/formatter tests,
then make build and make test.
# Conflicts:
#	mdl/grammar/parser/MDLParser.interp
#	mdl/grammar/parser/mdl_parser.go
@ako ako merged commit 460e1c0 into mendixlabs:main Apr 30, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants