Skip to content

Commit da6f250

Browse files
committed
Merge branch 'issues'
2 parents 99e33aa + 97cab9b commit da6f250

8 files changed

Lines changed: 180 additions & 1 deletion

File tree

.claude/commands/mxcli-dev/review.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ proactively. Add a row after every review that surfaces something new.
3232
| 7 | Skill/doc table references a function that doesn't exist (e.g. `formatActionStatement()` vs `formatAction()`) | Docs quality | Grep function names before writing: `grep -r "func formatA" mdl/executor/` |
3333
| 8 | "Always X" rule is too absolute for trivial edge cases (e.g. "always write failing test first" for one-char typos) | Docs quality | Soften to "prefer X" or add an exception clause; include the reasoning so readers can judge edge cases |
3434
| 9 | Doc comment promises a fallback/feature that doesn't exist in the code (e.g., "raw-map fallback in the client" when no such fallback was implemented) | Docs quality | Grep for function/type names referenced in doc comments to confirm they exist before committing |
35+
| 10 | BSON array items decoded by mongo driver are `primitive.D`, not `map[string]any` — bare type assertion `item.(map[string]any)` always fails silently, causing silent data loss (e.g. Languages not parsed, issue #480) | BSON parsing | Always use `extractBsonMap(item)` instead of `item.(map[string]any)`; write a parser unit test with `primitive.D` items to catch this class of bug |
36+
| 11 | `execShow` switch missing a case for a new `ShowXxx` constant — executor handler is wired but never dispatched, command silently does nothing | Dispatch gap | After adding a new `Show*` constant and handler, grep `executor_query.go` to confirm the case is present; add a mock test that calls the handler directly |
3537

3638
---
3739

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ create or replace navigation Responsive
457457
| Drop configuration | `drop configuration 'Name';` | Remove a configuration |
458458
| Alter language | `alter settings LANGUAGE key = value;` | DefaultLanguageCode |
459459
| Alter workflows | `alter settings workflows key = value;` | UserEntity, DefaultTaskParallelism |
460+
| List languages | `show languages;` | Lists language codes with translation string counts (requires `refresh catalog full`) |
460461

461462
## Business Events
462463

mdl-examples/doctype-tests/14-project-settings-examples.mdl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,6 @@ alter settings workflows UserEntity = 'System.User';
145145
-- ALTER SETTINGS LANGUAGE
146146
-- ALTER SETTINGS WORKFLOWS
147147
-- SHOW SETTINGS / DESCRIBE SETTINGS (read-only)
148+
-- SHOW LANGUAGES (lists language codes with string counts; requires refresh catalog full)
148149
--
149150
-- ============================================================================
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/catalog"
9+
)
10+
11+
func TestListLanguages_NilCatalog(t *testing.T) {
12+
ctx, _ := newMockCtx(t)
13+
// Catalog is nil by default in newMockCtx
14+
err := listLanguages(ctx)
15+
assertError(t, err)
16+
}
17+
18+
func TestListLanguages_EmptyStringsTable(t *testing.T) {
19+
cat, err := catalog.New()
20+
if err != nil {
21+
t.Fatalf("failed to create catalog: %v", err)
22+
}
23+
defer cat.Close()
24+
25+
ctx, buf := newMockCtx(t)
26+
ctx.Catalog = cat
27+
28+
assertNoError(t, listLanguages(ctx))
29+
assertContainsStr(t, buf.String(), "No translatable strings found")
30+
}
31+
32+
func TestListLanguages_WithRows(t *testing.T) {
33+
cat, err := catalog.New()
34+
if err != nil {
35+
t.Fatalf("failed to create catalog: %v", err)
36+
}
37+
defer cat.Close()
38+
39+
db := cat.CatalogDB()
40+
_, err = db.Exec(`INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ElementId, ModuleName) VALUES (?, ?, ?, ?, ?, ?, ?)`,
41+
"MyModule.HomePage", "Page", "Hello", "Caption", "en_US", "id1", "MyModule")
42+
if err != nil {
43+
t.Fatalf("failed to seed strings table: %v", err)
44+
}
45+
_, err = db.Exec(`INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ElementId, ModuleName) VALUES (?, ?, ?, ?, ?, ?, ?)`,
46+
"MyModule.HomePage", "Page", "Bonjour", "Caption", "fr_FR", "id2", "MyModule")
47+
if err != nil {
48+
t.Fatalf("failed to seed strings table: %v", err)
49+
}
50+
51+
ctx, buf := newMockCtx(t)
52+
ctx.Catalog = cat
53+
54+
assertNoError(t, listLanguages(ctx))
55+
out := buf.String()
56+
assertContainsStr(t, out, "en_US")
57+
assertContainsStr(t, out, "fr_FR")
58+
assertContainsStr(t, out, "Language")
59+
}

mdl/executor/executor_query.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ func execShow(ctx *ExecContext, s *ast.ShowStmt) error {
105105
return listBusinessEvents(ctx, s.InModule)
106106
case ast.ShowSettings:
107107
return listSettings(ctx)
108+
case ast.ShowLanguages:
109+
return listLanguages(ctx)
108110
case ast.ShowFragments:
109111
return listFragments(ctx)
110112
case ast.ShowDatabaseConnections:

model/types.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,19 @@ type ConventionSettings struct {
815815
// LanguageSettings represents Settings$LanguageSettings.
816816
type LanguageSettings struct {
817817
BaseElement
818-
DefaultLanguageCode string `json:"defaultLanguageCode,omitempty"`
818+
DefaultLanguageCode string `json:"defaultLanguageCode,omitempty"`
819+
Languages []Language `json:"languages,omitempty"`
820+
}
821+
822+
// Language represents a Texts$Language entry in the project language settings.
823+
// The Languages slice is populated by parseLanguageSettings and is available
824+
// for use by settings describers and future language-aware commands.
825+
type Language struct {
826+
Code string `json:"code"`
827+
CheckCompleteness bool `json:"checkCompleteness,omitempty"`
828+
CustomDateFormat string `json:"customDateFormat,omitempty"`
829+
CustomDateTimeFormat string `json:"customDateTimeFormat,omitempty"`
830+
CustomTimeFormat string `json:"customTimeFormat,omitempty"`
819831
}
820832

821833
// CertificateSettings represents Settings$CertificateSettings.

sdk/mpr/parser_settings.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ func parseLanguageSettings(raw map[string]any) *model.LanguageSettings {
173173
ls.ID = model.ID(extractBsonID(raw["$ID"]))
174174
ls.TypeName = extractString(raw["$Type"])
175175
ls.DefaultLanguageCode = extractString(raw["DefaultLanguageCode"])
176+
for _, item := range extractBsonArray(raw["Languages"]) {
177+
langMap := extractBsonMap(item)
178+
if langMap == nil {
179+
continue
180+
}
181+
ls.Languages = append(ls.Languages, model.Language{
182+
Code: extractString(langMap["Code"]),
183+
CheckCompleteness: extractBool(langMap["CheckCompleteness"], false),
184+
CustomDateFormat: extractString(langMap["CustomDateFormat"]),
185+
CustomDateTimeFormat: extractString(langMap["CustomDateTimeFormat"]),
186+
CustomTimeFormat: extractString(langMap["CustomTimeFormat"]),
187+
})
188+
}
176189
return ls
177190
}
178191

sdk/mpr/parser_settings_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package mpr
4+
5+
import (
6+
"testing"
7+
8+
"go.mongodb.org/mongo-driver/bson/primitive"
9+
)
10+
11+
// TestParseLanguageSettings_Languages verifies that Languages array items stored
12+
// as primitive.D (the BSON decoded type) are correctly parsed via extractBsonMap.
13+
// This is the fix for issue #480: bare .(map[string]any) assertions always fail
14+
// on primitive.D values, so extractBsonMap must be used instead.
15+
func TestParseLanguageSettings_Languages(t *testing.T) {
16+
raw := map[string]any{
17+
"$ID": "settings-lang-1",
18+
"$Type": "Settings$LanguageSettings",
19+
"DefaultLanguageCode": "en_US",
20+
"Languages": primitive.A{
21+
int32(2),
22+
primitive.D{
23+
{Key: "$ID", Value: "lang-1"},
24+
{Key: "$Type", Value: "Texts$Language"},
25+
{Key: "Code", Value: "en_US"},
26+
{Key: "CheckCompleteness", Value: true},
27+
{Key: "CustomDateFormat", Value: "MM/dd/yyyy"},
28+
{Key: "CustomDateTimeFormat", Value: "MM/dd/yyyy HH:mm"},
29+
{Key: "CustomTimeFormat", Value: "HH:mm"},
30+
},
31+
primitive.D{
32+
{Key: "$ID", Value: "lang-2"},
33+
{Key: "$Type", Value: "Texts$Language"},
34+
{Key: "Code", Value: "fr_FR"},
35+
{Key: "CheckCompleteness", Value: false},
36+
},
37+
},
38+
}
39+
40+
ls := parseLanguageSettings(raw)
41+
42+
if ls.DefaultLanguageCode != "en_US" {
43+
t.Errorf("DefaultLanguageCode = %q, want %q", ls.DefaultLanguageCode, "en_US")
44+
}
45+
if len(ls.Languages) != 2 {
46+
t.Fatalf("len(Languages) = %d, want 2", len(ls.Languages))
47+
}
48+
49+
en := ls.Languages[0]
50+
if en.Code != "en_US" {
51+
t.Errorf("Languages[0].Code = %q, want %q", en.Code, "en_US")
52+
}
53+
if !en.CheckCompleteness {
54+
t.Errorf("Languages[0].CheckCompleteness = false, want true")
55+
}
56+
if en.CustomDateFormat != "MM/dd/yyyy" {
57+
t.Errorf("Languages[0].CustomDateFormat = %q, want %q", en.CustomDateFormat, "MM/dd/yyyy")
58+
}
59+
if en.CustomDateTimeFormat != "MM/dd/yyyy HH:mm" {
60+
t.Errorf("Languages[0].CustomDateTimeFormat = %q, want %q", en.CustomDateTimeFormat, "MM/dd/yyyy HH:mm")
61+
}
62+
if en.CustomTimeFormat != "HH:mm" {
63+
t.Errorf("Languages[0].CustomTimeFormat = %q, want %q", en.CustomTimeFormat, "HH:mm")
64+
}
65+
66+
fr := ls.Languages[1]
67+
if fr.Code != "fr_FR" {
68+
t.Errorf("Languages[1].Code = %q, want %q", fr.Code, "fr_FR")
69+
}
70+
if fr.CheckCompleteness {
71+
t.Errorf("Languages[1].CheckCompleteness = true, want false")
72+
}
73+
}
74+
75+
// TestParseLanguageSettings_EmptyLanguages verifies that an absent or empty
76+
// Languages array results in a nil/empty slice without panicking.
77+
func TestParseLanguageSettings_EmptyLanguages(t *testing.T) {
78+
raw := map[string]any{
79+
"$ID": "settings-lang-2",
80+
"$Type": "Settings$LanguageSettings",
81+
"DefaultLanguageCode": "en_US",
82+
"Languages": primitive.A{int32(2)},
83+
}
84+
85+
ls := parseLanguageSettings(raw)
86+
if len(ls.Languages) != 0 {
87+
t.Errorf("len(Languages) = %d, want 0", len(ls.Languages))
88+
}
89+
}

0 commit comments

Comments
 (0)