1515using Microsoft . Extensions . Hosting ;
1616using Microsoft . Extensions . Logging ;
1717using Microsoft . Extensions . Options ;
18+ using Neuroglia . Serialization ;
1819using ServerlessWorkflow . Sdk . IO ;
1920using ServerlessWorkflow . Sdk . Models ;
2021using Synapse . Api . Application . Configuration ;
@@ -28,9 +29,11 @@ namespace Synapse.Api.Application.Services;
2829/// </summary>
2930/// <param name="serviceProvider">The current <see cref="IServiceProvider"/></param>
3031/// <param name="logger">The service used to perform logging</param>
32+ /// <param name="jsonSerializer">The service used to serialize/deserialize data to/from JSON</param>
33+ /// <param name="yamlSerializer">The service used to serialize/deserialize data to/from YAML</param>
3134/// <param name="workflowDefinitionReader">The service used to read <see cref="WorkflowDefinition"/>s</param>
3235/// <param name="options">The service used to access the current <see cref="ApiServerOptions"/></param>
33- public class WorkflowDatabaseInitializer ( IServiceProvider serviceProvider , ILogger < WorkflowDatabaseInitializer > logger , IWorkflowDefinitionReader workflowDefinitionReader , IOptions < ApiServerOptions > options )
36+ public class WorkflowDatabaseInitializer ( IServiceProvider serviceProvider , ILogger < WorkflowDatabaseInitializer > logger , IJsonSerializer jsonSerializer , IYamlSerializer yamlSerializer , IWorkflowDefinitionReader workflowDefinitionReader , IOptions < ApiServerOptions > options )
3437 : IHostedService
3538{
3639
@@ -44,6 +47,16 @@ public class WorkflowDatabaseInitializer(IServiceProvider serviceProvider, ILogg
4447 /// </summary>
4548 protected ILogger Logger { get ; } = logger ;
4649
50+ /// <summary>
51+ /// Gets the service used to serialize/deserialize data to/from JSON
52+ /// </summary>
53+ protected IJsonSerializer JsonSerializer { get ; } = jsonSerializer ;
54+
55+ /// <summary>
56+ /// Gets the service used to serialize/deserialize data to/from YAML
57+ /// </summary>
58+ protected IYamlSerializer YamlSerializer { get ; } = yamlSerializer ;
59+
4760 /// <summary>
4861 /// Gets the service used to read <see cref="WorkflowDefinition"/>s
4962 /// </summary>
@@ -80,15 +93,80 @@ public virtual async Task StartAsync(CancellationToken cancellationToken)
8093 this . Logger . LogWarning ( "The directory '{directory}' does not exist or cannot be found. Skipping static resource import" , directory . FullName ) ;
8194 return ;
8295 }
83- this . Logger . LogInformation ( "Starting importing static resources from directory '{directory}'..." , directory . FullName ) ;
96+ await this . ProvisionNamespacesAsync ( resources , cancellationToken ) . ConfigureAwait ( false ) ;
97+ await this . ProvisionWorkflowsAsync ( resources , cancellationToken ) . ConfigureAwait ( false ) ;
98+ await this . ProvisionFunctionsAsync ( resources , cancellationToken ) . ConfigureAwait ( false ) ;
99+ }
100+
101+ /// <summary>
102+ /// Provisions namespaces from statis resource files
103+ /// </summary>
104+ /// <param name="resources">The <see cref="IResourceRepository"/> used to manage <see cref="IResource"/>s</param>
105+ /// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
106+ /// <returns>A new awaitable <see cref="Task"/></returns>
107+ protected virtual async Task ProvisionNamespacesAsync ( IResourceRepository resources , CancellationToken cancellationToken )
108+ {
109+ var stopwatch = new Stopwatch ( ) ;
110+ var directory = new DirectoryInfo ( Path . Combine ( this . Options . Seeding . Directory , "namespaces" ) ) ;
111+ if ( ! directory . Exists ) return ;
112+ this . Logger . LogInformation ( "Starting importing namespaces from directory '{directory}'..." , directory . FullName ) ;
84113 var files = directory . GetFiles ( this . Options . Seeding . FilePattern , SearchOption . AllDirectories ) . Where ( f => f . FullName . EndsWith ( ".json" , StringComparison . OrdinalIgnoreCase ) || f . FullName . EndsWith ( ".yml" , StringComparison . OrdinalIgnoreCase ) || f . FullName . EndsWith ( ".yaml" , StringComparison . OrdinalIgnoreCase ) ) ;
85114 if ( ! files . Any ( ) )
86115 {
87- this . Logger . LogWarning ( "No static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import." , this . Options . Seeding . FilePattern , directory . FullName ) ;
116+ this . Logger . LogWarning ( "No namespace static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import." , this . Options . Seeding . FilePattern , directory . FullName ) ;
88117 return ;
89118 }
119+ stopwatch . Restart ( ) ;
90120 var count = 0 ;
121+ foreach ( var file in files )
122+ {
123+ try
124+ {
125+ var extension = file . FullName . Split ( '.' , StringSplitOptions . RemoveEmptyEntries ) . LastOrDefault ( ) ;
126+ var serializer = extension ? . ToLowerInvariant ( ) switch
127+ {
128+ "json" => ( ITextSerializer ) this . JsonSerializer ,
129+ "yml" or "yaml" => this . YamlSerializer ,
130+ _ => throw new NotSupportedException ( $ "The specified extension '{ extension } ' is not supported for static resource files")
131+ } ;
132+ using var stream = file . OpenRead ( ) ;
133+ using var streamReader = new StreamReader ( stream ) ;
134+ var text = await streamReader . ReadToEndAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
135+ var ns = serializer . Deserialize < NamespaceDefinition > ( text ) ! ;
136+ await resources . AddAsync ( ns , false , cancellationToken ) . ConfigureAwait ( false ) ;
137+ this . Logger . LogInformation ( "Successfully imported namespace '{namespace}' from file '{file}'" , $ "{ ns . Metadata . Name } ", file . FullName ) ;
138+ count ++ ;
139+ }
140+ catch ( Exception ex )
141+ {
142+ this . Logger . LogError ( "An error occurred while reading a namespace from file '{file}': {ex}" , file . FullName , ex ) ;
143+ continue ;
144+ }
145+ }
146+ stopwatch . Stop ( ) ;
147+ this . Logger . LogInformation ( "Completed importing {count} namespaces in {ms} milliseconds" , count , stopwatch . Elapsed . TotalMilliseconds ) ;
148+ }
149+
150+ /// <summary>
151+ /// Provisions workflows from statis resource files
152+ /// </summary>
153+ /// <param name="resources">The <see cref="IResourceRepository"/> used to manage <see cref="IResource"/>s</param>
154+ /// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
155+ /// <returns>A new awaitable <see cref="Task"/></returns>
156+ protected virtual async Task ProvisionWorkflowsAsync ( IResourceRepository resources , CancellationToken cancellationToken )
157+ {
158+ var stopwatch = new Stopwatch ( ) ;
159+ var directory = new DirectoryInfo ( Path . Combine ( this . Options . Seeding . Directory , "workflows" ) ) ;
160+ if ( ! directory . Exists ) return ;
161+ this . Logger . LogInformation ( "Starting importing workflows from directory '{directory}'..." , directory . FullName ) ;
162+ var files = directory . GetFiles ( this . Options . Seeding . FilePattern , SearchOption . AllDirectories ) . Where ( f => f . FullName . EndsWith ( ".json" , StringComparison . OrdinalIgnoreCase ) || f . FullName . EndsWith ( ".yml" , StringComparison . OrdinalIgnoreCase ) || f . FullName . EndsWith ( ".yaml" , StringComparison . OrdinalIgnoreCase ) ) ;
163+ if ( ! files . Any ( ) )
164+ {
165+ this . Logger . LogWarning ( "No workflow static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import." , this . Options . Seeding . FilePattern , directory . FullName ) ;
166+ return ;
167+ }
91168 stopwatch . Restart ( ) ;
169+ var count = 0 ;
92170 foreach ( var file in files )
93171 {
94172 try
@@ -110,11 +188,6 @@ public virtual async Task StartAsync(CancellationToken cancellationToken)
110188 Versions = [ workflowDefinition ]
111189 }
112190 } ;
113- if ( await resources . GetAsync < Namespace > ( workflow . GetNamespace ( ) ! , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) == null )
114- {
115- await resources . AddAsync ( new Namespace ( ) { Metadata = new ( ) { Name = workflow . GetNamespace ( ) ! } } , false , cancellationToken ) . ConfigureAwait ( false ) ;
116- this . Logger . LogInformation ( "Successfully created namespace '{namespace}'" , workflow . GetNamespace ( ) ) ;
117- }
118191 await resources . AddAsync ( workflow , false , cancellationToken ) . ConfigureAwait ( false ) ;
119192 }
120193 else
@@ -138,14 +211,63 @@ public virtual async Task StartAsync(CancellationToken cancellationToken)
138211 this . Logger . LogInformation ( "Successfully imported workflow '{workflow}' from file '{file}'" , $ "{ workflowDefinition . Document . Name } .{ workflowDefinition . Document . Namespace } :{ workflowDefinition . Document . Version } ", file . FullName ) ;
139212 count ++ ;
140213 }
141- catch ( Exception ex )
214+ catch ( Exception ex )
142215 {
143216 this . Logger . LogError ( "An error occurred while reading a workflow definition from file '{file}': {ex}" , file . FullName , ex ) ;
144217 continue ;
145218 }
146219 }
147220 stopwatch . Stop ( ) ;
148- this . Logger . LogInformation ( "Completed importing {count} static resources in {ms} milliseconds" , count , stopwatch . Elapsed . TotalMilliseconds ) ;
221+ this . Logger . LogInformation ( "Completed importing {count} workflows in {ms} milliseconds" , count , stopwatch . Elapsed . TotalMilliseconds ) ;
222+ }
223+
224+ /// <summary>
225+ /// Provisions functions from statis resource files
226+ /// </summary>
227+ /// <param name="resources">The <see cref="IResourceRepository"/> used to manage <see cref="IResource"/>s</param>
228+ /// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
229+ /// <returns>A new awaitable <see cref="Task"/></returns>
230+ protected virtual async Task ProvisionFunctionsAsync ( IResourceRepository resources , CancellationToken cancellationToken )
231+ {
232+ var stopwatch = new Stopwatch ( ) ;
233+ var directory = new DirectoryInfo ( Path . Combine ( this . Options . Seeding . Directory , "functions" ) ) ;
234+ if ( ! directory . Exists ) return ;
235+ this . Logger . LogInformation ( "Starting importing custom functions from directory '{directory}'..." , directory . FullName ) ;
236+ var files = directory . GetFiles ( this . Options . Seeding . FilePattern , SearchOption . AllDirectories ) . Where ( f => f . FullName . EndsWith ( ".json" , StringComparison . OrdinalIgnoreCase ) || f . FullName . EndsWith ( ".yml" , StringComparison . OrdinalIgnoreCase ) || f . FullName . EndsWith ( ".yaml" , StringComparison . OrdinalIgnoreCase ) ) ;
237+ if ( ! files . Any ( ) )
238+ {
239+ this . Logger . LogWarning ( "No custom function static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import." , this . Options . Seeding . FilePattern , directory . FullName ) ;
240+ return ;
241+ }
242+ stopwatch . Restart ( ) ;
243+ var count = 0 ;
244+ foreach ( var file in files )
245+ {
246+ try
247+ {
248+ var extension = file . FullName . Split ( '.' , StringSplitOptions . RemoveEmptyEntries ) . LastOrDefault ( ) ;
249+ var serializer = extension ? . ToLowerInvariant ( ) switch
250+ {
251+ "json" => ( ITextSerializer ) this . JsonSerializer ,
252+ "yml" or "yaml" => this . YamlSerializer ,
253+ _ => throw new NotSupportedException ( $ "The specified extension '{ extension } ' is not supported for static resource files")
254+ } ;
255+ using var stream = file . OpenRead ( ) ;
256+ using var streamReader = new StreamReader ( stream ) ;
257+ var text = await streamReader . ReadToEndAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
258+ var func = serializer . Deserialize < CustomFunction > ( text ) ! ;
259+ await resources . AddAsync ( func , false , cancellationToken ) . ConfigureAwait ( false ) ;
260+ this . Logger . LogInformation ( "Successfully imported custom function '{customFunction}' from file '{file}'" , func . GetQualifiedName ( ) , file . FullName ) ;
261+ count ++ ;
262+ }
263+ catch ( Exception ex )
264+ {
265+ this . Logger . LogError ( "An error occurred while reading a custom function from file '{file}': {ex}" , file . FullName , ex ) ;
266+ continue ;
267+ }
268+ }
269+ stopwatch . Stop ( ) ;
270+ this . Logger . LogInformation ( "Completed importing {count} custom functions in {ms} milliseconds" , count , stopwatch . Elapsed . TotalMilliseconds ) ;
149271 }
150272
151273 /// <inheritdoc/>
0 commit comments