Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/mxcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"strings"

"github.com/mendixlabs/mxcli/mdl/backend"
mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr"
"github.com/mendixlabs/mxcli/mdl/diaglog"
"github.com/mendixlabs/mxcli/mdl/executor"
"github.com/mendixlabs/mxcli/mdl/repl"
Expand Down Expand Up @@ -194,6 +196,7 @@ func resolveFormat(cmd *cobra.Command, defaultFormat string) string {
func newLoggedExecutor(mode string) (*executor.Executor, *diaglog.Logger) {
logger := diaglog.Init(version, mode)
exec := executor.New(os.Stdout)
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })
exec.SetLogger(logger)
if globalJSONFlag {
exec.SetFormat(executor.FormatJSON)
Expand Down
3 changes: 3 additions & 0 deletions examples/create_datagrid2_page/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"os"
"strings"

"github.com/mendixlabs/mxcli/mdl/backend"
mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr"
"github.com/mendixlabs/mxcli/mdl/executor"
"github.com/mendixlabs/mxcli/mdl/visitor"
)
Expand Down Expand Up @@ -51,6 +53,7 @@ func main() {

// Create the MDL executor with stdout for output
exec := executor.New(os.Stdout)
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })

// Define the MDL script to create a page with DataGrid2
// Note: Adjust module name, entity name, and attributes to match your project
Expand Down
2 changes: 2 additions & 0 deletions mdl/backend/mock/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ type MockBackend struct {
SerializeWidgetToOpaqueFunc func(w pages.Widget) any
SerializeDataSourceToOpaqueFunc func(ds pages.DataSource) any
BuildCreateAttributeObjectFunc func(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error)
BuildDataGrid2WidgetFunc func(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error)
BuildFilterWidgetFunc func(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error)

// AgentEditorBackend
ListAgentEditorModelsFunc func() ([]*agenteditor.Model, error)
Expand Down
20 changes: 18 additions & 2 deletions mdl/backend/mock/mock_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package mock

import (
"fmt"

"github.com/mendixlabs/mxcli/mdl/backend"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/pages"
Expand All @@ -17,7 +19,7 @@ func (m *MockBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator,
if m.OpenPageForMutationFunc != nil {
return m.OpenPageForMutationFunc(unitID)
}
return nil, nil
return nil, fmt.Errorf("MockBackend.OpenPageForMutation not configured")
}

// ---------------------------------------------------------------------------
Expand All @@ -28,7 +30,7 @@ func (m *MockBackend) OpenWorkflowForMutation(unitID model.ID) (backend.Workflow
if m.OpenWorkflowForMutationFunc != nil {
return m.OpenWorkflowForMutationFunc(unitID)
}
return nil, nil
return nil, fmt.Errorf("MockBackend.OpenWorkflowForMutation not configured")
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -94,3 +96,17 @@ func (m *MockBackend) BuildCreateAttributeObject(attributePath string, objectTyp
}
return nil, nil
}

func (m *MockBackend) BuildDataGrid2Widget(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error) {
if m.BuildDataGrid2WidgetFunc != nil {
return m.BuildDataGrid2WidgetFunc(id, name, spec, projectPath)
}
return nil, fmt.Errorf("MockBackend.BuildDataGrid2Widget not configured")
}

func (m *MockBackend) BuildFilterWidget(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error) {
if m.BuildFilterWidgetFunc != nil {
return m.BuildFilterWidgetFunc(spec, projectPath)
}
return nil, fmt.Errorf("MockBackend.BuildFilterWidget not configured")
}
10 changes: 10 additions & 0 deletions mdl/backend/mpr/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ type MprBackend struct {
path string
}

// New creates a new unconnected MprBackend. Call Connect(path) to open a project.
func New() *MprBackend {
return &MprBackend{}
}

// Wrap creates an MprBackend that wraps an existing Writer (and its Reader).
// This is used during migration when the Executor already owns the Writer
// and we want to expose it through the Backend interface without opening
Expand Down Expand Up @@ -75,6 +80,11 @@ func (b *MprBackend) Disconnect() error {
func (b *MprBackend) IsConnected() bool { return b.writer != nil }
func (b *MprBackend) Path() string { return b.path }

// MprReader returns the underlying *mpr.Reader for callers that still
// require direct SDK access (e.g. linter rules). Prefer Backend methods
// for new code.
func (b *MprBackend) MprReader() *mpr.Reader { return b.reader }

func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) }
func (b *MprBackend) ProjectVersion() *types.ProjectVersion { return convertProjectVersion(b.reader.ProjectVersion()) }
func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() }
Expand Down
32 changes: 3 additions & 29 deletions mdl/backend/mpr/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,36 +263,10 @@ func convertNavMenuItem(in *mpr.NavMenuItem) *types.NavMenuItem {
// Conversion helpers: mdl/types -> sdk/mpr (for write methods)
// ---------------------------------------------------------------------------

// unconvertNavProfileSpec is now a pass-through since mpr.NavigationProfileSpec
// is aliased to types.NavigationProfileSpec.
func unconvertNavProfileSpec(s types.NavigationProfileSpec) mpr.NavigationProfileSpec {
out := mpr.NavigationProfileSpec{
LoginPage: s.LoginPage,
NotFoundPage: s.NotFoundPage,
HasMenu: s.HasMenu,
}
if s.HomePages != nil {
out.HomePages = make([]mpr.NavHomePageSpec, len(s.HomePages))
for i, hp := range s.HomePages {
out.HomePages[i] = mpr.NavHomePageSpec{IsPage: hp.IsPage, Target: hp.Target, ForRole: hp.ForRole}
}
}
if s.MenuItems != nil {
out.MenuItems = make([]mpr.NavMenuItemSpec, len(s.MenuItems))
for i, mi := range s.MenuItems {
out.MenuItems[i] = unconvertNavMenuItemSpec(mi)
}
}
return out
}

func unconvertNavMenuItemSpec(in types.NavMenuItemSpec) mpr.NavMenuItemSpec {
out := mpr.NavMenuItemSpec{Caption: in.Caption, Page: in.Page, Microflow: in.Microflow}
if in.Items != nil {
out.Items = make([]mpr.NavMenuItemSpec, len(in.Items))
for i, sub := range in.Items {
out.Items[i] = unconvertNavMenuItemSpec(sub)
}
}
return out
return s
}

func unconvertEntityMemberAccessSlice(in []types.EntityMemberAccess) []mpr.EntityMemberAccess {
Expand Down
55 changes: 53 additions & 2 deletions mdl/backend/mpr/convert_roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package mprbackend

import (
"errors"
"reflect"
"testing"

"github.com/mendixlabs/mxcli/mdl/types"
Expand Down Expand Up @@ -492,7 +493,9 @@ func TestUnconvertNavMenuItemSpec_Isolated(t *testing.T) {
{Caption: "Child", Microflow: "MF2"},
},
}
out := unconvertNavMenuItemSpec(in)
// Since mpr.NavMenuItemSpec is aliased to types.NavMenuItemSpec,
// unconvert is now a pass-through. Verify the alias holds.
var out mpr.NavMenuItemSpec = in
if out.Caption != "Parent" || out.Page != "Page1" || out.Microflow != "MF1" {
t.Errorf("field mismatch: %+v", out)
}
Expand All @@ -503,7 +506,7 @@ func TestUnconvertNavMenuItemSpec_Isolated(t *testing.T) {

func TestUnconvertNavMenuItemSpec_NilItems(t *testing.T) {
in := types.NavMenuItemSpec{Caption: "Leaf"}
out := unconvertNavMenuItemSpec(in)
var out mpr.NavMenuItemSpec = in
if out.Items != nil {
t.Errorf("expected nil Items for leaf: %+v", out.Items)
}
Expand Down Expand Up @@ -598,3 +601,51 @@ func TestUnconvertImageCollection(t *testing.T) {
}
}

// ============================================================================
// Field-count drift assertions
// ============================================================================
//
// These tests catch silent field drift: if a struct gains a new field but
// the convert/unconvert function is not updated, the test fails.

func assertFieldCount(t *testing.T, name string, v any, expected int) {
t.Helper()
actual := reflect.TypeOf(v).NumField()
if actual != expected {
t.Errorf("%s field count changed: expected %d, got %d — update convert.go and this test", name, expected, actual)
}
}

func TestFieldCountDrift(t *testing.T) {
// mpr → types pairs (manually copied in convert.go).
// If a struct gains a field, update the convert function AND this count.
assertFieldCount(t, "mpr.FolderInfo", mpr.FolderInfo{}, 3)
assertFieldCount(t, "types.FolderInfo", types.FolderInfo{}, 3)
assertFieldCount(t, "mpr.UnitInfo", mpr.UnitInfo{}, 4)
assertFieldCount(t, "types.UnitInfo", types.UnitInfo{}, 4)
assertFieldCount(t, "mpr.RenameHit", mpr.RenameHit{}, 4)
assertFieldCount(t, "types.RenameHit", types.RenameHit{}, 4)
assertFieldCount(t, "mpr.RawUnit", mpr.RawUnit{}, 4)
assertFieldCount(t, "types.RawUnit", types.RawUnit{}, 4)
assertFieldCount(t, "mpr.RawUnitInfo", mpr.RawUnitInfo{}, 5)
assertFieldCount(t, "types.RawUnitInfo", types.RawUnitInfo{}, 5)
assertFieldCount(t, "mpr.RawCustomWidgetType", mpr.RawCustomWidgetType{}, 6)
assertFieldCount(t, "types.RawCustomWidgetType", types.RawCustomWidgetType{}, 6)
assertFieldCount(t, "mpr.JavaAction", mpr.JavaAction{}, 4)
assertFieldCount(t, "types.JavaAction", types.JavaAction{}, 4)
assertFieldCount(t, "mpr.JavaScriptAction", mpr.JavaScriptAction{}, 12)
assertFieldCount(t, "types.JavaScriptAction", types.JavaScriptAction{}, 12)
assertFieldCount(t, "mpr.NavigationDocument", mpr.NavigationDocument{}, 4)
assertFieldCount(t, "types.NavigationDocument", types.NavigationDocument{}, 4)
assertFieldCount(t, "mpr.JsonStructure", mpr.JsonStructure{}, 8)
assertFieldCount(t, "types.JsonStructure", types.JsonStructure{}, 8)
assertFieldCount(t, "mpr.JsonElement", mpr.JsonElement{}, 14)
assertFieldCount(t, "types.JsonElement", types.JsonElement{}, 14)
assertFieldCount(t, "mpr.ImageCollection", mpr.ImageCollection{}, 6)
assertFieldCount(t, "types.ImageCollection", types.ImageCollection{}, 6)
assertFieldCount(t, "mpr.EntityMemberAccess", mpr.EntityMemberAccess{}, 3)
assertFieldCount(t, "types.EntityMemberAccess", types.EntityMemberAccess{}, 3)
assertFieldCount(t, "mpr.EntityAccessRevocation", mpr.EntityAccessRevocation{}, 6)
assertFieldCount(t, "types.EntityAccessRevocation", types.EntityAccessRevocation{}, 6)
}

Loading
Loading