Skip to content
Merged
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
14 changes: 14 additions & 0 deletions example/source/AuthenticationFragment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring.Expressions;

namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Example;

[Document("auth-fragment", Type = DocumentType.Fragment)]
public class AuthenticationFragment : IFragment
{
public void Fragment(IFragmentContext context)
{
context.SetHeader("X-Auth-Fragment", "true");
context.AuthenticationBasic("{{username}}", "{{password}}");
}
}
4 changes: 3 additions & 1 deletion src/Authoring/Attributes/DocumentAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;

[AttributeUsage(AttributeTargets.Class)]
public class DocumentAttribute(string? name = null, DocumentScope scope = DocumentScope.Any) : Attribute
public class DocumentAttribute(string? name = null) : Attribute
{
public string? Name { get; } = name;
public DocumentScope Scope { get; init; } = DocumentScope.Any;
public DocumentType Type { get; init; } = DocumentType.Policy;
}
5 changes: 5 additions & 0 deletions src/Authoring/DocumentScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
public enum DocumentScope
{
Any = 0, Global, Workspace, Product, Api, Operation
}

public enum DocumentType
{
Policy = 0, Fragment
}
18 changes: 18 additions & 0 deletions src/Authoring/IFragment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;

/// <summary>
/// Interface for policy fragments. Policy fragments are reusable policy elements
/// that can be included in policy definitions using the include-fragment policy.
/// </summary>
public interface IFragment
{
/// <summary>
/// Defines the policy elements that make up this fragment.
/// The policies defined in this method will be compiled directly into the fragment.
/// </summary>
/// <param name="context">The fragment context providing access to all policy operations.</param>
void Fragment(IFragmentContext context);
}
719 changes: 719 additions & 0 deletions src/Authoring/IFragmentContext.cs

Large diffs are not rendered by default.

49 changes: 37 additions & 12 deletions src/Core/Compiling/DocumentCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ public DocumentCompiler(Lazy<BlockCompiler> blockCompiler)

public IDocumentCompilationResult Compile(Compilation compilation, ClassDeclarationSyntax document)
{
var methods = document.DescendantNodes()
.OfType<MethodDeclarationSyntax>();
var policyDocument = new XElement("policies");
DocumentCompilationContext context = new(compilation, document, policyDocument);
var semanticModel = compilation.GetSemanticModel(document.SyntaxTree);
var documentType = document.ExtractDocumentType(semanticModel);
var methods = document.DescendantNodes().OfType<MethodDeclarationSyntax>();
var rootElement = new XElement(documentType == DocumentType.Fragment ? "fragment" : "policies");
var context = new DocumentCompilationContext(compilation, document, rootElement);

if (documentType == DocumentType.Fragment)
CompileFragment(context, methods);
else
CompilePolicy(context, methods);

return context;
}

private void CompilePolicy(DocumentCompilationContext context, IEnumerable<MethodDeclarationSyntax> methods)
{
foreach (var method in methods)
{
var sectionName = method.Identifier.ValueText switch
Expand All @@ -45,12 +56,30 @@ public IDocumentCompilationResult Compile(Compilation compilation, ClassDeclarat

CompileSection(context, sectionName, method);
}

return context;
}

private void CompileFragment(DocumentCompilationContext context, IEnumerable<MethodDeclarationSyntax> methods)
{
var fragmentMethod = methods.FirstOrDefault(m => m.Identifier.ValueText == "Fragment");

if (fragmentMethod != null && ValidateMethodBody(fragmentMethod, context))
{
_blockCompiler.Value.Compile(context, fragmentMethod.Body!);
}
}

private void CompileSection(DocumentCompilationContext context, string section, MethodDeclarationSyntax method)
{
if (!ValidateMethodBody(method, context))
return;

var sectionElement = new XElement(section);
var sectionContext = new DocumentCompilationContext(context, sectionElement);
_blockCompiler.Value.Compile(sectionContext, method.Body!);
context.AddPolicy(sectionElement);
}

private bool ValidateMethodBody(MethodDeclarationSyntax method, DocumentCompilationContext context)
{
if (method.Body is null)
{
Expand All @@ -59,12 +88,8 @@ private void CompileSection(DocumentCompilationContext context, string section,
method.GetLocation(),
method.Identifier.ValueText
));
return;
return false;
}

var sectionElement = new XElement(section);
var sectionContext = new DocumentCompilationContext(context, sectionElement);
_blockCompiler.Value.Compile(sectionContext, method.Body);
context.AddPolicy(sectionElement);
return true;
}
}
8 changes: 8 additions & 0 deletions src/Core/Compiling/SyntaxExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public static string ExtractDocumentFileName(this ClassDeclarationSyntax documen
return attributeArgumentExpression?.Token.ValueText ?? document.Identifier.ValueText;
}

public static DocumentType ExtractDocumentType(this ClassDeclarationSyntax document, SemanticModel model)
{
var attributeSyntax = document.AttributeLists.GetFirstAttributeOfType<DocumentAttribute>(model);
var fragmentArgument = attributeSyntax?.ArgumentList?.Arguments
.FirstOrDefault(arg => arg.Expression.ToString().Contains(nameof(DocumentType.Fragment)));
return fragmentArgument != null ? DocumentType.Fragment : DocumentType.Policy;
}

public static IEnumerable<ClassDeclarationSyntax> GetDocumentAttributedClasses(this SyntaxNode syntax,
SemanticModel semanticModel)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Microsoft",
"classifications": [
"Policy Toolkit",
"Policy Fragment",
"Class"
],
"name": "Policy Toolkit Policy Fragment Class",
"generatorVersions": "[1.0.0.0-*)",
"description": "Creates a new Policy Toolkit policy fragment class",
"groupIdentity": "Azure.ApiManagement.PolicyToolkit.Templates.PolicyFragment",
"identity": "Azure.ApiManagement.PolicyToolkit.Templates.PolicyFragment.1.0",
"shortName": "policytoolkitfragment",
"tags": {
"language": "C#",
"type": "item"
},
"sourceName": "PolicyFragment1",
"preferDefaultName": true,
"defaultName": "PolicyFragment1",
"primaryOutputs": [
{
"path": ".cs"
}
],
"symbols": {
"DefaultNamespace": {
"type": "bind",
"binding": "msbuild:RootNamespace",
"replaces": "Company.PolicyProject1"
}
},
"constraints": {
"csharp-only": {
"type": "project-capability",
"args": "CSharp"
}
}
}
17 changes: 17 additions & 0 deletions src/Templates/content/create-policy-fragment/PolicyFragment1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Company.PolicyProject1;

[Document(Type = DocumentType.Fragment)]
public class PolicyFragment1 : IFragment
{
public void Fragment(IFragmentContext context)
{
// Add reusable policy logic here
// This fragment can be included in any policy section (inbound, outbound, backend, on-error)

// Example: Set a common header
// context.SetHeader("X-Custom-Header", "fragment-value");

// Example: Set a variable
// context.SetVariable("fragment-processed", "true");
}
}
100 changes: 100 additions & 0 deletions test/Test.Core/Compiling/DocumentTypeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Compiling;

[TestClass]
public class DocumentTypeTests
{
[TestMethod]
[DataRow(
"""
[Document]
public class PolicyDocument : IDocument
{
public void Inbound(IInboundContext context)
{
context.SetHeader("X-Test", "value");
}
}
""",
"""
<policies>
<inbound>
<set-header name="X-Test" exists-action="override">
<value>value</value>
</set-header>
</inbound>
</policies>
""",
DisplayName = "Should compile regular policy document with policies root"
)]
[DataRow(
"""
[Document( Type = DocumentType.Policy )]
public class PolicyDocument : IDocument
{
public void Inbound(IInboundContext context)
{
context.SetHeader("X-Test", "value");
}
}
""",
"""
<policies>
<inbound>
<set-header name="X-Test" exists-action="override">
<value>value</value>
</set-header>
</inbound>
</policies>
""",
DisplayName = "Should compile explicit policy document with policies root"
)]
[DataRow(
"""
[Document( Type = DocumentType.Fragment )]
public class PolicyFragment : IFragment
{
public void Fragment(IFragmentContext context)
{
context.SetHeader("X-Fragment", "fragment-value");
}
}
""",
"""
<fragment>
<set-header name="X-Fragment" exists-action="override">
<value>fragment-value</value>
</set-header>
</fragment>
""",
DisplayName = "Should compile policy fragment with fragment root using Fragment method"
)]
[DataRow(
"""
[Document("my-fragment", Type = DocumentType.Fragment)]
public class NamedPolicyFragment : IFragment
{
public void Fragment(IFragmentContext context)
{
context.SetHeader("X-Named-Fragment", "named-value");
context.Base();
}
}
""",
"""
<fragment>
<set-header name="X-Named-Fragment" exists-action="override">
<value>named-value</value>
</set-header>
<base />
</fragment>
""",
DisplayName = "Should compile named policy fragment with multiple policies using Fragment method"
)]
public void ShouldCompileDocumentWithCorrectType(string code, string expectedXml)
{
code.CompileDocument().Should().BeSuccessful().And.DocumentEquivalentTo(expectedXml);
}
}
Loading