-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Structurizr JSON parser was implemented
- Loading branch information
1 parent
a66302e
commit 48a6c9e
Showing
15 changed files
with
342 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
namespace Byndyusoft.ArchitectureTesting.StructurizrParser.Extensions | ||
{ | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
internal static class DictionaryExtensions | ||
{ | ||
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) | ||
{ | ||
if (dictionary == null) | ||
throw new ArgumentNullException(nameof(dictionary)); | ||
|
||
dictionary.TryGetValue(key, out var value); | ||
return value; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
namespace Byndyusoft.ArchitectureTesting.StructurizrParser | ||
{ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Abstractions.ServiceContracts; | ||
using Abstractions.ServiceContracts.Dependencies; | ||
using Extensions; | ||
using Model; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Serialization; | ||
using StructurizrModel = Model.Model; | ||
|
||
/// <summary> | ||
/// Парсер JSON'а, генерируемого Structurizr | ||
/// </summary> | ||
public class JsonParser | ||
{ | ||
private readonly Func<string, bool> _serviceNameMatcher; | ||
|
||
/// <summary> | ||
/// Инициализирует парсер | ||
/// </summary> | ||
/// <param name="serviceNameMatcher">Матчер, распознающий имя сервиса среди сегментов Url'а его репозитория</param> | ||
public JsonParser(Func<string, bool> serviceNameMatcher) | ||
{ | ||
_serviceNameMatcher = serviceNameMatcher ?? throw new ArgumentNullException(nameof(serviceNameMatcher)); | ||
} | ||
|
||
private static Workspace DeserializeJson(string jsonString) | ||
=> JsonConvert.DeserializeObject<Workspace>( | ||
jsonString, | ||
new JsonSerializerSettings | ||
{ | ||
ContractResolver = new CamelCasePropertyNamesContractResolver() | ||
} | ||
)!; | ||
|
||
private static Relationship[] GetRelationships(IReadOnlyDictionary<int, Element> elementsByIds, StructurizrModel model) | ||
=> model.CustomElements | ||
.SelectMany(x => x.Relationships) | ||
.Concat( | ||
model.SoftwareSystems.SelectMany( | ||
softwareSystem => | ||
softwareSystem.Containers.SelectMany(container => container.Relationships) | ||
) | ||
) | ||
.Where(x => elementsByIds.ContainsKey(x.SourceId) && elementsByIds.ContainsKey(x.DestinationId)) | ||
.ToArray(); | ||
|
||
private static DependencyBase MapIncomingRelationshipToDependency( | ||
IReadOnlyDictionary<int, Element> elementsByIds, | ||
Relationship relationship | ||
) | ||
{ | ||
var sourceElement = elementsByIds[relationship.SourceId]; | ||
if (sourceElement.IsRabbit()) | ||
return new RabbitDependency {Name = sourceElement.Name, Direction = MqDependencyDirection.Incoming}; | ||
if (sourceElement.IsKafka()) | ||
return new KafkaDependency {Name = sourceElement.Name, Direction = MqDependencyDirection.Incoming}; | ||
|
||
throw new NotSupportedException( | ||
$"Can not parse relationship {relationship} from {sourceElement} to {elementsByIds[relationship.DestinationId]}" | ||
); | ||
} | ||
|
||
private string ExtractServiceNameFromUrl(string repositoryUrl) | ||
=> repositoryUrl.Split(new[] {'\\', '/'}, StringSplitOptions.RemoveEmptyEntries).Single(_serviceNameMatcher); | ||
|
||
private DependencyBase MapOutgoingRelationshipToDependency( | ||
IReadOnlyDictionary<int, Element> elementsByIds, | ||
Relationship relationship | ||
) | ||
{ | ||
var destinationElement = elementsByIds[relationship.DestinationId]; | ||
if (relationship.IsSyncCall()) | ||
{ | ||
if (destinationElement.IsWebApi()) | ||
return new ApiDependency {Name = ExtractServiceNameFromUrl(destinationElement.Url!)}; | ||
if (destinationElement.IsMsSql() || destinationElement.IsPostgreSql()) | ||
return new DbDependency {Name = destinationElement.Name}; | ||
if (destinationElement.IsS3()) | ||
return new S3Dependency {Name = destinationElement.Name}; | ||
} | ||
else | ||
{ | ||
if (destinationElement.IsRabbit()) | ||
return new RabbitDependency {Name = destinationElement.Name, Direction = MqDependencyDirection.Outgoing}; | ||
if (destinationElement.IsKafka()) | ||
return new KafkaDependency {Name = destinationElement.Name, Direction = MqDependencyDirection.Outgoing}; | ||
} | ||
|
||
throw new NotSupportedException( | ||
$"Can not parse relationship {relationship} from {elementsByIds[relationship.SourceId]} to {destinationElement}" | ||
); | ||
} | ||
|
||
private ServiceContract MapToService( | ||
IReadOnlyDictionary<int, Element> elementsByIds, | ||
Element element, | ||
Relationship[]? incomingRelationships, | ||
Relationship[]? outgoingRelationships | ||
) | ||
{ | ||
var service = new ServiceContract {Name = ExtractServiceNameFromUrl(element.Url!)}; | ||
|
||
var dependencies = Enumerable.Empty<DependencyBase>(); | ||
if (incomingRelationships != null) | ||
dependencies = dependencies.Concat( | ||
incomingRelationships | ||
.Where(x => x.IsAsyncCall()) | ||
.Select(x => MapIncomingRelationshipToDependency(elementsByIds, x)) | ||
); | ||
if (outgoingRelationships != null) | ||
dependencies = dependencies.Concat( | ||
outgoingRelationships.Select(x => MapOutgoingRelationshipToDependency(elementsByIds, x)) | ||
); | ||
|
||
service.Dependencies = dependencies.ToArray(); | ||
|
||
return service; | ||
} | ||
|
||
/// <summary> | ||
/// Парсит JSON-строку <paramref name="jsonString"/>, сгенерированную Structurizr, в объектную модель | ||
/// </summary> | ||
/// <param name="jsonString">JSON-строка, сгенерированная Structurizr</param> | ||
public ServiceContract[] Parse(string jsonString) | ||
{ | ||
var model = DeserializeJson(jsonString).Model; | ||
var elementsByIds = model.CustomElements | ||
.Concat(model.SoftwareSystems.SelectMany(x => x.Containers)) | ||
.ToDictionary(x => x.Id); | ||
var relationships = GetRelationships(elementsByIds, model); | ||
var relationsBySourceId = relationships | ||
.GroupBy(x => x.SourceId) | ||
.ToDictionary(x => x.Key, x => x.ToArray()); | ||
var relationsByDestinationId = relationships | ||
.GroupBy(x => x.DestinationId) | ||
.ToDictionary(x => x.Key, x => x.ToArray()); | ||
|
||
return model.SoftwareSystems | ||
.Where(x => x.IsExternalSystem() == false) | ||
.SelectMany(x => x.Containers) | ||
.Where(x => x.IsDatabase() == false && x.IsMq() == false) | ||
.Select( | ||
x => MapToService( | ||
elementsByIds, | ||
x, | ||
relationsByDestinationId.GetValueOrDefault(x.Id), | ||
relationsBySourceId.GetValueOrDefault(x.Id) | ||
) | ||
) | ||
.ToArray(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
namespace Byndyusoft.ArchitectureTesting.StructurizrParser.Model | ||
{ | ||
using System; | ||
|
||
internal class Element : ITagged | ||
{ | ||
public int Id { get; set; } | ||
|
||
public string Name { get; set; } | ||
|
||
public string Tags { get; set; } | ||
|
||
public string? Metadata { get; set; } | ||
|
||
public string? Technology { get; set; } | ||
|
||
public string? Url { get; set; } | ||
|
||
public Relationship[] Relationships { get; set; } = Array.Empty<Relationship>(); | ||
|
||
public override string ToString() => Name; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
namespace Byndyusoft.ArchitectureTesting.StructurizrParser.Model | ||
{ | ||
using System; | ||
|
||
internal static class ElementExtensions | ||
{ | ||
public static bool IsWebApi(this Element element) => element.HasTag("WebApi"); | ||
|
||
public static bool IsDatabase(this Element element) => element.HasTag("Database"); | ||
|
||
public static bool IsMq(this Element element) => element.HasTag("MQ"); | ||
|
||
private static string GetTechnology(this Element element) => (element.Technology ?? element.Metadata)!; | ||
|
||
public static bool IsMsSql(this Element element) | ||
=> element.IsDatabase() && string.Equals(element.GetTechnology(), "MSSQL", StringComparison.InvariantCultureIgnoreCase); | ||
|
||
public static bool IsPostgreSql(this Element element) | ||
=> element.IsDatabase() && string.Equals(element.GetTechnology(), "PostgreSQL", StringComparison.InvariantCultureIgnoreCase); | ||
|
||
public static bool IsS3(this Element element) | ||
=> element.IsDatabase() && string.Equals(element.GetTechnology(), "MinIO", StringComparison.InvariantCultureIgnoreCase); | ||
|
||
public static bool IsRabbit(this Element element) | ||
=> element.IsMq() && string.Equals(element.GetTechnology(), "RabbitMQ", StringComparison.InvariantCultureIgnoreCase); | ||
|
||
public static bool IsKafka(this Element element) | ||
=> element.IsMq() && string.Equals(element.GetTechnology(), "Kafka", StringComparison.InvariantCultureIgnoreCase); | ||
} | ||
} |
Oops, something went wrong.