Skip to content

Commit a2057f8

Browse files
dionesiusapclaude
andcommitted
fix: address review feedback from PR #210
Fixes four issues identified by @ako in code review: 1. Windows file:// URL RFC 8089 compliance - Add leading slash for Windows paths: file:///C:/path - Previously: file://C:/path (incorrect, treats C: as host) - Now: file:///C:/path (correct per RFC 8089) 2. Deduplicate path resolution in fetchODataMetadata - Remove redundant file:// extraction and relative path resolution - Now relies on NormalizeURL() already called in createODataClient - Simplify to: PathFromURL() + os.ReadFile() - Remove unused mprDir parameter 3. Improve ServiceUrl validation error message - Add migration hint explaining the breaking change - Show how to create constant: CREATE CONSTANT Module.ApiLocation - Explain this enforces Mendix best practice 4. Fix unsafe os.Chdir in tests - Replace TestFetchODataMetadata_RelativePathWithoutProject - New test uses NormalizeURL() to generate file:// URL - No process-global state mutation - Tests now expect normalized URLs (matching production flow) All tests pass. Addresses review comments without changing functionality. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent eb23c9b commit a2057f8

3 files changed

Lines changed: 48 additions & 67 deletions

File tree

internal/pathutil/uri.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ func NormalizeURL(rawURL string, baseDir string) (string, error) {
7878
}
7979

8080
// Return as file:// URL with forward slashes (cross-platform)
81-
return "file://" + filepath.ToSlash(absPath), nil
81+
// RFC 8089 requires three slashes: file:///path or file:///C:/path
82+
slashed := filepath.ToSlash(absPath)
83+
if !strings.HasPrefix(slashed, "/") {
84+
// Windows path like C:/Users/x needs leading slash: file:///C:/Users/x
85+
slashed = "/" + slashed
86+
}
87+
return "file://" + slashed, nil
8288
}
8389

8490
// PathFromURL extracts a filesystem path from a URL, handling both file:// URLs and HTTP(S) URLs.

mdl/executor/cmd_odata.go

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,13 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error {
993993
if stmt.ServiceUrl != "" {
994994
// ServiceUrl must be a constant reference (e.g., @Module.ConstantName)
995995
if !strings.HasPrefix(stmt.ServiceUrl, "@") {
996-
return fmt.Errorf("ServiceUrl must be a constant reference starting with '@' (e.g., '@Module.LocationConstant'), got: %s", stmt.ServiceUrl)
996+
return fmt.Errorf(`ServiceUrl must now be a constant reference (e.g., '@Module.ApiLocation').
997+
Previously literal URLs were allowed; this enforces the Mendix best practice of externalizing configuration.
998+
Create a constant first:
999+
CREATE CONSTANT Module.ApiLocation TYPE String DEFAULT 'https://api.example.com/';
1000+
Then reference it:
1001+
ServiceUrl: '@Module.ApiLocation'
1002+
Got: %s`, stmt.ServiceUrl)
9971003
}
9981004
cfg.OverrideLocation = true
9991005
cfg.CustomLocation = stmt.ServiceUrl
@@ -1022,7 +1028,7 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error {
10221028
}
10231029
newSvc.MetadataUrl = normalizedUrl
10241030

1025-
metadata, hash, err := fetchODataMetadata(normalizedUrl, mprDir)
1031+
metadata, hash, err := fetchODataMetadata(normalizedUrl)
10261032
if err != nil {
10271033
fmt.Fprintf(e.output, "Warning: could not fetch $metadata: %v\n", err)
10281034
} else if metadata != "" {
@@ -1427,46 +1433,26 @@ func astEntityDefToModel(def *ast.PublishedEntityDef) (*model.PublishedEntityTyp
14271433
// fetchODataMetadata downloads or reads the $metadata document.
14281434
// Supports:
14291435
// - https://... or http://... (HTTP fetch)
1430-
// - file:///abs/path (local absolute path)
1431-
// - ./path or path/file.xml (local relative path, resolved against mprDir)
1436+
// - file:///abs/path (local absolute path from normalized URL)
14321437
//
14331438
// Returns the metadata XML and its SHA-256 hash, or empty strings if the fetch fails.
1434-
// If mprDir is empty and a relative path is given, resolves against cwd.
1435-
func fetchODataMetadata(metadataUrl string, mprDir string) (metadata string, hash string, err error) {
1439+
// Note: metadataUrl is expected to be already normalized by NormalizeURL() in createODataClient,
1440+
// so all relative paths have been converted to absolute file:// URLs.
1441+
func fetchODataMetadata(metadataUrl string) (metadata string, hash string, err error) {
14361442
if metadataUrl == "" {
14371443
return "", "", nil
14381444
}
14391445

14401446
var body []byte
14411447

1442-
// Detect if this is a local file path (file:// or not http/https)
1443-
isHTTP := strings.HasPrefix(metadataUrl, "http://") || strings.HasPrefix(metadataUrl, "https://")
1448+
// At this point, metadataUrl is already normalized by NormalizeURL() in createODataClient:
1449+
// - Relative paths have been converted to absolute file:// URLs
1450+
// - HTTP(S) URLs are unchanged
1451+
// So we only need to distinguish file:// vs HTTP(S)
14441452

1445-
if !isHTTP {
1446-
// Local file path - extract and resolve
1447-
filePath := metadataUrl
1448-
if strings.HasPrefix(metadataUrl, "file://") {
1449-
filePath = pathutil.URIToPath(metadataUrl)
1450-
if filePath == "" {
1451-
return "", "", fmt.Errorf("invalid file:// URI: %s", metadataUrl)
1452-
}
1453-
}
1454-
1455-
// Resolve relative paths
1456-
if !filepath.IsAbs(filePath) {
1457-
if mprDir != "" {
1458-
filePath = filepath.Join(mprDir, filePath)
1459-
} else {
1460-
// No project loaded - use cwd
1461-
cwd, err := os.Getwd()
1462-
if err != nil {
1463-
return "", "", fmt.Errorf("failed to resolve relative path: %w", err)
1464-
}
1465-
filePath = filepath.Join(cwd, filePath)
1466-
}
1467-
}
1468-
1469-
// Read local file
1453+
filePath := pathutil.PathFromURL(metadataUrl)
1454+
if filePath != "" {
1455+
// Local file - read directly (path is already absolute)
14701456
body, err = os.ReadFile(filePath)
14711457
if err != nil {
14721458
return "", "", fmt.Errorf("failed to read local metadata file %s: %w", filePath, err)

mdl/executor/cmd_odata_test.go

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,49 +20,34 @@ func TestFetchODataMetadata_LocalFile(t *testing.T) {
2020
t.Fatalf("Failed to create test metadata file: %v", err)
2121
}
2222

23+
// Convert to proper file:// URL (RFC 8089 compliant)
24+
fileURL, err := pathutil.NormalizeURL(metadataPath, tmpDir)
25+
if err != nil {
26+
t.Fatalf("Failed to normalize path: %v", err)
27+
}
28+
2329
tests := []struct {
2430
name string
2531
url string
26-
mprDir string
2732
wantErr bool
2833
errContains string
2934
}{
3035
{
31-
name: "absolute file:// URL",
32-
url: "file://" + metadataPath,
33-
mprDir: "",
34-
wantErr: false,
35-
},
36-
{
37-
name: "absolute path without file://",
38-
url: metadataPath,
39-
mprDir: "",
40-
wantErr: false,
41-
},
42-
{
43-
name: "relative path with mprDir",
44-
url: "metadata.xml",
45-
mprDir: tmpDir,
46-
wantErr: false,
47-
},
48-
{
49-
name: "relative path with ./ prefix",
50-
url: "./metadata.xml",
51-
mprDir: tmpDir,
36+
name: "RFC 8089 file:// URL",
37+
url: fileURL,
5238
wantErr: false,
5339
},
5440
{
5541
name: "nonexistent file",
5642
url: "file:///nonexistent/metadata.xml",
57-
mprDir: "",
5843
wantErr: true,
5944
errContains: "failed to read local metadata file",
6045
},
6146
}
6247

6348
for _, tt := range tests {
6449
t.Run(tt.name, func(t *testing.T) {
65-
metadata, hash, err := fetchODataMetadata(tt.url, tt.mprDir)
50+
metadata, hash, err := fetchODataMetadata(tt.url)
6651

6752
if tt.wantErr {
6853
if err == nil {
@@ -87,7 +72,7 @@ func TestFetchODataMetadata_LocalFile(t *testing.T) {
8772
}
8873

8974
// Hash should be consistent
90-
_, hash2, _ := fetchODataMetadata(tt.url, tt.mprDir)
75+
_, hash2, _ := fetchODataMetadata(tt.url)
9176
if hash != hash2 {
9277
t.Errorf("Hash inconsistent between calls: %q vs %q", hash, hash2)
9378
}
@@ -180,20 +165,24 @@ func TestNormalizeMetadataUrl(t *testing.T) {
180165
}
181166
}
182167

183-
func TestFetchODataMetadata_RelativePathWithoutProject(t *testing.T) {
184-
// Create metadata file in current directory
168+
func TestFetchODataMetadata_LocalFileAbsolute(t *testing.T) {
169+
// Create metadata file with absolute path
185170
tmpDir := t.TempDir()
186-
oldCwd, _ := os.Getwd()
187-
defer os.Chdir(oldCwd)
188-
os.Chdir(tmpDir)
189171

190172
metadataContent := `<?xml version="1.0"?><edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0"></edmx:Edmx>`
191-
if err := os.WriteFile("local.xml", []byte(metadataContent), 0644); err != nil {
173+
filePath := filepath.Join(tmpDir, "local.xml")
174+
if err := os.WriteFile(filePath, []byte(metadataContent), 0644); err != nil {
192175
t.Fatalf("Failed to create test file: %v", err)
193176
}
194177

195-
// Test with empty mprDir (should resolve against cwd)
196-
metadata, hash, err := fetchODataMetadata("local.xml", "")
178+
// Convert to file:// URL (simulates what NormalizeURL does)
179+
fileURL := "file://" + filepath.ToSlash(filePath)
180+
if !strings.HasPrefix(filePath, "/") {
181+
// Windows: add leading slash for RFC 8089 compliance
182+
fileURL = "file:///" + filepath.ToSlash(filePath)
183+
}
184+
185+
metadata, hash, err := fetchODataMetadata(fileURL)
197186
if err != nil {
198187
t.Errorf("Unexpected error: %v", err)
199188
}

0 commit comments

Comments
 (0)