Skip to content

Commit ec16ac8

Browse files
akoclaude
andcommitted
Add UnknownElement fallback and table-driven parser registries (issue #19)
Stop silent data loss for unrecognized BSON $Type values. Unknown microflow objects and workflow activities are now preserved as UnknownElement with raw fields and inferred property kinds, instead of being silently dropped. - Add UnknownElement type in model/ with MicroflowObject and WorkflowActivity interface compliance - Add inferPropertyKind and extractBsonArrayWithMarker helpers - Replace switch dispatchers with table-driven registries in parseMicroflowObject, parseMicroflowAction, parseWorkflowActivity - Add writer round-trip for UnknownElement in serializeMicroflowObject Based on proposal and implementation by @engalar (engalar/mxcli@c44fe450). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8f7ad69 commit ec16ac8

6 files changed

Lines changed: 279 additions & 118 deletions

File tree

model/types.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,3 +714,35 @@ type DistributionSettings struct {
714714
IsDistributable bool `json:"isDistributable"`
715715
Version string `json:"version,omitempty"`
716716
}
717+
718+
// UnknownElement is a generic fallback for BSON elements with unrecognized $Type values.
719+
// It preserves all raw BSON fields so developers can diagnose unimplemented types
720+
// without silent data loss.
721+
//
722+
// FieldKinds maps each raw field name to its inferred Mendix property kind
723+
// (e.g. "primitive", "part", "by-name-reference", "collection:part-primary").
724+
// This guides implementors in writing a proper parser without inspecting the
725+
// mendixmodelsdk JS source manually.
726+
type UnknownElement struct {
727+
BaseElement
728+
Position Point `json:"position,omitempty"`
729+
Name string `json:"name,omitempty"`
730+
Caption string `json:"caption,omitempty"`
731+
RawFields map[string]any `json:"-"`
732+
FieldKinds map[string]string `json:"-"`
733+
}
734+
735+
// GetPosition returns the element's position (satisfies microflows.MicroflowObject).
736+
func (u *UnknownElement) GetPosition() Point { return u.Position }
737+
738+
// SetPosition sets the element's position (satisfies microflows.MicroflowObject).
739+
func (u *UnknownElement) SetPosition(p Point) { u.Position = p }
740+
741+
// GetName returns the element's name (satisfies workflows.WorkflowActivity).
742+
func (u *UnknownElement) GetName() string { return u.Name }
743+
744+
// GetCaption returns the element's caption (satisfies workflows.WorkflowActivity).
745+
func (u *UnknownElement) GetCaption() string { return u.Caption }
746+
747+
// ActivityType returns the type name (satisfies workflows.WorkflowActivity).
748+
func (u *UnknownElement) ActivityType() string { return u.TypeName }

sdk/mpr/parser.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package mpr
44

55
import (
66
"encoding/base64"
7+
"strings"
78

89
"go.mongodb.org/mongo-driver/bson/primitive"
910
)
@@ -148,3 +149,105 @@ func extractBsonSlice(v any) []any {
148149
}
149150
return nil
150151
}
152+
153+
// BsonArrayInfo holds the extracted items and the marker from a Mendix BSON array.
154+
type BsonArrayInfo struct {
155+
Marker int32
156+
Items []any
157+
}
158+
159+
// extractBsonArrayWithMarker extracts items from a Mendix BSON array, preserving the marker.
160+
// Returns the marker (1, 2, or 3) and the items after the marker.
161+
func extractBsonArrayWithMarker(v any) BsonArrayInfo {
162+
if v == nil {
163+
return BsonArrayInfo{}
164+
}
165+
166+
var slice []any
167+
switch val := v.(type) {
168+
case primitive.A:
169+
slice = []any(val)
170+
case []any:
171+
slice = val
172+
default:
173+
return BsonArrayInfo{}
174+
}
175+
176+
if len(slice) > 0 {
177+
if marker, ok := slice[0].(int32); ok && (marker == 1 || marker == 2 || marker == 3) {
178+
return BsonArrayInfo{Marker: marker, Items: slice[1:]}
179+
}
180+
}
181+
return BsonArrayInfo{Items: slice}
182+
}
183+
184+
// inferPropertyKind determines the Mendix property kind of a BSON field from its key
185+
// and value shape. Returns one of: "id", "type-discriminator", "by-name-reference",
186+
// "primitive", "part", "collection:by-name" (marker=1), "collection:part-secondary"
187+
// (marker=2), "collection:part-primary" (marker=3), "collection".
188+
// Used by UnknownElement to surface diagnostic info when an unimplemented $Type is encountered.
189+
func inferPropertyKind(key string, v any) string {
190+
if v == nil {
191+
return "primitive"
192+
}
193+
194+
// Key-based shortcuts take priority over value shape.
195+
switch key {
196+
case "$ID", "$ContainerID":
197+
return "id"
198+
case "$Type":
199+
return "type-discriminator"
200+
}
201+
202+
switch val := v.(type) {
203+
case map[string]any:
204+
if _, hasType := val["$Type"]; hasType {
205+
return "part"
206+
}
207+
if _, hasID := val["$ID"]; hasID {
208+
return "part"
209+
}
210+
return "primitive"
211+
212+
case primitive.D:
213+
m := val.Map()
214+
if _, hasType := m["$Type"]; hasType {
215+
return "part"
216+
}
217+
if _, hasID := m["$ID"]; hasID {
218+
return "part"
219+
}
220+
return "primitive"
221+
222+
case primitive.M:
223+
if _, hasType := val["$Type"]; hasType {
224+
return "part"
225+
}
226+
if _, hasID := val["$ID"]; hasID {
227+
return "part"
228+
}
229+
return "primitive"
230+
231+
case primitive.A, []any:
232+
info := extractBsonArrayWithMarker(v)
233+
switch info.Marker {
234+
case 1:
235+
return "collection:by-name"
236+
case 2:
237+
return "collection:part-secondary"
238+
case 3:
239+
return "collection:part-primary"
240+
}
241+
return "collection"
242+
243+
case string:
244+
// Heuristic: qualified names like "Module.Entity" are likely by-name references.
245+
if strings.Contains(val, ".") && !strings.Contains(val, " ") && !strings.Contains(val, "/") {
246+
return "by-name-reference"
247+
}
248+
return "primitive"
249+
250+
default:
251+
return "primitive"
252+
}
253+
}

sdk/mpr/parser_microflow.go

Lines changed: 72 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -245,37 +245,40 @@ func parseMicroflowObjectCollection(raw map[string]any) *microflows.MicroflowObj
245245
return collection
246246
}
247247

248+
// microflowObjectParsers maps Mendix $Type strings to their parser functions.
249+
// Adding support for a new type requires only one new entry here.
250+
// Declared as a nil var and populated in init() so that the map literal can
251+
// reference parseLoopedActivity, which itself calls parseMicroflowObjectCollection,
252+
// keeping the package-level initialization order unambiguous.
253+
var microflowObjectParsers map[string]func(map[string]any) microflows.MicroflowObject
254+
255+
func init() {
256+
microflowObjectParsers = map[string]func(map[string]any) microflows.MicroflowObject{
257+
"Microflows$StartEvent": func(r map[string]any) microflows.MicroflowObject { return parseStartEvent(r) },
258+
"Microflows$EndEvent": func(r map[string]any) microflows.MicroflowObject { return parseEndEvent(r) },
259+
"Microflows$ErrorEvent": func(r map[string]any) microflows.MicroflowObject { return parseErrorEvent(r) },
260+
"Microflows$ActionActivity": func(r map[string]any) microflows.MicroflowObject { return parseActionActivity(r) },
261+
"Microflows$ExclusiveSplit": func(r map[string]any) microflows.MicroflowObject { return parseExclusiveSplit(r) },
262+
"Microflows$ExclusiveMerge": func(r map[string]any) microflows.MicroflowObject { return parseExclusiveMerge(r) },
263+
"Microflows$InheritanceSplit": func(r map[string]any) microflows.MicroflowObject { return parseInheritanceSplit(r) },
264+
"Microflows$LoopedActivity": func(r map[string]any) microflows.MicroflowObject { return parseLoopedActivity(r) },
265+
"Microflows$BreakEvent": func(r map[string]any) microflows.MicroflowObject { return parseBreakEvent(r) },
266+
"Microflows$ContinueEvent": func(r map[string]any) microflows.MicroflowObject { return parseContinueEvent(r) },
267+
"Microflows$Annotation": func(r map[string]any) microflows.MicroflowObject { return parseMicroflowAnnotation(r) },
268+
}
269+
}
270+
248271
// parseMicroflowObject parses a single microflow object based on its $Type.
272+
// Returns nil for elements with an empty $Type (corrupt or placeholder records).
249273
func parseMicroflowObject(raw map[string]any) microflows.MicroflowObject {
250274
typeName, _ := raw["$Type"].(string)
251-
252-
switch typeName {
253-
case "Microflows$StartEvent":
254-
return parseStartEvent(raw)
255-
case "Microflows$EndEvent":
256-
return parseEndEvent(raw)
257-
case "Microflows$ErrorEvent":
258-
return parseErrorEvent(raw)
259-
case "Microflows$ActionActivity":
260-
return parseActionActivity(raw)
261-
case "Microflows$ExclusiveSplit":
262-
return parseExclusiveSplit(raw)
263-
case "Microflows$ExclusiveMerge":
264-
return parseExclusiveMerge(raw)
265-
case "Microflows$InheritanceSplit":
266-
return parseInheritanceSplit(raw)
267-
case "Microflows$LoopedActivity":
268-
return parseLoopedActivity(raw)
269-
case "Microflows$BreakEvent":
270-
return parseBreakEvent(raw)
271-
case "Microflows$ContinueEvent":
272-
return parseContinueEvent(raw)
273-
case "Microflows$Annotation":
274-
return parseMicroflowAnnotation(raw)
275-
default:
276-
// Unknown type - return nil or a generic object
275+
if typeName == "" {
277276
return nil
278277
}
278+
if fn, ok := microflowObjectParsers[typeName]; ok {
279+
return fn(raw)
280+
}
281+
return newUnknownObject(typeName, raw)
279282
}
280283

281284
func parseStartEvent(raw map[string]any) *microflows.StartEvent {
@@ -447,85 +450,68 @@ func parseActionActivity(raw map[string]any) *microflows.ActionActivity {
447450
return activity
448451
}
449452

450-
// parseMicroflowAction parses a microflow action based on its $Type.
451-
func parseMicroflowAction(raw map[string]any) microflows.MicroflowAction {
452-
typeName, _ := raw["$Type"].(string)
453-
454-
switch typeName {
453+
// microflowActionParsers maps Mendix $Type strings to their action parser functions.
454+
// Storage names (e.g. CreateChangeAction) and qualified names (e.g. CreateObjectAction)
455+
// both map to the same parser to handle BSON format variations.
456+
var microflowActionParsers = map[string]func(map[string]any) microflows.MicroflowAction{
455457
// Variable actions
456-
case "Microflows$CreateVariableAction":
457-
return parseCreateVariableAction(raw)
458-
case "Microflows$ChangeVariableAction":
459-
return parseChangeVariableAction(raw)
458+
"Microflows$CreateVariableAction": func(r map[string]any) microflows.MicroflowAction { return parseCreateVariableAction(r) },
459+
"Microflows$ChangeVariableAction": func(r map[string]any) microflows.MicroflowAction { return parseChangeVariableAction(r) },
460460

461461
// Object actions (storageName may differ from qualifiedName)
462-
case "Microflows$CreateObjectAction", "Microflows$CreateChangeAction":
463-
return parseCreateObjectAction(raw)
464-
case "Microflows$ChangeObjectAction", "Microflows$ChangeAction":
465-
return parseChangeObjectAction(raw)
466-
case "Microflows$DeleteAction":
467-
return parseDeleteAction(raw)
468-
case "Microflows$CommitAction":
469-
return parseCommitAction(raw)
470-
case "Microflows$RollbackAction":
471-
return parseRollbackAction(raw)
462+
"Microflows$CreateObjectAction": func(r map[string]any) microflows.MicroflowAction { return parseCreateObjectAction(r) },
463+
"Microflows$CreateChangeAction": func(r map[string]any) microflows.MicroflowAction { return parseCreateObjectAction(r) },
464+
"Microflows$ChangeObjectAction": func(r map[string]any) microflows.MicroflowAction { return parseChangeObjectAction(r) },
465+
"Microflows$ChangeAction": func(r map[string]any) microflows.MicroflowAction { return parseChangeObjectAction(r) },
466+
"Microflows$DeleteAction": func(r map[string]any) microflows.MicroflowAction { return parseDeleteAction(r) },
467+
"Microflows$CommitAction": func(r map[string]any) microflows.MicroflowAction { return parseCommitAction(r) },
468+
"Microflows$RollbackAction": func(r map[string]any) microflows.MicroflowAction { return parseRollbackAction(r) },
472469

473470
// Retrieve actions
474-
case "Microflows$RetrieveAction":
475-
return parseRetrieveAction(raw)
476-
case "Microflows$AggregateListAction", "Microflows$AggregateAction": // AggregateAction is storageName
477-
return parseAggregateListAction(raw)
471+
"Microflows$RetrieveAction": func(r map[string]any) microflows.MicroflowAction { return parseRetrieveAction(r) },
472+
"Microflows$AggregateListAction": func(r map[string]any) microflows.MicroflowAction { return parseAggregateListAction(r) },
473+
"Microflows$AggregateAction": func(r map[string]any) microflows.MicroflowAction { return parseAggregateListAction(r) },
478474

479475
// List actions
480-
case "Microflows$CreateListAction":
481-
return parseCreateListAction(raw)
482-
case "Microflows$ChangeListAction":
483-
return parseChangeListAction(raw)
484-
case "Microflows$ListOperationAction", "Microflows$ListOperationsAction": // ListOperationsAction is storageName
485-
return parseListOperationAction(raw)
476+
"Microflows$CreateListAction": func(r map[string]any) microflows.MicroflowAction { return parseCreateListAction(r) },
477+
"Microflows$ChangeListAction": func(r map[string]any) microflows.MicroflowAction { return parseChangeListAction(r) },
478+
"Microflows$ListOperationAction": func(r map[string]any) microflows.MicroflowAction { return parseListOperationAction(r) },
479+
"Microflows$ListOperationsAction": func(r map[string]any) microflows.MicroflowAction { return parseListOperationAction(r) },
486480

487481
// Integration actions
488-
case "Microflows$MicroflowCallAction":
489-
return parseMicroflowCallAction(raw)
490-
case "Microflows$JavaActionCallAction":
491-
return parseJavaActionCallAction(raw)
492-
case "Microflows$CallExternalAction":
493-
return parseCallExternalAction(raw)
494-
495-
// Client actions
496-
case "Microflows$ShowFormAction", "Microflows$ShowPageAction": // ShowFormAction is storageName
497-
return parseShowPageAction(raw)
498-
case "Microflows$ShowHomePageAction":
499-
return parseShowHomePageAction(raw)
500-
case "Microflows$CloseFormAction":
501-
return parseClosePageAction(raw)
502-
case "Microflows$ShowMessageAction":
503-
return parseShowMessageAction(raw)
504-
case "Microflows$ValidationFeedbackAction":
505-
return parseValidationFeedbackAction(raw)
506-
case "Microflows$DownloadFileAction":
507-
return parseDownloadFileAction(raw)
482+
"Microflows$MicroflowCallAction": func(r map[string]any) microflows.MicroflowAction { return parseMicroflowCallAction(r) },
483+
"Microflows$JavaActionCallAction": func(r map[string]any) microflows.MicroflowAction { return parseJavaActionCallAction(r) },
484+
"Microflows$CallExternalAction": func(r map[string]any) microflows.MicroflowAction { return parseCallExternalAction(r) },
485+
486+
// Client actions (ShowFormAction is storageName for ShowPageAction)
487+
"Microflows$ShowFormAction": func(r map[string]any) microflows.MicroflowAction { return parseShowPageAction(r) },
488+
"Microflows$ShowPageAction": func(r map[string]any) microflows.MicroflowAction { return parseShowPageAction(r) },
489+
"Microflows$ShowHomePageAction": func(r map[string]any) microflows.MicroflowAction { return parseShowHomePageAction(r) },
490+
"Microflows$CloseFormAction": func(r map[string]any) microflows.MicroflowAction { return parseClosePageAction(r) },
491+
"Microflows$ShowMessageAction": func(r map[string]any) microflows.MicroflowAction { return parseShowMessageAction(r) },
492+
"Microflows$ValidationFeedbackAction": func(r map[string]any) microflows.MicroflowAction { return parseValidationFeedbackAction(r) },
493+
"Microflows$DownloadFileAction": func(r map[string]any) microflows.MicroflowAction { return parseDownloadFileAction(r) },
508494

509495
// Log action
510-
case "Microflows$LogMessageAction":
511-
return parseLogMessageAction(raw)
496+
"Microflows$LogMessageAction": func(r map[string]any) microflows.MicroflowAction { return parseLogMessageAction(r) },
512497

513498
// Cast action
514-
case "Microflows$CastAction":
515-
return parseCastAction(raw)
499+
"Microflows$CastAction": func(r map[string]any) microflows.MicroflowAction { return parseCastAction(r) },
516500

517501
// REST call action
518-
case "Microflows$RestCallAction":
519-
return parseRestCallAction(raw)
502+
"Microflows$RestCallAction": func(r map[string]any) microflows.MicroflowAction { return parseRestCallAction(r) },
520503

521504
// Database Connector action
522-
case "DatabaseConnector$ExecuteDatabaseQueryAction":
523-
return parseExecuteDatabaseQueryAction(raw)
505+
"DatabaseConnector$ExecuteDatabaseQueryAction": func(r map[string]any) microflows.MicroflowAction { return parseExecuteDatabaseQueryAction(r) },
506+
}
524507

525-
default:
526-
// Return an unknown action placeholder with the type name
527-
return &microflows.UnknownAction{TypeName: typeName}
508+
// parseMicroflowAction parses a microflow action based on its $Type.
509+
func parseMicroflowAction(raw map[string]any) microflows.MicroflowAction {
510+
typeName, _ := raw["$Type"].(string)
511+
if fn, ok := microflowActionParsers[typeName]; ok {
512+
return fn(raw)
528513
}
514+
return &microflows.UnknownAction{TypeName: typeName}
529515
}
530516

531517
func parseCreateVariableAction(raw map[string]any) *microflows.CreateVariableAction {

sdk/mpr/parser_unknown.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package mpr
4+
5+
import "github.com/mendixlabs/mxcli/model"
6+
7+
// newUnknownObject creates an UnknownElement that preserves raw BSON fields
8+
// for unrecognized $Type values, preventing silent data loss.
9+
// FieldKinds is populated by inferPropertyKind so callers can see the inferred
10+
// Mendix property kind for each field without inspecting the SDK JS source.
11+
func newUnknownObject(typeName string, raw map[string]any) *model.UnknownElement {
12+
id := ""
13+
if raw != nil {
14+
id = extractBsonID(raw["$ID"])
15+
}
16+
elem := &model.UnknownElement{
17+
BaseElement: model.BaseElement{ID: model.ID(id), TypeName: typeName},
18+
RawFields: raw,
19+
}
20+
if raw != nil {
21+
elem.Position = parsePoint(raw["RelativeMiddlePoint"])
22+
elem.Name = extractString(raw["Name"])
23+
elem.Caption = extractString(raw["Caption"])
24+
elem.FieldKinds = make(map[string]string, len(raw))
25+
for k, v := range raw {
26+
elem.FieldKinds[k] = inferPropertyKind(k, v)
27+
}
28+
}
29+
return elem
30+
}

0 commit comments

Comments
 (0)