Skip to content

Commit 7322537

Browse files
FilPagMielek
andauthored
feat: Implement support for creating simple policy fragments (#167)
Co-authored-by: Rafał Mielowski <[email protected]>
1 parent 1572a01 commit 7322537

File tree

10 files changed

+961
-13
lines changed

10 files changed

+961
-13
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
2+
using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring.Expressions;
3+
4+
namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Example;
5+
6+
[Document("auth-fragment", Type = DocumentType.Fragment)]
7+
public class AuthenticationFragment : IFragment
8+
{
9+
public void Fragment(IFragmentContext context)
10+
{
11+
context.SetHeader("X-Auth-Fragment", "true");
12+
context.AuthenticationBasic("{{username}}", "{{password}}");
13+
}
14+
}

src/Authoring/Attributes/DocumentAttribute.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
55

66
[AttributeUsage(AttributeTargets.Class)]
7-
public class DocumentAttribute(string? name = null, DocumentScope scope = DocumentScope.Any) : Attribute
7+
public class DocumentAttribute(string? name = null) : Attribute
88
{
99
public string? Name { get; } = name;
10+
public DocumentScope Scope { get; init; } = DocumentScope.Any;
11+
public DocumentType Type { get; init; } = DocumentType.Policy;
1012
}

src/Authoring/DocumentScope.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
66
public enum DocumentScope
77
{
88
Any = 0, Global, Workspace, Product, Api, Operation
9+
}
10+
11+
public enum DocumentType
12+
{
13+
Policy = 0, Fragment
914
}

src/Authoring/IFragment.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring;
5+
6+
/// <summary>
7+
/// Interface for policy fragments. Policy fragments are reusable policy elements
8+
/// that can be included in policy definitions using the include-fragment policy.
9+
/// </summary>
10+
public interface IFragment
11+
{
12+
/// <summary>
13+
/// Defines the policy elements that make up this fragment.
14+
/// The policies defined in this method will be compiled directly into the fragment.
15+
/// </summary>
16+
/// <param name="context">The fragment context providing access to all policy operations.</param>
17+
void Fragment(IFragmentContext context);
18+
}

src/Authoring/IFragmentContext.cs

Lines changed: 719 additions & 0 deletions
Large diffs are not rendered by default.

src/Core/Compiling/DocumentCompiler.cs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,22 @@ public DocumentCompiler(Lazy<BlockCompiler> blockCompiler)
2222

2323
public IDocumentCompilationResult Compile(Compilation compilation, ClassDeclarationSyntax document)
2424
{
25-
var methods = document.DescendantNodes()
26-
.OfType<MethodDeclarationSyntax>();
27-
var policyDocument = new XElement("policies");
28-
DocumentCompilationContext context = new(compilation, document, policyDocument);
25+
var semanticModel = compilation.GetSemanticModel(document.SyntaxTree);
26+
var documentType = document.ExtractDocumentType(semanticModel);
27+
var methods = document.DescendantNodes().OfType<MethodDeclarationSyntax>();
28+
var rootElement = new XElement(documentType == DocumentType.Fragment ? "fragment" : "policies");
29+
var context = new DocumentCompilationContext(compilation, document, rootElement);
2930

31+
if (documentType == DocumentType.Fragment)
32+
CompileFragment(context, methods);
33+
else
34+
CompilePolicy(context, methods);
35+
36+
return context;
37+
}
38+
39+
private void CompilePolicy(DocumentCompilationContext context, IEnumerable<MethodDeclarationSyntax> methods)
40+
{
3041
foreach (var method in methods)
3142
{
3243
var sectionName = method.Identifier.ValueText switch
@@ -45,12 +56,30 @@ public IDocumentCompilationResult Compile(Compilation compilation, ClassDeclarat
4556

4657
CompileSection(context, sectionName, method);
4758
}
48-
49-
return context;
5059
}
5160

61+
private void CompileFragment(DocumentCompilationContext context, IEnumerable<MethodDeclarationSyntax> methods)
62+
{
63+
var fragmentMethod = methods.FirstOrDefault(m => m.Identifier.ValueText == "Fragment");
64+
65+
if (fragmentMethod != null && ValidateMethodBody(fragmentMethod, context))
66+
{
67+
_blockCompiler.Value.Compile(context, fragmentMethod.Body!);
68+
}
69+
}
5270

5371
private void CompileSection(DocumentCompilationContext context, string section, MethodDeclarationSyntax method)
72+
{
73+
if (!ValidateMethodBody(method, context))
74+
return;
75+
76+
var sectionElement = new XElement(section);
77+
var sectionContext = new DocumentCompilationContext(context, sectionElement);
78+
_blockCompiler.Value.Compile(sectionContext, method.Body!);
79+
context.AddPolicy(sectionElement);
80+
}
81+
82+
private bool ValidateMethodBody(MethodDeclarationSyntax method, DocumentCompilationContext context)
5483
{
5584
if (method.Body is null)
5685
{
@@ -59,12 +88,8 @@ private void CompileSection(DocumentCompilationContext context, string section,
5988
method.GetLocation(),
6089
method.Identifier.ValueText
6190
));
62-
return;
91+
return false;
6392
}
64-
65-
var sectionElement = new XElement(section);
66-
var sectionContext = new DocumentCompilationContext(context, sectionElement);
67-
_blockCompiler.Value.Compile(sectionContext, method.Body);
68-
context.AddPolicy(sectionElement);
93+
return true;
6994
}
7095
}

src/Core/Compiling/SyntaxExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ public static string ExtractDocumentFileName(this ClassDeclarationSyntax documen
3333
return attributeArgumentExpression?.Token.ValueText ?? document.Identifier.ValueText;
3434
}
3535

36+
public static DocumentType ExtractDocumentType(this ClassDeclarationSyntax document, SemanticModel model)
37+
{
38+
var attributeSyntax = document.AttributeLists.GetFirstAttributeOfType<DocumentAttribute>(model);
39+
var fragmentArgument = attributeSyntax?.ArgumentList?.Arguments
40+
.FirstOrDefault(arg => arg.Expression.ToString().Contains(nameof(DocumentType.Fragment)));
41+
return fragmentArgument != null ? DocumentType.Fragment : DocumentType.Policy;
42+
}
43+
3644
public static IEnumerable<ClassDeclarationSyntax> GetDocumentAttributedClasses(this SyntaxNode syntax,
3745
SemanticModel semanticModel)
3846
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$schema": "http://json.schemastore.org/template",
3+
"author": "Microsoft",
4+
"classifications": [
5+
"Policy Toolkit",
6+
"Policy Fragment",
7+
"Class"
8+
],
9+
"name": "Policy Toolkit Policy Fragment Class",
10+
"generatorVersions": "[1.0.0.0-*)",
11+
"description": "Creates a new Policy Toolkit policy fragment class",
12+
"groupIdentity": "Azure.ApiManagement.PolicyToolkit.Templates.PolicyFragment",
13+
"identity": "Azure.ApiManagement.PolicyToolkit.Templates.PolicyFragment.1.0",
14+
"shortName": "policytoolkitfragment",
15+
"tags": {
16+
"language": "C#",
17+
"type": "item"
18+
},
19+
"sourceName": "PolicyFragment1",
20+
"preferDefaultName": true,
21+
"defaultName": "PolicyFragment1",
22+
"primaryOutputs": [
23+
{
24+
"path": ".cs"
25+
}
26+
],
27+
"symbols": {
28+
"DefaultNamespace": {
29+
"type": "bind",
30+
"binding": "msbuild:RootNamespace",
31+
"replaces": "Company.PolicyProject1"
32+
}
33+
},
34+
"constraints": {
35+
"csharp-only": {
36+
"type": "project-capability",
37+
"args": "CSharp"
38+
}
39+
}
40+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Company.PolicyProject1;
2+
3+
[Document(Type = DocumentType.Fragment)]
4+
public class PolicyFragment1 : IFragment
5+
{
6+
public void Fragment(IFragmentContext context)
7+
{
8+
// Add reusable policy logic here
9+
// This fragment can be included in any policy section (inbound, outbound, backend, on-error)
10+
11+
// Example: Set a common header
12+
// context.SetHeader("X-Custom-Header", "fragment-value");
13+
14+
// Example: Set a variable
15+
// context.SetVariable("fragment-processed", "true");
16+
}
17+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Compiling;
5+
6+
[TestClass]
7+
public class DocumentTypeTests
8+
{
9+
[TestMethod]
10+
[DataRow(
11+
"""
12+
[Document]
13+
public class PolicyDocument : IDocument
14+
{
15+
public void Inbound(IInboundContext context)
16+
{
17+
context.SetHeader("X-Test", "value");
18+
}
19+
}
20+
""",
21+
"""
22+
<policies>
23+
<inbound>
24+
<set-header name="X-Test" exists-action="override">
25+
<value>value</value>
26+
</set-header>
27+
</inbound>
28+
</policies>
29+
""",
30+
DisplayName = "Should compile regular policy document with policies root"
31+
)]
32+
[DataRow(
33+
"""
34+
[Document( Type = DocumentType.Policy )]
35+
public class PolicyDocument : IDocument
36+
{
37+
public void Inbound(IInboundContext context)
38+
{
39+
context.SetHeader("X-Test", "value");
40+
}
41+
}
42+
""",
43+
"""
44+
<policies>
45+
<inbound>
46+
<set-header name="X-Test" exists-action="override">
47+
<value>value</value>
48+
</set-header>
49+
</inbound>
50+
</policies>
51+
""",
52+
DisplayName = "Should compile explicit policy document with policies root"
53+
)]
54+
[DataRow(
55+
"""
56+
[Document( Type = DocumentType.Fragment )]
57+
public class PolicyFragment : IFragment
58+
{
59+
public void Fragment(IFragmentContext context)
60+
{
61+
context.SetHeader("X-Fragment", "fragment-value");
62+
}
63+
}
64+
""",
65+
"""
66+
<fragment>
67+
<set-header name="X-Fragment" exists-action="override">
68+
<value>fragment-value</value>
69+
</set-header>
70+
</fragment>
71+
""",
72+
DisplayName = "Should compile policy fragment with fragment root using Fragment method"
73+
)]
74+
[DataRow(
75+
"""
76+
[Document("my-fragment", Type = DocumentType.Fragment)]
77+
public class NamedPolicyFragment : IFragment
78+
{
79+
public void Fragment(IFragmentContext context)
80+
{
81+
context.SetHeader("X-Named-Fragment", "named-value");
82+
context.Base();
83+
}
84+
}
85+
""",
86+
"""
87+
<fragment>
88+
<set-header name="X-Named-Fragment" exists-action="override">
89+
<value>named-value</value>
90+
</set-header>
91+
<base />
92+
</fragment>
93+
""",
94+
DisplayName = "Should compile named policy fragment with multiple policies using Fragment method"
95+
)]
96+
public void ShouldCompileDocumentWithCorrectType(string code, string expectedXml)
97+
{
98+
code.CompileDocument().Should().BeSuccessful().And.DocumentEquivalentTo(expectedXml);
99+
}
100+
}

0 commit comments

Comments
 (0)