Skip to content
Open
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
49 changes: 49 additions & 0 deletions SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
namespace SysML2.NET.CodeGenerator.Extensions
{
using System;
using System.Collections.Generic;
using System.Linq;

using uml4net.Classification;
using uml4net.CommonStructure;
using uml4net.Extensions;
using uml4net.SimpleClassifiers;
using uml4net.StructuredClassifiers;

/// <summary>
/// Extension class for the <see cref="IProperty"/>
Expand Down Expand Up @@ -136,5 +139,51 @@ public static string QueryIfStatementContentForNonEmpty(this IProperty property,

return "THIS WILL PRODUCE COMPILE ERROR";
}

/// <summary>
/// Returns every <see cref="IConstraint"/> from the owning class's <c>OwnedRule</c> that
/// applies to the given derived <see cref="IProperty"/>. The XMI shipped with this
/// project links derivation rules to the owning class (not the specific attribute) and uses
/// the naming convention <c>derive{ClassName}{PropertyName}</c>; both signals are honoured.
/// </summary>
/// <param name="property">The property to query. Must not be <see langword="null"/>.</param>
/// <returns>An enumerable of constraining <see cref="IConstraint"/>s; empty when none apply.</returns>
public static IEnumerable<IConstraint> QueryOwnedConstraints(this IProperty property)
{
ArgumentNullException.ThrowIfNull(property);

if (property.Owner is not IClass owningClass)
{
return [];
}

var explicitMatches = owningClass.OwnedRule
.Where(rule => rule.ConstrainedElement.Contains(property))
.ToList();

if (explicitMatches.Count > 0)
{
return explicitMatches;
}

var expectedDeriveName = "derive" + owningClass.Name + property.Name.CapitalizeFirstLetter();

return owningClass.OwnedRule
.Where(rule => string.Equals(rule.Name, expectedDeriveName, StringComparison.Ordinal));
}

/// <summary>
/// Returns every <see cref="IConstraint"/> directly owned by the given <see cref="IOperation"/>
/// via its <c>OwnedRule</c> namespace facet. Used by the Extend code-generation template to
/// surface the operation's constraint(s) as XML <c>&lt;remarks&gt;</c> blocks.
/// </summary>
/// <param name="operation">The operation to query. Must not be <see langword="null"/>.</param>
/// <returns>An enumerable of <see cref="IConstraint"/>s; empty when none are declared.</returns>
public static IEnumerable<IConstraint> QueryOwnedConstraints(this IOperation operation)
{
ArgumentNullException.ThrowIfNull(operation);

return operation.OwnedRule;
}
}
}
147 changes: 147 additions & 0 deletions SysML2.NET.CodeGenerator/HandleBarHelpers/PropertyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
namespace SysML2.NET.CodeGenerator.HandleBarHelpers
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
Expand All @@ -36,6 +37,7 @@ namespace SysML2.NET.CodeGenerator.HandleBarHelpers
using uml4net.Classification;
using uml4net.CommonStructure;
using uml4net.StructuredClassifiers;
using uml4net.Values;

/// <summary>
/// A handlebars block helper for the <see cref="IProperty"/> interface
Expand Down Expand Up @@ -1120,6 +1122,151 @@ public static void RegisterPropertyHelper(this IHandlebars handlebars)

return property.Type is IClassifier { IsAbstract: true };
});

handlebars.RegisterHelper("Property.QueryHasOwnedConstraints", (_, arguments) =>
{
if (arguments.Length != 1 || arguments[0] is not IProperty property)
{
throw new HandlebarsException("{{Property.QueryHasOwnedConstraints}} expects a single IProperty argument");
}

return HasRenderableConstraint(property.QueryOwnedConstraints());
});

handlebars.RegisterHelper("Operation.QueryHasOwnedConstraints", (_, arguments) =>
{
if (arguments.Length != 1 || arguments[0] is not IOperation operation)
{
throw new HandlebarsException("{{Operation.QueryHasOwnedConstraints}} expects a single IOperation argument");
}

return HasRenderableConstraint(operation.QueryOwnedConstraints());
});

handlebars.RegisterHelper("Property.WriteOwnedRulesAsRemarksBlock", (writer, _, arguments) =>
{
if (arguments.Length != 1 || arguments[0] is not IProperty property)
{
throw new HandlebarsException("{{Property.WriteOwnedRulesAsRemarksBlock}} expects a single IProperty argument");
}

writer.WriteSafeString(BuildOwnedRulesRemarksBlock(property.QueryOwnedConstraints()));
});

handlebars.RegisterHelper("Operation.WriteOwnedRulesAsRemarksBlock", (writer, _, arguments) =>
{
if (arguments.Length != 1 || arguments[0] is not IOperation operation)
{
throw new HandlebarsException("{{Operation.WriteOwnedRulesAsRemarksBlock}} expects a single IOperation argument");
}

writer.WriteSafeString(BuildOwnedRulesRemarksBlock(operation.QueryOwnedConstraints()));
});
}

/// <summary>
/// Builds a complete XML <c>&lt;remarks&gt;</c> block listing every constraint body carried by
/// the supplied <see cref="IConstraint"/> sequence, labelled by language. Returns the empty
/// string when no constraint carries an <see cref="IOpaqueExpression"/> body.
/// </summary>
/// <param name="constraints">The constraints to render.</param>
/// <returns>
/// A multi-line string starting with <c>/// &lt;remarks&gt;</c> and ending with <c>/// &lt;/remarks&gt;</c>,
/// each line prefixed for direct emission inside the Extend template; empty when nothing to emit.
/// </returns>
/// <summary>
/// Returns <see langword="true"/> when at least one constraint in <paramref name="constraints"/>
/// carries an <see cref="IOpaqueExpression"/> with at least one non-blank body line — i.e. when
/// <see cref="BuildOwnedRulesRemarksBlock"/> would emit a non-empty <c>&lt;remarks&gt;</c> block.
/// </summary>
/// <param name="constraints">The constraints to evaluate.</param>
/// <returns>Whether the helpers would produce any output for these constraints.</returns>
private static bool HasRenderableConstraint(IEnumerable<IConstraint> constraints)
{
foreach (var constraint in constraints)
{
var opaqueExpression = constraint.Specification?.OfType<IOpaqueExpression>().FirstOrDefault();

if (opaqueExpression?.Body != null && opaqueExpression.Body.Any(body => !string.IsNullOrWhiteSpace(body)))
{
return true;
}
}

return false;
}

private static string BuildOwnedRulesRemarksBlock(IEnumerable<IConstraint> constraints)
{
var entries = new List<(string Language, string Body)>();

foreach (var constraint in constraints)
{
var opaqueExpression = constraint.Specification?.OfType<IOpaqueExpression>().FirstOrDefault();

if (opaqueExpression == null || opaqueExpression.Body == null)
{
continue;
}

var bodies = opaqueExpression.Body;
var languages = opaqueExpression.Language;

for (var index = 0; index < bodies.Count; index++)
{
if (string.IsNullOrWhiteSpace(bodies[index]))
{
continue;
}

var language = languages != null && index < languages.Count && !string.IsNullOrWhiteSpace(languages[index])
? languages[index].Trim()
: "Constraint";

entries.Add((language, bodies[index].Trim()));
}
}

if (entries.Count == 0)
{
return string.Empty;
}

var sb = new StringBuilder();
sb.AppendLine("/// <remarks>");

for (var index = 0; index < entries.Count; index++)
{
var (language, body) = entries[index];

sb.AppendLine($"/// {EscapeXml(language)}:");
sb.AppendLine("/// <code>");

foreach (var line in body.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'))
{
sb.AppendLine($"/// {EscapeXml(line.TrimEnd())}");
}

sb.AppendLine("/// </code>");
}

sb.Append("/// </remarks>");

return sb.ToString();
}

/// <summary>
/// Replaces XML-significant characters with their entity equivalents so the result is safe to
/// embed inside an XML doc comment.
/// </summary>
/// <param name="value">The string to escape.</param>
/// <returns>The escaped string.</returns>
private static string EscapeXml(string value)
{
return value
.Replace("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
using System;
using System.Collections.Generic;

{{ #Class.WriteEnumerationNameSpaces this}}
{{ #Class.WriteEnumerationNameSpacesWithOperation this}}
{{ #Class.WriteNameSpaces this POCO}}

/// <summary>
Expand All @@ -37,6 +37,9 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
/// <summary>
/// Computes the derived property.
/// </summary>
{{#if (Property.QueryHasOwnedConstraints property)}}
{{Property.WriteOwnedRulesAsRemarksBlock property}}
{{/if}}
/// <param name="{{String.LowerCaseFirstLetter ../this.Name}}Subject">
/// The subject <see cref="I{{../this.Name}}"/>
/// </param>
Expand All @@ -53,6 +56,9 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
{{/each}}
{{#each (this.OwnedOperation) as | operation | }}
{{ #Documentation operation }}
{{#if (Operation.QueryHasOwnedConstraints operation)}}
{{Operation.WriteOwnedRulesAsRemarksBlock operation}}
{{/if}}
/// <param name="{{String.LowerCaseFirstLetter ../this.Name}}Subject">
/// The subject <see cref="I{{../this.Name}}"/>
/// </param>
Expand All @@ -63,7 +69,7 @@ namespace SysML2.NET.Core.POCO.{{ #NamedElement.WriteFullyQualifiedNameSpace thi
throw new NotSupportedException("Create a GitHub issue when this method is required");
}
{{#unless @last}}

{{/unless}}
{{/each}}
}
Expand Down
2 changes: 2 additions & 0 deletions SysML2.NET.Tests/Extend/NamespaceExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ public void VerifyComputeVisibleMembershipsOperation()
[Test]
public void VerifyComputeImportedMembershipsOperation()
{
Assert.That(() => ((INamespace)null).ComputeImportedMembershipsOperation([]), Throws.TypeOf<ArgumentNullException>());

var namespaceElement = new Namespace();

Assert.That(namespaceElement.ComputeImportedMembershipsOperation([]), Has.Count.EqualTo(0));
Expand Down
33 changes: 27 additions & 6 deletions SysML2.NET.Tests/Extend/OwningMembershipExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,53 @@
namespace SysML2.NET.Tests.Extend
{
using System;

using NUnit.Framework;


using SysML2.NET.Core.POCO.Root.Elements;
using SysML2.NET.Core.POCO.Root.Namespaces;
using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

[TestFixture]
public class OwningMembershipExtensionsTestFixture
{
[Test]
public void ComputeOwnedMemberElement_ThrowsArgumentNullException()
public void VerifyComputeOwnedMemberElement()
{
Assert.That(() => ((IOwningMembership)null).ComputeOwnedMemberElement(), Throws.TypeOf<ArgumentNullException>());

// OwnedRelatedElement.Count == 0 → IncompleteModelException
var emptyMembership = new OwningMembership();
Assert.That(() => emptyMembership.ComputeOwnedMemberElement(), Throws.TypeOf<IncompleteModelException>());

// OwnedRelatedElement.Count == 1 → returns that element
var container = new Namespace();
var singleMembership = new OwningMembership();
var ownedElement = new Definition { DeclaredName = "SingleElement" };
container.AssignOwnership(singleMembership, ownedElement);
Assert.That(singleMembership.ComputeOwnedMemberElement(), Is.SameAs(ownedElement));

// OwnedRelatedElement.Count > 1 → IncompleteModelException
var multiMembership = new OwningMembership();
((IContainedRelationship)multiMembership).OwnedRelatedElement.Add(new Definition());
((IContainedRelationship)multiMembership).OwnedRelatedElement.Add(new Definition());
Assert.That(() => multiMembership.ComputeOwnedMemberElement(), Throws.TypeOf<IncompleteModelException>());
}

[Test]
public void ComputeOwnedMemberElementId_ThrowsNotSupportedException()
{
Assert.That(() => ((IOwningMembership)null).ComputeOwnedMemberElementId(), Throws.TypeOf<NotSupportedException>());
}

[Test]
public void ComputeOwnedMemberName_ThrowsArgumentNullException()
{
Assert.That(() => ((IOwningMembership)null).ComputeOwnedMemberName(), Throws.TypeOf<ArgumentNullException>());
}

[Test]
public void ComputeOwnedMemberShortName_ThrowsArgumentNullException()
{
Expand Down
Loading
Loading