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
30 changes: 29 additions & 1 deletion src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,28 @@ private void EmitCallbackRegistration(string indent, AtsParameterInfo p, string
// Legacy untyped callback returning any — preserve return value.
WriteLine($"{indent}\t\treturn {callExpr}");
}
else if (p.CallbackParameters is { Count: > 0 } callbackParameters && callbackParameters.Any(cp => cp.Type.Category == AtsTypeCategory.Dto))
{
var argNames = new List<string>(callbackParameters.Count);
for (var i = 0; i < callbackParameters.Count; i++)
{
var argName = $"arg{i}";
argNames.Add(argName);
var goType = MapTypeRefToGo(callbackParameters[i].Type, false);
WriteLine($"{indent}\t\t{argName} := callbackArg[{goType}](args, {i})");
}

WriteLine($"{indent}\t\tcb({string.Join(", ", argNames)})");
WriteLine($"{indent}\t\treturn map[string]any{{");
for (var i = 0; i < callbackParameters.Count; i++)
{
if (callbackParameters[i].Type.Category == AtsTypeCategory.Dto)
{
WriteLine($"{indent}\t\t\t\"p{i}\": serializeValue({argNames[i]}),");
}
}
WriteLine($"{indent}\t\t}}");
}
else
{
WriteLine($"{indent}\t\t{callExpr}");
Expand Down Expand Up @@ -1840,9 +1862,15 @@ private void GenerateCreateBuilder(AtsContext context)
WriteLine("\t}");
}
WriteLine("\tif _, ok := resolved[\"Args\"]; !ok { resolved[\"Args\"] = os.Args[1:] }");
WriteLine("\tif _, ok := resolved[\"ProjectDirectory\"]; !ok {");
WriteLine("\tif projectDirectory, ok := resolved[\"ProjectDirectory\"].(string); !ok || projectDirectory == \"\" {");
WriteLine("\t\tif pwd, err := os.Getwd(); err == nil { resolved[\"ProjectDirectory\"] = pwd }");
WriteLine("\t}");
WriteLine("\tif appHostFilePath, ok := resolved[\"AppHostFilePath\"].(string); !ok || appHostFilePath == \"\" {");
WriteLine("\t\tif appHostFilePath := os.Getenv(\"ASPIRE_APPHOST_FILEPATH\"); appHostFilePath != \"\" { resolved[\"AppHostFilePath\"] = appHostFilePath }");
WriteLine("\t}");
WriteLine("\tif dashboardApplicationName, ok := resolved[\"DashboardApplicationName\"].(string); ok && dashboardApplicationName == \"\" {");
WriteLine("\t\tdelete(resolved, \"DashboardApplicationName\")");
WriteLine("\t}");
WriteLine();
WriteLine($"\tresult, err := c.invokeCapability(context.Background(), \"{AtsConstants.CreateBuilderCapability}\", map[string]any{{\"argsOrOptions\": resolved}})");
WriteLine("\tif err != nil { return nil, err }");
Expand Down
61 changes: 61 additions & 0 deletions src/Aspire.Hosting.CodeGeneration.Go/Resources/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
)

Expand Down Expand Up @@ -712,11 +713,71 @@ func decodeAs[T any](raw any) (T, error) {
}
var out T
if err := json.Unmarshal(bytes, &out); err != nil {
if decoded, ok := decodeStructFields[T](raw); ok {
return decoded, nil
}
return zero, err
}
return out, nil
}

func decodeStructFields[T any](raw any) (T, bool) {
var zero T
rawMap, ok := raw.(map[string]any)
if !ok {
return zero, false
}

targetType := reflect.TypeOf((*T)(nil)).Elem()
isPointer := targetType.Kind() == reflect.Ptr
if isPointer {
targetType = targetType.Elem()
}
if targetType.Kind() != reflect.Struct {
return zero, false
}

targetValue := reflect.New(targetType)
structValue := targetValue.Elem()
for i := 0; i < targetType.NumField(); i++ {
fieldInfo := targetType.Field(i)
fieldValue := structValue.Field(i)
if !fieldValue.CanSet() {
continue
}

fieldName := fieldInfo.Name
if tag := fieldInfo.Tag.Get("json"); tag != "" {
name, _, _ := strings.Cut(tag, ",")
if name == "-" {
continue
}
if name != "" {
fieldName = name
}
}

rawFieldValue, ok := rawMap[fieldName]
if !ok {
continue
}

bytes, err := json.Marshal(rawFieldValue)
if err != nil {
continue
}
if err := json.Unmarshal(bytes, fieldValue.Addr().Interface()); err != nil {
continue
}
}

if isPointer {
return targetValue.Interface().(T), true
}

return structValue.Interface().(T), true
}

// ── deepUpdate ───────────────────────────────────────────────────────────────
//
// Used by generated code to merge variadic Options structs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ private void GenerateDtoTypes(IReadOnlyList<AtsDtoTypeInfo> dtoTypes)

var dtoName = _dtoNames[dto.TypeId];
WriteLine($"/** {dto.Name} DTO. */");
WriteLine($"class {dtoName} {{");
WriteLine($"class {dtoName} implements JsonSerializable {{");

// Fields
foreach (var property in dto.Properties)
Expand Down
105 changes: 89 additions & 16 deletions src/Aspire.Hosting.CodeGeneration.Java/Resources/Transport.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ void onCancel(Runnable listener) {
}
}

interface JsonSerializable {
Map<String, Object> toMap();
}

/**
* AspireClient handles JSON-RPC communication with the AppHost server.
*/
Expand Down Expand Up @@ -314,7 +318,7 @@ private String readLine() throws IOException {
private void handleServerRequest(Map<String, Object> request) throws IOException {
String method = (String) request.get("method");
Object idObj = request.get("id");
Map<String, Object> params = (Map<String, Object>) request.get("params");
Object params = request.get("params");

debug("Received server request: " + method);

Expand All @@ -323,25 +327,33 @@ private void handleServerRequest(Map<String, Object> request) throws IOException

try {
if ("invokeCallback".equals(method)) {
String callbackId = (String) params.get("callbackId");
List<Object> args = (List<Object>) params.get("args");

Function<Object[], Object> callback = callbacks.get(callbackId);
if (callback != null) {
Object[] unwrappedArgs = args.stream()
.map(this::unwrapResult)
.toArray();
result = awaitValue(callback.apply(unwrappedArgs));
String callbackId = getCallbackId(params);
if (callbackId == null) {
error = createError(-32602, "Invalid params: callbackId is required.");
} else {
error = createError(-32601, "Callback not found: " + callbackId);
List<Object> args = getCallbackArgs(params);

Function<Object[], Object> callback = callbacks.get(callbackId);
if (callback != null) {
Object[] unwrappedArgs = args.stream()
.map(this::unwrapResult)
.toArray();
result = awaitValue(callback.apply(unwrappedArgs));
} else {
error = createError(-32601, "Callback not found: " + callbackId);
}
}
} else if ("cancel".equals(method)) {
String cancellationId = (String) params.get("cancellationId");
Consumer<Void> handler = cancellations.get(cancellationId);
if (handler != null) {
handler.accept(null);
String cancellationId = getCancellationId(params);
if (cancellationId == null) {
error = createError(-32602, "Invalid params: cancellationId is required.");
} else {
Consumer<Void> handler = cancellations.get(cancellationId);
if (handler != null) {
handler.accept(null);
}
result = true;
}
result = true;
} else {
error = createError(-32601, "Unknown method: " + method);
}
Expand All @@ -362,6 +374,64 @@ private void handleServerRequest(Map<String, Object> request) throws IOException
sendMessage(response);
}

@SuppressWarnings("unchecked")
private String getCallbackId(Object params) {
if (params instanceof List<?> list && !list.isEmpty()) {
return asString(list.get(0));
}

if (params instanceof Map<?, ?> map) {
return asString(map.get("callbackId"));
}

return null;
}

@SuppressWarnings("unchecked")
private List<Object> getCallbackArgs(Object params) {
Object args = null;
if (params instanceof List<?> list && list.size() > 1) {
args = list.get(1);
} else if (params instanceof Map<?, ?> map) {
args = map.get("args");
}

if (args instanceof Map<?, ?> map) {
List<Object> positionalArgs = new ArrayList<>();
for (var i = 0; ; i++) {
var key = "p" + i;
if (map.containsKey(key)) {
positionalArgs.add(map.get(key));
} else {
break;
}
}
return positionalArgs;
}

if (args instanceof List<?> list) {
return (List<Object>) list;
}

return args == null ? List.of() : List.of(args);
}

private String getCancellationId(Object params) {
if (params instanceof List<?> list && !list.isEmpty()) {
return asString(list.get(0));
}

if (params instanceof Map<?, ?> map) {
return asString(map.get("cancellationId"));
}

return null;
}

private String asString(Object value) {
return value instanceof String string ? string : null;
}

private Map<String, Object> createError(int code, String message) {
Map<String, Object> error = new HashMap<>();
error.put("code", code);
Expand Down Expand Up @@ -462,6 +532,9 @@ public static Object serializeValue(Object value) {
if (value instanceof AspireUnion union) {
return serializeValue(union.getValue());
}
if (value instanceof JsonSerializable jsonSerializable) {
return jsonSerializable.toMap();
}
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
Expand Down
19 changes: 2 additions & 17 deletions src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.TypeSystem;

Expand Down Expand Up @@ -933,23 +934,7 @@ private static string ToSnakeCase(string name)
return name;
}

var result = new System.Text.StringBuilder();
result.Append(char.ToLowerInvariant(name[0]));

for (int i = 1; i < name.Length; i++)
{
var c = name[i];
if (char.IsUpper(c))
{
result.Append('_');
result.Append(char.ToLowerInvariant(c));
}
else
{
result.Append(c);
}
}
var resultStr = result.ToString();
var resultStr = JsonNamingPolicy.SnakeCaseLower.ConvertName(name);
resultStr = resultStr.Replace("environment", "env");
resultStr = resultStr.Replace("configuration", "config");
resultStr = resultStr.Replace("application", "app");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,7 @@ def create_builder(
*,
args: typing.Iterable[str] | None = None,
project_directory: str | None = None,
app_host_file_path: str | None = None,
container_registry_override: str | None = None,
disable_dashboard: bool | None = None,
dashboard_application_name: str | None = None,
Expand All @@ -1813,6 +1814,8 @@ def create_builder(
passed to the Aspire command line (arguments specified after '--'). Specifying them here will override that default.
project_directory (str): The directory containing the AppHost project file. By default, this will use the ASPIRE_PROJECT_DIRECTORY
environment variable if set, otherwise it will use the current working directory.
app_host_file_path (str): The path to the AppHost source file. By default, this will use the ASPIRE_APPHOST_FILEPATH
environment variable if set.
container_registry_override (str): When containers are used, use this value to override the container registry.
disable_dashboard (bool): Determines whether the dashboard is disabled.
dashboard_application_name (str): The application name to display in the dashboard.
Expand Down Expand Up @@ -1842,6 +1845,12 @@ elif not effective_options.get('Args'):
effective_options['ProjectDirectory'] = project_directory
elif not effective_options.get('ProjectDirectory'):
effective_options['ProjectDirectory'] = os.environ.get('ASPIRE_PROJECT_DIRECTORY', os.getcwd())
if app_host_file_path is not None:
effective_options['AppHostFilePath'] = app_host_file_path
elif not effective_options.get('AppHostFilePath'):
app_host_file_path = os.environ.get('ASPIRE_APPHOST_FILEPATH')
if app_host_file_path:
effective_options['AppHostFilePath'] = app_host_file_path
if container_registry_override is not None:
effective_options['ContainerRegistryOverride'] = container_registry_override
if disable_dashboard is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ private Expression BuildMarshalArgs(ParameterExpression[] paramExprs, ParameterI
var marshalCall = Expression.Call(
Expression.Constant(this),
marshalMethod,
Expression.Convert(paramExpr, typeof(object)));
Expression.Convert(paramExpr, typeof(object)),
Expression.Constant(param.ParameterType, typeof(Type)));

// Use positional key (p0, p1, p2, ...) instead of param.Name
var addCall = Expression.Call(jsonObjVar, addMethod!, Expression.Constant($"p{paramIndex}"), marshalCall);
Expand All @@ -169,9 +170,9 @@ private Expression BuildMarshalArgs(ParameterExpression[] paramExprs, ParameterI
return Expression.Block(new[] { jsonObjVar }, expressions);
}

private JsonNode? MarshalArg(object? value)
private JsonNode? MarshalArg(object? value, Type declaredType)
{
return _marshaller.MarshalToJson(value);
return _marshaller.MarshalToJson(value, declaredType);
}

private Expression BuildSyncVoidCall(string callbackId, Expression? argsExpr, Expression? ctExpr, int ctParamIndex)
Expand Down
Loading
Loading