private static string GenerateComponentCollection(TypeDescription type)
        {
            var collectionType = $"global::Improbable.Stdlib.ComponentCollection<{type.Fqn()}>";

            return($@"public static {collectionType} CreateComponentCollection()
{{
    return new {collectionType}(ComponentId, Create, ApplyUpdate);
}}");
        }
예제 #2
0
        public string Generate(TypeDescription type)
        {
            if (!HasAnnotation(type, WellKnownAnnotations.CreateTableAttribute))
            {
                return(string.Empty);
            }

            var setupCommands = type.Annotations.GetAnnotationStrings(WellKnownAnnotations.CreateTableAttribute, 0);

            var primaryKeyFields = type.Fields.WithAnnotation(WellKnownAnnotations.PrimaryKeyAttribute).ToList();

            if (primaryKeyFields.Count == 0)
            {
                throw new Exception($"{type.QualifiedName} is exposed to the database, but no fields are marked with [{WellKnownAnnotations.PrimaryKeyAttribute}]");
            }

            var columnCreator = CreateColumns(type.QualifiedName, type.Fields);

            var primaryKeyColumnNames = string.Join(", ", primaryKeyFields.Select(f => $@"{f.Name}"));
            var primaryKey            = $"PRIMARY KEY ({primaryKeyColumnNames})";

            var selectClause = string.Join(", ", type.Fields.Select(f => $@"{f.Name}{PostgresTypeConversion(f)}"));
            var ordinal      = 0;

            var indexFields = type.Fields.WithAnnotation(WellKnownAnnotations.IndexAttribute);

            var typeName = $"{type.Fqn()}";

            return($@"public static string CreateTypeTable(string tableName)
{{
    return $@""CREATE TABLE {{tableName}} (
{Indent(2, columnCreator.TrimEnd())}
{Indent(2, primaryKey)}
    );"";
}}

public static {typeName} FromQuery(global::Npgsql.NpgsqlDataReader reader)
{{
    return new {typeName} (
{Indent(2, CreateReader(type, type.Fields)).TrimEnd()}
    );
}}

public const string SelectClause = ""{selectClause}"";

{string.Join(Environment.NewLine, type.Fields.Select(f => $"public const int {SnakeCaseToPascalCase(f.Name)}Ordinal = {ordinal++};"))}

public struct DatabaseChangeNotification
{{
    public {type.Fqn()}? Old {{ get; set; }}

    public {type.Fqn()} New {{ get; set; }}
}}

public static string InitializeDatabase(string tableName)
{{
    return $@""
{string.Join(Environment.NewLine, setupCommands)}

{{CreateTypeTable(tableName)}}

{string.Join(Environment.NewLine, indexFields.Select(f => f.Annotations.GetAnnotationString(WellKnownAnnotations.IndexAttribute, 0).Replace("{fieldName}", f.Name)))}

-- Setup change notifications. This maps to the DatabaseChangeNotification class.
CREATE OR REPLACE FUNCTION notify_{{tableName}}() RETURNS TRIGGER AS $$
    BEGIN
        IF TG_OP = 'UPDATE' THEN
            PERFORM pg_notify( '{{tableName}}'::text, json_build_object( 'old', row_to_json(OLD), 'new', row_to_json(NEW) )::text);
        ELSE
            PERFORM pg_notify( '{{tableName}}'::text, json_build_object( 'new', row_to_json(NEW) )::text);
        END IF;
        RETURN NEW;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER notify_{{tableName}}_tgr
    AFTER INSERT OR UPDATE on {{tableName}}
    FOR EACH ROW EXECUTE PROCEDURE notify_{{tableName}}();"";
}}
");
        }
예제 #3
0
        public string Generate(TypeDescription type)
        {
            var exposedFields = type.Fields.Where(Types.IsFieldExposedToDatabase).ToList();

            if (!exposedFields.Any())
            {
                return(string.Empty);
            }

            var profileIdFields = type.Fields.Where(f => Annotations.HasAnnotations(f, ProfileIdAnnotation)).ToArray();

            if (!profileIdFields.Any())
            {
                throw new InvalidOperationException($"{type.QualifiedName} is missing a string field annotated with {ProfileIdAnnotation}");
            }

            if (profileIdFields.Length != 1)
            {
                throw new InvalidOperationException($"{type.QualifiedName} has multiple fields annotated with {ProfileIdAnnotation}. Only one is allowed.");
            }

            var profileIdField = profileIdFields[0];

            if (!profileIdField.IsSingular() || !profileIdField.HasPrimitive(PrimitiveType.String))
            {
                throw new InvalidOperationException($"{profileIdField.Name} is annotated with {ProfileIdAnnotation}, which requires it to be a string type.");
            }

            var profileFieldName = SnakeCaseToPascalCase(profileIdField.Name);

            var sb = new StringBuilder();

            // Provide shortcuts
            foreach (var field in exposedFields)
            {
                sb.AppendLine($"public const string {DatabaseSyncFieldName(field)} = \"{type.QualifiedName}.{field.Name}\";");
                sb.AppendLine($"public const string {PathFieldName(field)} = \"{type.ComponentId}_{field.FieldId}\";");
            }

            // Use the static Linq methods below to avoid needing to import System.Linq.
            const string enumerable       = "global::System.Linq.Enumerable";
            const string isImmediateChild = "global::Improbable.DatabaseSync.DatabaseSync.IsImmediateChild";
            const string insertStatement  = "insert into {databaseName} (path, name, count)";

            var valueFields = exposedFields.WithAnnotation(ValueAnnotation).ToList();
            var listFields  = exposedFields.WithAnnotation(ValueListAnnotation).ToList();

            sb.AppendLine($@"[global::Improbable.DatabaseSync.Hydrate({type.ComponentId})]
public static {SchemaComponentUpdate} Hydrate({GenericIEnumerable}<{DatabaseSyncItemClass}> items, string profilePath)
{{
    var update = new Update();

    foreach(var item in items)
    {{
        // Update count and list fields.
        switch(item.Name)
        {{
{string.Join("\n", valueFields.Select(field => Indent(3, $"case {DatabaseSyncFieldName(field)}: update.Set{FieldName(field)}(item.Count); break;")))}
{string.Join("\n", listFields.Select(field => Indent(3, $@"case {DatabaseSyncFieldName(field)}:
update.Set{FieldName(field)}({enumerable}.ToArray({enumerable}.Where(items,  i => {isImmediateChild}(i.Path, item.Path))));
break;")))}
        }}
    }}

    return update.ToSchemaUpdate();
}}

[global::Improbable.DatabaseSync.ProfileIdFromSchemaData]
public static string GetProfileIdFromComponentData(global::Improbable.Worker.CInterop.SchemaObject fields)
{{
    return fields.GetString({profileIdField.FieldId});
}}

public static string ComponentToDatabase(string databaseName, in {type.Fqn()} item)
{{
    if (string.IsNullOrEmpty(item.{profileFieldName}))
    {{
        throw new global::System.ArgumentNullException(nameof(item.{profileFieldName}));
    }}
    return $@""{string.Join("\n", valueFields.Select(field => $"{insertStatement} values('{{item.{profileFieldName}}}.{{{PathFieldName(field)}}}', '{{{DatabaseSyncFieldName(field)}}}', {{item.{FieldName(field)}}});"))}
{string.Join("\n", listFields.Select(field => $"{insertStatement} values('{{item.{profileFieldName}}}.{{{PathFieldName(field)}}}', '{{{DatabaseSyncFieldName(field)}}}', 0);"))}"";
}}

[global::Improbable.DatabaseSync.ProfileId]
public string DatabaseSyncProfileId => {profileFieldName};

{
                    string.Join("\n", exposedFields.Select(field => $@"public string {FieldName(field)}Path()
{{
    return {profileFieldName} + ""."" + {PathFieldName(field)};
}}
"))
                }");
            return(sb.ToString().TrimEnd());
        }
        private static string GenerateCommands(TypeDescription type, IEnumerable <ComponentDefinition.CommandDefinition> commands)
        {
            var text           = new StringBuilder();
            var bindingMethods = new StringBuilder();

            var commandIndices = new StringBuilder();

            foreach (var cmd in commands)
            {
                var(request, response) = cmd.InnerFqns();
                var cmdName = cmd.Name();

                commandIndices.AppendLine($"{cmdName} = {cmd.CommandIndex},");

                var boundResponseSender = "";

                // Don't allow workers to send command responses for system commands.
                if (!type.IsRestricted)
                {
                    boundResponseSender = $@"public void Send{cmdName}Response(long id, {response} response)
{{
    {type.Fqn()}.Send{cmdName}Response(connection, id, response);
}}";
                }

                bindingMethods.AppendLine($@"
/// <summary>
/// Sends a command to the worker that is authoritative over the bound entityId./>.
/// </summary>
/// <param name=""request""> The request payload.</param>
/// <param name=""cancellation""> A token used to mark the task as cancelled.
/// Cancelling will NOT cancel the sending of the command to the authoritative worker; it WILL be processed by the runtime and the target worker.
/// It only marks the task as cancelled locally. Use this for flow control or cleanup of state.
/// </param>
/// <param name=""timeout""> The amount of time that must pass before a command response is considered ""timed out"".</param>
/// <param name=""commandParameters""> Options used to configure how the command is sent. </param>
/// <returns> A Task containing response payload. </returns>
public global::System.Threading.Tasks.Task<{response}> Send{cmdName}Async({request} request, {CancellationTokenType} cancellation = default, uint? timeout = null, global::Improbable.Worker.CInterop.CommandParameters? commandParameters = null)
{{
    return {type.Fqn()}.Send{cmdName}Async(connection, entityId, request, cancellation, timeout, commandParameters);
}}
{boundResponseSender}");
                var responseSender = "";

                // Don't allow workers to send command responses for system commands.
                if (!type.IsRestricted)
                {
                    responseSender = $@"public static void Send{cmdName}Response({WorkerConnectionType} connection, long id, {response} response)
{{
    var schemaResponse = global::Improbable.Worker.CInterop.SchemaCommandResponse.Create();
    response.ApplyToSchemaObject(schemaResponse.GetObject());

    connection.SendCommandResponse(id, {type.Fqn()}.ComponentId, {cmd.CommandIndex}, schemaResponse);
}}
";
                }

                text.AppendLine($@"{responseSender}
/// <summary>
/// Sends a command to the worker that is authoritative over <paramref name=""entityId""/>.
/// </summary>
/// <param name=""connection""> </param>
/// <param name=""entityId""> </param>
/// <param name=""request""> The request payload.</param>
/// <param name=""cancellation""> A token used to mark the task as cancelled.
/// Cancelling will NOT cancel the sending of the command to the authoritative worker; it WILL be processed by the runtime and the target worker.
/// It only marks the task as cancelled locally. Use this for flow control or cleanup of state.
/// </param>
/// <param name=""timeout""> The amount of time that must pass before a command response is considered ""timed out"".</param>
/// <param name=""commandParameters""> Options used to configure how the command is sent. </param>
/// <param name=""taskOptions""> Options that control how the task is scheduled and executed. </param>
/// <returns> A Task containing response payload. </returns>
public static global::System.Threading.Tasks.Task<{response}> Send{cmdName}Async({WorkerConnectionType} connection,
                                                                                 {Types.EntityIdType} entityId,
                                                                                 {request} request,
                                                                                 {CancellationTokenType} cancellation = default,
                                                                                 uint? timeout = null,
                                                                                 global::Improbable.Worker.CInterop.CommandParameters? commandParameters = null,
                                                                                 global::System.Threading.Tasks.TaskCreationOptions taskOptions = global::System.Threading.Tasks.TaskCreationOptions.RunContinuationsAsynchronously)
{{
    var schemaRequest = global::Improbable.Worker.CInterop.SchemaCommandRequest.Create();
    request.ApplyToSchemaObject(schemaRequest.GetObject());

    var completion = new global::System.Threading.Tasks.TaskCompletionSource<{response}>(taskOptions);
    if (cancellation.CanBeCanceled)
    {{
        cancellation.Register(() => completion.TrySetCanceled(cancellation));
    }}

    void Complete(global::Improbable.Stdlib.WorkerConnection.CommandResponses r)
    {{
        var result = new {response}(r.UserCommand.Response.SchemaData.Value.GetObject());
        completion.TrySetResult(result);
    }}

    void Fail(global::Improbable.Worker.CInterop.StatusCode code, string message)
    {{
        completion.TrySetException(new global::Improbable.Stdlib.CommandFailedException(code, message));
    }}

    connection.Send(entityId, {type.Fqn()}.ComponentId, {cmd.CommandIndex}, schemaRequest, timeout, commandParameters, Complete, Fail);

    return completion.Task;
}}");
            }

            if (commandIndices.Length > 0)
            {
                text.Append($@"
public enum Commands
{{
{Indent(1, commandIndices.ToString().TrimEnd())}
}}

public static Commands GetCommandType(global::Improbable.Worker.CInterop.CommandRequestOp request)
{{
    if (request.Request.ComponentId != ComponentId)
    {{
        throw new global::System.InvalidOperationException($""Mismatch of ComponentId (expected {{ComponentId}} but got {{request.Request.ComponentId}}"");
    }}

    return (Commands) request.Request.CommandIndex;
}}

public readonly struct CommandSenderBinding
{{
    private readonly {WorkerConnectionType} connection;
    private readonly {Types.EntityIdType} entityId;

    public CommandSenderBinding({WorkerConnectionType} connection, {Types.EntityIdType} entityId)
    {{
        this.connection = connection;
        this.entityId = entityId;
    }}
{Indent(1, bindingMethods.ToString().TrimEnd())}
}}

public static CommandSenderBinding Bind({WorkerConnectionType} connection, {Types.EntityIdType} entityId)
{{
    return new CommandSenderBinding(connection, entityId);
}}
");
            }

            return(text.ToString());
        }
        public static string GenerateSendUpdate(TypeDescription type)
        {
            return($@"public static void SendUpdate({WorkerConnectionType} connection, {Types.EntityIdType} entityId, {$"{type.Fqn()}.Update"} update, global::Improbable.Worker.CInterop.UpdateParameters? updateParams = null)
{{
    connection.SendComponentUpdate(entityId.Value, ComponentId, update.ToSchemaUpdate(), updateParams);
}}
");
        }