Skip to content

Commit

Permalink
Structurizr JSON parser was implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
litichevskiydv committed Jun 27, 2024
1 parent a66302e commit 48a6c9e
Show file tree
Hide file tree
Showing 15 changed files with 342 additions and 6 deletions.
9 changes: 8 additions & 1 deletion Byndyusoft.ArchitectureTesting.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
common.props = common.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstractions", "src\Abstractions\Abstractions.csproj", "{0B01C1F0-B144-4772-872A-D056C3F03F9A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Abstractions", "src\Abstractions\Abstractions.csproj", "{0B01C1F0-B144-4772-872A-D056C3F03F9A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StructurizrParser", "src\StructurizrParser\StructurizrParser.csproj", "{DD449432-9A79-49A7-82CA-27FC3A8A356D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -28,12 +30,17 @@ Global
{0B01C1F0-B144-4772-872A-D056C3F03F9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B01C1F0-B144-4772-872A-D056C3F03F9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B01C1F0-B144-4772-872A-D056C3F03F9A}.Release|Any CPU.Build.0 = Release|Any CPU
{DD449432-9A79-49A7-82CA-27FC3A8A356D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD449432-9A79-49A7-82CA-27FC3A8A356D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD449432-9A79-49A7-82CA-27FC3A8A356D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD449432-9A79-49A7-82CA-27FC3A8A356D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0B01C1F0-B144-4772-872A-D056C3F03F9A} = {9D5D61C4-D494-4124-A184-59C12C2BB2BC}
{DD449432-9A79-49A7-82CA-27FC3A8A356D} = {9D5D61C4-D494-4124-A184-59C12C2BB2BC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A79E4AD9-244B-42E6-BD30-255B9C3DEC46}
Expand Down
17 changes: 12 additions & 5 deletions Byndyusoft.ArchitectureTesting.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=CleanUp/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="CleanUp"&gt;&lt;RemoveCodeRedundancies&gt;True&lt;/RemoveCodeRedundancies&gt;&lt;CSUseAutoProperty&gt;True&lt;/CSUseAutoProperty&gt;&lt;CSMakeFieldReadonly&gt;True&lt;/CSMakeFieldReadonly&gt;&lt;CSMakeAutoPropertyGetOnly&gt;True&lt;/CSMakeAutoPropertyGetOnly&gt;&lt;CSArrangeQualifiers&gt;True&lt;/CSArrangeQualifiers&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;CSShortenReferences&gt;True&lt;/CSShortenReferences&gt;&lt;CSReformatCode&gt;True&lt;/CSReformatCode&gt;&lt;CSharpFormatDocComments&gt;True&lt;/CSharpFormatDocComments&gt;&lt;AspOptimizeRegisterDirectives&gt;True&lt;/AspOptimizeRegisterDirectives&gt;&lt;HtmlReformatCode&gt;True&lt;/HtmlReformatCode&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;JsReformatCode&gt;True&lt;/JsReformatCode&gt;&lt;JsFormatDocComments&gt;True&lt;/JsFormatDocComments&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;CssAlphabetizeProperties&gt;True&lt;/CssAlphabetizeProperties&gt;&lt;CssReformatCode&gt;True&lt;/CssReformatCode&gt;&lt;CSEnforceVarKeywordUsageSettings&gt;True&lt;/CSEnforceVarKeywordUsageSettings&gt;&lt;CSReorderTypeMembers&gt;True&lt;/CSReorderTypeMembers&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/RecentlyUsedProfile/@EntryValue">CleanUp</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">CleanUp</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_FIRST_ARG_BY_PAREN/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">ExpressionBody</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/USE_HEURISTICS_FOR_BODY_STYLE/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_FIRST_ARG_BY_PAREN/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_LINQ_QUERY/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_ARGUMENT/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_ARRAY_AND_OBJECT_INITIALIZER/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_EXPRESSION/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_EXTENDS_LIST/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_FOR_STMT/@EntryValue">True</s:Boolean>
Expand All @@ -20,6 +22,7 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_CONSTRAINS/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_LIST/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">NEXT_LINE_SHIFTED_2</s:String>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_BEFORE_CONTROL_TRANSFER_STATEMENTS/@EntryValue">1</s:Int64>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FIXED_BRACES_STYLE/@EntryValue">ONLY_FOR_MULTILINE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOR_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_FOREACH_BRACES_STYLE/@EntryValue">ALWAYS_REMOVE</s:String>
Expand All @@ -29,18 +32,22 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_ANONYMOUS_METHOD_BLOCK/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INITIALIZER_BRACES/@EntryValue">NEXT_LINE_SHIFTED_2</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_METHOD_ON_SINGLE_LINE/@EntryValue">NEVER</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_EMBEDDED_STATEMENT_ON_SAME_LINE/@EntryValue">NEVER</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">LINE_BREAK</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/STICK_COMMENT/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_DECLARATION_LPAR/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_ARROW_WITH_EXPRESSIONS/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_DECLARATION_RPAR/@EntryValue">True</s:Boolean>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LIMIT/@EntryValue">140</s:Int64>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_MULTIPLE_TYPE_PARAMEER_CONSTRAINTS_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_PARAMETERS_STYLE/@EntryValue">CHOP_IF_LONG</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_PARAMETERS_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/HtmlFormatter/NormalizeTagNames/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/HtmlFormatter/TagsAreNotIndentedInside/@EntryValue">html,body</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/AddImportsToDeepestScope/@EntryValue">True</s:Boolean>
Expand Down Expand Up @@ -93,8 +100,8 @@
<s:Boolean x:Key="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=TabNavigationExplainer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/Housekeeping/Layout/DialogWindows/RefactoringWizardWindow/Location/@EntryValue">-807,-30</s:String>
<s:String x:Key="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue">MemberGenerator</s:String>
<s:String x:Key="/Default/Housekeeping/Layout/DialogWindows/RefactoringWizardWindow/Location/@EntryValue">-640,-30</s:String>
<s:String x:Key="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue">VsShortcutSchemeOptionsPage</s:String>
<s:Boolean x:Key="/Default/Housekeeping/TreeModelBrowserPanelPersistence/IsPreviewVisible/=UnitTestSessionDescriptor/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/Housekeeping/TreeModelBrowserPanelPersistence/PreviewOrientation/=UnitTestSessionDescriptor/@EntryIndexedValue">Vertical</s:String>
<s:Double x:Key="/Default/Housekeeping/TreeModelBrowserPanelPersistence/PreviewSplitterHorizontalProportion/=UnitTestSessionDescriptor/@EntryIndexedValue">1</s:Double>
Expand Down
17 changes: 17 additions & 0 deletions src/StructurizrParser/Extensions/DictionaryExtensions.cs
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;
}
}
}
157 changes: 157 additions & 0 deletions src/StructurizrParser/JsonParser.cs
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();
}
}
}
23 changes: 23 additions & 0 deletions src/StructurizrParser/Model/Element.cs
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;
}
}
30 changes: 30 additions & 0 deletions src/StructurizrParser/Model/ElementExtensions.cs
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);
}
}
Loading

0 comments on commit 48a6c9e

Please sign in to comment.