Skip to content
Merged
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
28 changes: 27 additions & 1 deletion src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ func (c *client) invokeCapability(ctx context.Context, capabilityID string, args
return nil, err
}

result, err := c.sendRequest(ctx, "invokeCapability", []any{capabilityID, args})
result, err := c.sendRequest(ctx, "invokeCapability", []any{capabilityID, c.marshalTransportValue(args)})
if err != nil {
return nil, err
}
Expand All @@ -673,6 +673,32 @@ func (c *client) invokeCapability(ctx context.Context, capabilityID string, args
return wrapIfHandle(result, c), nil
}

func (c *client) marshalTransportValue(value any) any {
if callback, ok := value.(func(...any) any); ok {
return c.registerCallback(callback)
}

serialized := serializeValue(value)
switch v := serialized.(type) {
case func(...any) any:
return c.registerCallback(v)
case map[string]any:
result := make(map[string]any, len(v))
for key, nestedValue := range v {
result[key] = c.marshalTransportValue(nestedValue)
}
return result
case []any:
result := make([]any, len(v))
for i, item := range v {
result[i] = c.marshalTransportValue(item)
}
return result
default:
return serialized
}
}

func (c *client) authenticate(ctx context.Context, token string) error {
result, err := c.sendRequest(ctx, "authenticate", []any{token})
if err != nil {
Expand Down
128 changes: 119 additions & 9 deletions src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,21 @@ private void GenerateDtoTypes(IReadOnlyList<AtsDtoTypeInfo> dtoTypes)
}
WriteLine();

WriteLine(" @SuppressWarnings(\"unchecked\")");
WriteLine($" public static {dtoName} fromMap(Map<String, Object> map) {{");
WriteLine($" var value = new {dtoName}();");
foreach (var property in dto.Properties)
{
var fieldName = ToCamelCase(property.Name);
var methodName = ToPascalCase(property.Name);
var transportValueName = $"{fieldName}Value";
WriteLine($" var {transportValueName} = map.get(\"{property.Name}\");");
WriteLine($" value.set{methodName}({RenderJavaDtoPropertyTransportValueConversion(property.Type, transportValueName, property.IsOptional)});");
}
WriteLine(" return value;");
WriteLine(" }");
WriteLine();

// toMap method for serialization
WriteLine(" public Map<String, Object> toMap() {");
WriteLine(" Map<String, Object> map = new HashMap<>();");
Expand Down Expand Up @@ -1546,14 +1561,8 @@ private void GenerateCapabilityMethodImplementation(AtsCapabilityInfo capability
}
else if (returnInfo.HasReturn)
{
if (IsUnionType(capability.ReturnType))
{
WriteLine($" return AspireUnion.of(getClient().invokeCapability(\"{capability.CapabilityId}\", reqArgs));");
}
else
{
WriteLine($" return ({returnInfo.ReturnType}) getClient().invokeCapability(\"{capability.CapabilityId}\", reqArgs);");
}
WriteLine($" var result = getClient().invokeCapability(\"{capability.CapabilityId}\", reqArgs);");
WriteLine($" return {RenderJavaTransportValueConversion(capability.ReturnType, "result", capability.ReturnType?.IsNullable == true)};");
}
else
{
Expand Down Expand Up @@ -1670,7 +1679,108 @@ private string GetCallbackArgumentExpression(AtsCallbackParameterInfo callbackPa
return $"AspireUnion.of(args[{index}])";
}

return $"({MapCallbackTypeToJava(callbackParameter.Type)}) args[{index}]";
return RenderJavaTransportValueConversion(callbackParameter.Type, $"args[{index}]", callbackParameter.Type?.IsNullable == true);
}

private string RenderJavaTransportValueConversion(AtsTypeRef? typeRef, string valueExpression, bool isOptional, int depth = 0)
{
if (typeRef is null)
{
return valueExpression;
}

if (typeRef.TypeId == AtsConstants.ReferenceExpressionTypeId)
{
return $"(ReferenceExpression) {valueExpression}";
}

if (IsCancellationTokenTypeId(typeRef.TypeId))
{
return $"(CancellationToken) {valueExpression}";
}

var allowNull = isOptional || typeRef.IsNullable == true;
var converted = typeRef.Category switch
{
AtsTypeCategory.Primitive => RenderJavaPrimitiveTransportValueConversion(typeRef.TypeId, valueExpression, allowNull),
AtsTypeCategory.Enum => RenderJavaEnumTransportValueConversion(typeRef.TypeId, valueExpression, allowNull),
AtsTypeCategory.Dto => RenderJavaDtoTransportValueConversion(typeRef.TypeId, valueExpression, allowNull),
AtsTypeCategory.Handle => $"({MapTypeRefToJava(typeRef, allowNull)}) {valueExpression}",
AtsTypeCategory.Array => $"({MapTypeRefToJava(typeRef, allowNull)}) {valueExpression}",
AtsTypeCategory.List => RenderJavaListTransportValueConversion(typeRef, valueExpression, allowNull, depth),
AtsTypeCategory.Dict => $"({MapTypeRefToJava(typeRef, allowNull, useBoxedTypes: true)}) {valueExpression}",
AtsTypeCategory.Union => $"AspireUnion.of({valueExpression})",
_ => valueExpression
};

return converted;
}

private string RenderJavaDtoPropertyTransportValueConversion(AtsTypeRef? typeRef, string valueExpression, bool isOptional)
{
if (typeRef?.Category != AtsTypeCategory.Dict)
{
return RenderJavaTransportValueConversion(typeRef, valueExpression, isOptional);
}

var allowNull = isOptional || typeRef.IsNullable == true;
var converted = $"({MapDtoPropertyTypeToJava(typeRef, allowNull, useBoxedTypes: true)}) {valueExpression}";

return allowNull ? $"{valueExpression} == null ? null : {converted}" : converted;
}

private static string RenderJavaPrimitiveTransportValueConversion(string typeId, string valueExpression, bool allowNull)
{
var converted = typeId switch
{
AtsConstants.String or AtsConstants.Char or
AtsConstants.DateTime or AtsConstants.DateTimeOffset or
AtsConstants.DateOnly or AtsConstants.TimeOnly or
AtsConstants.Guid or AtsConstants.Uri => $"(String) {valueExpression}",
AtsConstants.Number or AtsConstants.TimeSpan => $"((Number) {valueExpression}).doubleValue()",
AtsConstants.Boolean => $"(Boolean) {valueExpression}",
AtsConstants.Void => "null",
_ => valueExpression
};

return allowNull && !string.Equals(converted, valueExpression, StringComparison.Ordinal)
? $"{valueExpression} == null ? null : {converted}"
: converted;
}

private string RenderJavaEnumTransportValueConversion(string typeId, string valueExpression, bool allowNull)
{
if (!_enumNames.TryGetValue(typeId, out var enumName))
{
return $"(String) {valueExpression}";
}

var converted = $"{enumName}.fromValue((String) {valueExpression})";
return allowNull ? $"{valueExpression} == null ? null : {converted}" : converted;
}

private string RenderJavaDtoTransportValueConversion(string typeId, string valueExpression, bool allowNull)
{
if (!_dtoNames.TryGetValue(typeId, out var dtoName))
{
return $"(Map<String, Object>) {valueExpression}";
}

var converted = $"{dtoName}.fromMap((Map<String, Object>) {valueExpression})";
return allowNull ? $"{valueExpression} == null ? null : {converted}" : converted;
}

private string RenderJavaListTransportValueConversion(AtsTypeRef typeRef, string valueExpression, bool allowNull, int depth)
{
var itemName = $"item{depth}";
var convertedItem = RenderJavaTransportValueConversion(
typeRef.ElementType,
itemName,
typeRef.ElementType?.IsNullable == true,
depth + 1);
var converted = $"((List<Object>) {valueExpression}).stream().map({itemName} -> {convertedItem}).toList()";

return allowNull ? $"{valueExpression} == null ? null : {converted}" : converted;
}

private string MapCallbackTypeToJava(AtsTypeRef? typeRef)
Expand Down
41 changes: 40 additions & 1 deletion src/Aspire.Hosting.CodeGeneration.Java/Resources/Transport.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public Object invokeCapability(String capabilityId, Map<String, Object> args) {

Map<String, Object> params = new HashMap<>();
params.put("capabilityId", capabilityId);
params.put("args", args);
params.put("args", marshalTransportValue(args));

Map<String, Object> request = new HashMap<>();
request.put("jsonrpc", "2.0");
Expand All @@ -185,6 +185,45 @@ public Object invokeCapability(String capabilityId, Map<String, Object> args) {
}
}

@SuppressWarnings("unchecked")
private Object marshalTransportValue(Object value) {
if (value == null) {
return null;
}

if (value instanceof Function<?, ?> function) {
Function<Object, Object> typedFunction = (Function<Object, Object>) function;
return registerCallback(args -> typedFunction.apply(args.length > 0 ? args[0] : null));
}

Object serialized = serializeValue(value);
if (serialized instanceof Map) {
Map<String, Object> map = (Map<String, Object>) serialized;
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
result.put(entry.getKey(), marshalTransportValue(entry.getValue()));
}
return result;
}
if (serialized instanceof List) {
List<Object> list = (List<Object>) serialized;
List<Object> result = new ArrayList<>();
for (Object item : list) {
result.add(marshalTransportValue(item));
}
return result;
}
if (serialized instanceof Object[] array) {
List<Object> result = new ArrayList<>();
for (Object item : array) {
result.add(marshalTransportValue(item));
}
return result;
}

return serialized;
}

public void authenticate(String token) {
int id = requestId.incrementAndGet();

Expand Down
11 changes: 10 additions & 1 deletion src/Aspire.Hosting.CodeGeneration.Python/PythonModuleBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,7 @@ Capabilities are operations exposed by [AspireExport] attributes.
Results are automatically wrapped in Handle objects when applicable.
'''
self._check_connection()
result = self._send_request("invokeCapability", capability_id, args or {})
result = self._send_request("invokeCapability", capability_id, self._marshal_transport_value(args or {}))

# Check for structured error response
if _is_ats_error(result):
Expand All @@ -1140,6 +1140,15 @@ raise AspireError(result["$error"])
# Wrap handles automatically
return _wrap_if_handle(result, self, kwargs)

def _marshal_transport_value(self, value: typing.Any) -> typing.Any:
if callable(value):
return self.register_callback(value)
if isinstance(value, dict):
return {key: self._marshal_transport_value(nested_value) for key, nested_value in value.items()}
if isinstance(value, (list, tuple)):
return [self._marshal_transport_value(item) for item in value]
return value

def _send_request(self, method: str, *params: typing.Any) -> typing.Any:
'''Send a JSON-RPC request and wait for response'''
with self._lock:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus;

namespace Aspire.Hosting.ApplicationModel;

Expand Down Expand Up @@ -361,14 +362,66 @@ public sealed class UpdateCommandStateContext
/// <summary>
/// The resource snapshot.
/// </summary>
[AspireExportIgnore(Reason = "CustomResourceSnapshot contains object-valued properties that are not statically representable in polyglot SDKs. Use ResourceSnapshotData for the curated ATS projection.")]
public required CustomResourceSnapshot ResourceSnapshot { get; init; }

/// <summary>
/// Gets the resource snapshot data available to polyglot command state callbacks.
/// </summary>
[AspireExport(MethodName = "resourceSnapshot")]
internal UpdateCommandStateResourceSnapshot ResourceSnapshotData => UpdateCommandStateResourceSnapshot.FromSnapshot(ResourceSnapshot);

/// <summary>
/// The service provider.
/// </summary>
[AspireExportIgnore(Reason = "IServiceProvider is not usable from polyglot command state callbacks.")]
public required IServiceProvider ServiceProvider { get; init; }
}

/// <summary>
/// Resource snapshot data exposed to polyglot command state callbacks.
/// </summary>
[AspireDto]
internal sealed class UpdateCommandStateResourceSnapshot
{
/// <summary>
/// The type of the resource.
/// </summary>
public required string ResourceType { get; init; }

/// <summary>
/// The current lifecycle state text for the resource.
/// </summary>
public string? State { get; init; }

/// <summary>
/// The display style for the current lifecycle state.
/// </summary>
public string? StateStyle { get; init; }

/// <summary>
/// The current health status for the resource.
/// </summary>
public HealthStatus? HealthStatus { get; init; }

/// <summary>
/// The exit code of the resource.
/// </summary>
public int? ExitCode { get; init; }

internal static UpdateCommandStateResourceSnapshot FromSnapshot(CustomResourceSnapshot snapshot)
{
return new()
{
ResourceType = snapshot.ResourceType,
State = snapshot.State?.Text,
StateStyle = snapshot.State?.Style,
HealthStatus = snapshot.HealthStatus,
ExitCode = snapshot.ExitCode
};
}
}

/// <summary>
/// Context for <see cref="ResourceCommandAnnotation.ExecuteCommand"/>.
/// </summary>
Expand All @@ -379,6 +432,7 @@ public sealed class ExecuteCommandContext
/// <summary>
/// The service provider.
/// </summary>
[AspireExportIgnore(Reason = "IServiceProvider is not usable from polyglot command callbacks.")]
public required IServiceProvider ServiceProvider { get; init; }

/// <summary>
Expand Down
Loading
Loading