Skip to content

Commit 1402ec3

Browse files
akoclaude
andcommitted
feat: add consumed REST client BSON writer with mx check validation
Implement CREATE/UPDATE/DELETE for Rest$ConsumedRestService documents, wiring the executor to serialize AST → model → BSON. Key BSON findings from mx check: parameters use Rest$OperationParameter (not Rest$RestOperationParameter), and data types use DataTypes$IntegerType (not DataTypes$IntegerAttributeType). - sdk/mpr/writer_rest.go: BSON serialization for all polymorphic types - mdl/executor/cmd_rest_clients.go: CREATE/DROP REST CLIENT execution - sdk/mpr/parser_rest.go: handle both DataTypes$*Type formats - 15 integration tests (11 roundtrip + 4 mx check, all 0 errors) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7c4e163 commit 1402ec3

File tree

6 files changed

+1402
-11
lines changed

6 files changed

+1402
-11
lines changed

mdl/executor/cmd_rest_clients.go

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,24 +213,161 @@ func outputRestOperation(w io.Writer, op *model.RestClientOperation) {
213213
// createRestClient handles CREATE REST CLIENT statement.
214214
func (e *Executor) createRestClient(stmt *ast.CreateRestClientStmt) error {
215215
if e.writer == nil {
216-
return fmt.Errorf("project not open for writing")
216+
return fmt.Errorf("not connected to a project (read-only mode)")
217217
}
218218

219-
// For now, just validate the statement was parsed correctly
220-
fmt.Fprintf(e.output, "Parsed REST client: %s.%s with %d operations\n",
221-
stmt.Name.Module, stmt.Name.Name, len(stmt.Operations))
222-
fmt.Fprintln(e.output, "Note: BSON writing not yet implemented for consumed REST services")
219+
moduleName := stmt.Name.Module
220+
module, err := e.findModule(moduleName)
221+
if err != nil {
222+
return fmt.Errorf("module not found: %s", moduleName)
223+
}
224+
225+
// Check for existing service with same name
226+
existingServices, _ := e.reader.ListConsumedRestServices()
227+
h, err := e.getHierarchy()
228+
if err != nil {
229+
return fmt.Errorf("failed to build hierarchy: %w", err)
230+
}
223231

232+
for _, existing := range existingServices {
233+
existModID := h.FindModuleID(existing.ContainerID)
234+
existModName := h.GetModuleName(existModID)
235+
if strings.EqualFold(existModName, moduleName) && strings.EqualFold(existing.Name, stmt.Name.Name) {
236+
if stmt.CreateOrModify {
237+
// Delete existing and recreate
238+
if err := e.writer.DeleteConsumedRestService(existing.ID); err != nil {
239+
return fmt.Errorf("failed to delete existing REST client: %w", err)
240+
}
241+
} else {
242+
return fmt.Errorf("REST client already exists: %s.%s (use CREATE OR MODIFY to overwrite)", moduleName, stmt.Name.Name)
243+
}
244+
}
245+
}
246+
247+
// Resolve folder if specified
248+
containerID := module.ID
249+
if stmt.Folder != "" {
250+
folderID, err := e.resolveFolder(module.ID, stmt.Folder)
251+
if err != nil {
252+
return fmt.Errorf("failed to resolve folder '%s': %w", stmt.Folder, err)
253+
}
254+
containerID = folderID
255+
}
256+
257+
// Build the model from AST
258+
svc := &model.ConsumedRestService{
259+
ContainerID: containerID,
260+
Name: stmt.Name.Name,
261+
Documentation: stmt.Documentation,
262+
BaseUrl: stmt.BaseUrl,
263+
}
264+
265+
// Authentication
266+
if stmt.Authentication != nil {
267+
svc.Authentication = &model.RestAuthentication{
268+
Scheme: stmt.Authentication.Scheme,
269+
Username: stmt.Authentication.Username,
270+
Password: stmt.Authentication.Password,
271+
}
272+
}
273+
274+
// Operations
275+
for _, opDef := range stmt.Operations {
276+
op := buildRestClientOperation(opDef)
277+
svc.Operations = append(svc.Operations, op)
278+
}
279+
280+
// Write to project
281+
if err := e.writer.CreateConsumedRestService(svc); err != nil {
282+
return fmt.Errorf("failed to create REST client: %w", err)
283+
}
284+
285+
fmt.Fprintf(e.output, "Created REST client: %s.%s (%d operations)\n", moduleName, stmt.Name.Name, len(svc.Operations))
224286
return nil
225287
}
226288

289+
// buildRestClientOperation converts an AST RestOperationDef to a model RestClientOperation.
290+
func buildRestClientOperation(opDef *ast.RestOperationDef) *model.RestClientOperation {
291+
op := &model.RestClientOperation{
292+
Name: opDef.Name,
293+
Documentation: opDef.Documentation,
294+
HttpMethod: opDef.Method,
295+
Path: opDef.Path,
296+
BodyType: opDef.BodyType,
297+
BodyVariable: opDef.BodyVariable,
298+
ResponseType: opDef.ResponseType,
299+
ResponseVariable: opDef.ResponseVariable,
300+
Timeout: opDef.Timeout,
301+
}
302+
303+
// Path parameters
304+
for _, p := range opDef.Parameters {
305+
name := strings.TrimPrefix(p.Name, "$")
306+
op.Parameters = append(op.Parameters, &model.RestClientParameter{
307+
Name: name,
308+
DataType: p.DataType,
309+
})
310+
}
311+
312+
// Query parameters
313+
for _, q := range opDef.QueryParameters {
314+
name := strings.TrimPrefix(q.Name, "$")
315+
op.QueryParameters = append(op.QueryParameters, &model.RestClientParameter{
316+
Name: name,
317+
DataType: q.DataType,
318+
})
319+
}
320+
321+
// Headers
322+
for _, h := range opDef.Headers {
323+
header := &model.RestClientHeader{
324+
Name: h.Name,
325+
}
326+
if h.Variable != "" {
327+
// Dynamic header: 'prefix' + $Var or just $Var
328+
if h.Prefix != "" {
329+
header.Value = h.Prefix + "{1}"
330+
} else {
331+
header.Value = "{1}"
332+
}
333+
} else {
334+
header.Value = h.Value
335+
}
336+
op.Headers = append(op.Headers, header)
337+
}
338+
339+
return op
340+
}
341+
227342
// dropRestClient handles DROP REST CLIENT statement.
228343
func (e *Executor) dropRestClient(stmt *ast.DropRestClientStmt) error {
229344
if e.writer == nil {
230-
return fmt.Errorf("project not open for writing")
345+
return fmt.Errorf("not connected to a project (read-only mode)")
346+
}
347+
348+
services, err := e.reader.ListConsumedRestServices()
349+
if err != nil {
350+
return fmt.Errorf("failed to list consumed REST services: %w", err)
351+
}
352+
353+
h, err := e.getHierarchy()
354+
if err != nil {
355+
return fmt.Errorf("failed to build hierarchy: %w", err)
356+
}
357+
358+
for _, svc := range services {
359+
modID := h.FindModuleID(svc.ContainerID)
360+
moduleName := h.GetModuleName(modID)
361+
if strings.EqualFold(moduleName, stmt.Name.Module) && strings.EqualFold(svc.Name, stmt.Name.Name) {
362+
if err := e.writer.DeleteConsumedRestService(svc.ID); err != nil {
363+
return fmt.Errorf("failed to delete REST client: %w", err)
364+
}
365+
fmt.Fprintf(e.output, "Dropped REST client: %s.%s\n", moduleName, svc.Name)
366+
return nil
367+
}
231368
}
232369

233-
return fmt.Errorf("DROP REST CLIENT not yet implemented")
370+
return fmt.Errorf("REST client not found: %s", stmt.Name)
234371
}
235372

236373
// formatRestAuthValue formats an authentication value for MDL output.

mdl/executor/roundtrip_helpers_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ func (e *testEnv) assertRoundtrip(createMDL string, opts ...RoundtripOption) Rou
427427
case *ast.CreateDatabaseConnectionStmt:
428428
qualifiedName = s.Name.String()
429429
describeCmd = "DESCRIBE DATABASE CONNECTION " + qualifiedName + ";"
430+
case *ast.CreateRestClientStmt:
431+
qualifiedName = s.Name.String()
432+
describeCmd = "DESCRIBE REST CLIENT " + qualifiedName + ";"
430433
default:
431434
e.t.Fatalf("Unsupported statement type for roundtrip: %T", prog.Statements[0])
432435
return result

0 commit comments

Comments
 (0)