Skip to content

Commit

Permalink
Merge pull request #47 from ejball/events
Browse files Browse the repository at this point in the history
  • Loading branch information
ejball authored Jul 4, 2024
2 parents b1c03ce + 2ced2d5 commit 3ef5774
Show file tree
Hide file tree
Showing 21 changed files with 182 additions and 30 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>

<PropertyGroup>
<VersionPrefix>2.13.0</VersionPrefix>
<PackageValidationBaselineVersion>2.12.1</PackageValidationBaselineVersion>
<VersionPrefix>2.14.0</VersionPrefix>
<PackageValidationBaselineVersion>2.13.0</PackageValidationBaselineVersion>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
4 changes: 4 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes

## 2.14.0

* Support events. (Must opt-in via `FsdParserSettings.SupportsEvents`.)

## 2.13.0

* Add `WriteNoIndent` and `WriteLineNoIndent` to `CodeWriter`.
Expand Down
10 changes: 5 additions & 5 deletions src/Facility.CodeGen.Console/CodeGeneratorApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public int Run(IReadOnlyList<string> args)
/// </summary>
protected virtual IReadOnlyList<string> ExtraUsage => [];

/// <summary>
/// Creates the service parser.
/// </summary>
protected abstract ServiceParser CreateParser();

/// <summary>
/// Creates the code generator.
/// </summary>
Expand All @@ -113,11 +118,6 @@ public int Run(IReadOnlyList<string> args)
/// <returns>The file generator settings.</returns>
protected abstract FileGeneratorSettings CreateSettings(ArgsReader args);

/// <summary>
/// Creates the service parser.
/// </summary>
protected virtual ServiceParser CreateParser() => new FsdParser();

private void WriteUsage(CodeGenerator generator)
{
System.Console.WriteLine($"Usage: {generator.GeneratorName} input output [options]");
Expand Down
1 change: 1 addition & 0 deletions src/Facility.Definition/CodeGen/FileGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class FileGenerator
/// <param name="generator">The code generator.</param>
/// <param name="settings">The settings.</param>
/// <returns>The number of updated files.</returns>
[Obsolete("Use the overload that takes a parser.")]
public static int GenerateFiles(CodeGenerator generator, FileGeneratorSettings settings) => GenerateFiles(new FsdParser(), generator, settings);

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Facility.Definition/Facility.Definition.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Facility.Definition.UnitTests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Faithlife.Parsing" />
</ItemGroup>
Expand Down
12 changes: 11 additions & 1 deletion src/Facility.Definition/Fsd/FsdGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ public sealed class FsdGenerator : CodeGenerator
/// <summary>
/// Generates an FSD file for a service definition.
/// </summary>
/// <param name="parser">The service parser.</param>
/// <param name="settings">The settings.</param>
/// <returns>The number of updated files.</returns>
public static int GenerateFsd(ServiceParser parser, FsdGeneratorSettings settings) =>
FileGenerator.GenerateFiles(parser, new FsdGenerator { GeneratorName = nameof(FsdGenerator) }, settings);

/// <summary>
/// Generates an FSD file for a service definition.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns>The number of updated files.</returns>
[Obsolete("Use the overload that takes a parser.")]
public static int GenerateFsd(FsdGeneratorSettings settings) =>
FileGenerator.GenerateFiles(new FsdGenerator { GeneratorName = nameof(FsdGenerator) }, settings);

Expand Down Expand Up @@ -52,7 +62,7 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service)
if (member is ServiceMethodInfo method)
{
WriteSummaryAndAttributes(code, method);
code.WriteLine($"method {method.Name}");
code.WriteLine($"{method.Kind.GetKeyword()} {method.Name}");
using (code.Block("{", "}:"))
WriteFields(code, method.RequestFields);
using (code.Block())
Expand Down
34 changes: 34 additions & 0 deletions src/Facility.Definition/Fsd/FsdParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ namespace Facility.Definition.Fsd;
/// </summary>
public sealed class FsdParser : ServiceParser
{
/// <summary>
/// Creates an FSD parser.
/// </summary>
public FsdParser(FsdParserSettings settings)
{
m_supportsEvents = settings.SupportsEvents;
}

/// <summary>
/// Creates an FSD parser.
/// </summary>
[Obsolete("Use the constructor that takes FsdParserSettings.")]
public FsdParser()
{
}

/// <summary>
/// Implements TryParseDefinition.
/// </summary>
Expand Down Expand Up @@ -63,6 +79,22 @@ protected override bool TryParseDefinitionCore(ServiceDefinitionText text, out S
: [.. targetMember.Remarks, .. s_oneEmptyString, .. remarksSection.Lines];
}
}

foreach (var method in service.AllMethods)
{
switch (method.Kind)
{
case ServiceMethodKind.Normal:
break;
case ServiceMethodKind.Event:
if (!m_supportsEvents)
errorList.Add(new ServiceDefinitionError("Events are not supported by this parser.", method.GetPart(ServicePartKind.Keyword)!.Position));
break;
default:
errorList.Add(new ServiceDefinitionError("Unexpected method kind.", method.GetPart(ServicePartKind.Keyword)!.Position));
break;
}
}
}
catch (ParseException exception)
{
Expand Down Expand Up @@ -201,4 +233,6 @@ private static void ReadRemarksAfterDefinition(ServiceDefinitionText source, Lis
private static readonly Regex s_interleavedMarkdown = new(@"^```fsd\b", RegexOptions.Multiline);
private static readonly Regex s_markdownHeading = new(@"^#\s+");
private static readonly string[] s_oneEmptyString = [""];

private readonly bool m_supportsEvents;
}
12 changes: 12 additions & 0 deletions src/Facility.Definition/Fsd/FsdParserSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Facility.Definition.Fsd;

/// <summary>
/// Settings for parsing an FSD file.
/// </summary>
public sealed class FsdParserSettings
{
/// <summary>
/// True to allow events.
/// </summary>
public bool SupportsEvents { get; set; }
}
4 changes: 2 additions & 2 deletions src/Facility.Definition/Fsd/FsdParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ private static IParser<ServiceMethodInfo> MethodParser(Context context) =>
from comments1 in CommentOrWhiteSpaceParser.Many()
from attributes in AttributeParser(context).Delimited(",").Bracketed("[", "]").Many()
from comments2 in CommentOrWhiteSpaceParser.Many()
from keyword in KeywordParser("method")
from keyword in KeywordParser("method", "event")
from name in NameParser.Named("method name")
from requestFields in FieldParser(context).Many().Bracketed("{", "}")
from colon in PunctuationParser(":")
from responseFields in FieldParser(context).Many().Bracketed("{", "}")
select new ServiceMethodInfo(name.Value, requestFields, responseFields,
select new ServiceMethodInfo(keyword.Value == "event" ? ServiceMethodKind.Event : ServiceMethodKind.Normal, name.Value, requestFields, responseFields,
attributes.SelectMany(x => x),
BuildSummary(comments1, comments2),
context.GetRemarksSection(name.Value)?.Lines,
Expand Down
9 changes: 9 additions & 0 deletions src/Facility.Definition/Http/HttpMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ internal HttpMethodInfo(ServiceMethodInfo methodInfo, ServiceInfo serviceInfo)
Method = "POST";
Path = $"/{methodInfo.Name}";
HttpStatusCode? statusCode = null;
var isEvent = methodInfo.Kind == ServiceMethodKind.Event;

foreach (var methodParameter in GetHttpParameters(methodInfo))
{
Expand Down Expand Up @@ -223,6 +224,14 @@ internal HttpMethodInfo(ServiceMethodInfo methodInfo, ServiceInfo serviceInfo)
ResponseHeaderFields = responseHeaderFields;
ValidResponses = [.. GetValidResponses(serviceInfo, statusCode, responseNormalFields, responseBodyFields).OrderBy(x => x.StatusCode)];

if (isEvent)
{
if (responseHeaderFields.Count != 0)
AddValidationError(new ServiceDefinitionError("Events do not support response headers.", methodInfo.Position));
if (ValidResponses.Count != 1)
AddValidationError(new ServiceDefinitionError("Events do not support multiple status codes.", methodInfo.Position));
}

var duplicateStatusCode = ValidResponses.GroupBy(x => x.StatusCode).FirstOrDefault(x => x.Count() > 1);
if (duplicateStatusCode is not null)
AddValidationError(new ServiceDefinitionError($"Multiple handlers for status code {(int) duplicateStatusCode.Key}.", methodInfo.Position));
Expand Down
20 changes: 15 additions & 5 deletions src/Facility.Definition/Http/HttpServiceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ public static bool TryCreate(ServiceInfo serviceInfo, out HttpServiceInfo httpSe
public string? Url { get; }

/// <summary>
/// The HTTP mapping for the methods.
/// The HTTP mapping for all methods (normal methods and event methods).
/// </summary>
public IReadOnlyList<HttpMethodInfo> Methods { get; }
public IReadOnlyList<HttpMethodInfo> AllMethods { get; }

/// <summary>
/// The HTTP mapping for normal methods.
/// </summary>
public IReadOnlyList<HttpMethodInfo> Methods => AllMethods.Where(x => x.ServiceMethod.Kind == ServiceMethodKind.Normal).ToList();

/// <summary>
/// The HTTP mapping for event methods.
/// </summary>
public IReadOnlyList<HttpMethodInfo> Events => AllMethods.Where(x => x.ServiceMethod.Kind == ServiceMethodKind.Event).ToList();

/// <summary>
/// The HTTP mapping for the error sets.
Expand All @@ -47,7 +57,7 @@ public static bool TryCreate(ServiceInfo serviceInfo, out HttpServiceInfo httpSe
/// <summary>
/// The children of the element, if any.
/// </summary>
public override IEnumerable<HttpElementInfo> GetChildren() => Methods.AsEnumerable<HttpElementInfo>().Concat(ErrorSets);
public override IEnumerable<HttpElementInfo> GetChildren() => AllMethods.AsEnumerable<HttpElementInfo>().Concat(ErrorSets);

private HttpServiceInfo(ServiceInfo serviceInfo)
{
Expand All @@ -73,10 +83,10 @@ private HttpServiceInfo(ServiceInfo serviceInfo)
}
}

Methods = serviceInfo.Methods.Select(x => new HttpMethodInfo(x, serviceInfo)).ToList();
AllMethods = serviceInfo.AllMethods.Select(x => new HttpMethodInfo(x, serviceInfo)).ToList();
ErrorSets = serviceInfo.ErrorSets.Select(x => new HttpErrorSetInfo(x)).ToList();

var methodsByRoute = Methods.OrderBy(x => x, HttpMethodInfo.ByRouteComparer).ToList();
var methodsByRoute = AllMethods.OrderBy(x => x, HttpMethodInfo.ByRouteComparer).ToList();
for (var index = 1; index < methodsByRoute.Count; index++)
{
var left = methodsByRoute[index - 1];
Expand Down
7 changes: 7 additions & 0 deletions src/Facility.Definition/ServiceDefinitionUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ internal static ServiceDefinitionError CreateInvalidAttributeValueError(string a
/// </summary>
public static bool IsValidName(string? name) => name is not null && s_validNameRegex.IsMatch(name);

internal static string GetKeyword(this ServiceMethodKind kind) => kind switch
{
ServiceMethodKind.Normal => "method",
ServiceMethodKind.Event => "event",
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};

internal static IReadOnlyList<T> ToReadOnlyList<T>(this IEnumerable<T>? items) => new ReadOnlyCollection<T>((items ?? []).ToList());

#if NET6_0_OR_GREATER
Expand Down
17 changes: 14 additions & 3 deletions src/Facility.Definition/ServiceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,24 @@ public ServiceInfo(string name, IEnumerable<ServiceMemberInfo> members, IEnumera
}

/// <summary>
/// All of the service members..
/// All service members.
/// </summary>
public IReadOnlyList<ServiceMemberInfo> Members { get; }

/// <summary>
/// The methods.
/// All methods (normal methods and event methods).
/// </summary>
public IReadOnlyList<ServiceMethodInfo> Methods => Members.OfType<ServiceMethodInfo>().ToReadOnlyList();
public IReadOnlyList<ServiceMethodInfo> AllMethods => Members.OfType<ServiceMethodInfo>().ToReadOnlyList();

/// <summary>
/// The normal methods.
/// </summary>
public IReadOnlyList<ServiceMethodInfo> Methods => Members.OfType<ServiceMethodInfo>().Where(x => x.Kind == ServiceMethodKind.Normal).ToReadOnlyList();

/// <summary>
/// The event methods.
/// </summary>
public IReadOnlyList<ServiceMethodInfo> Events => Members.OfType<ServiceMethodInfo>().Where(x => x.Kind == ServiceMethodKind.Event).ToReadOnlyList();

/// <summary>
/// The DTOs.
Expand Down Expand Up @@ -125,6 +135,7 @@ ServiceMemberInfo DoExcludeTag(ServiceMemberInfo member)
if (member is ServiceMethodInfo method)
{
return new ServiceMethodInfo(
kind: method.Kind,
name: method.Name,
requestFields: method.RequestFields.Where(ShouldNotExclude),
responseFields: method.ResponseFields.Where(ShouldNotExclude),
Expand Down
14 changes: 14 additions & 0 deletions src/Facility.Definition/ServiceMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,29 @@ public sealed class ServiceMethodInfo : ServiceMemberInfo
/// Creates a method.
/// </summary>
public ServiceMethodInfo(string name, IEnumerable<ServiceFieldInfo>? requestFields = null, IEnumerable<ServiceFieldInfo>? responseFields = null, IEnumerable<ServiceAttributeInfo>? attributes = null, string? summary = null, IEnumerable<string>? remarks = null, params ServicePart[] parts)
: this(ServiceMethodKind.Normal, name, requestFields, responseFields, attributes, summary, remarks, parts)
{
}

/// <summary>
/// Creates a method.
/// </summary>
public ServiceMethodInfo(ServiceMethodKind kind, string name, IEnumerable<ServiceFieldInfo>? requestFields = null, IEnumerable<ServiceFieldInfo>? responseFields = null, IEnumerable<ServiceAttributeInfo>? attributes = null, string? summary = null, IEnumerable<string>? remarks = null, params ServicePart[] parts)
: base(name, attributes, summary, remarks, parts)
{
Kind = kind;
RequestFields = requestFields.ToReadOnlyList();
ResponseFields = responseFields.ToReadOnlyList();

ValidateNoDuplicateNames(RequestFields, "request field");
ValidateNoDuplicateNames(ResponseFields, "response field");
}

/// <summary>
/// The kind of the method.
/// </summary>
public ServiceMethodKind Kind { get; }

/// <summary>
/// The request fields of the method.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/Facility.Definition/ServiceMethodKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Facility.Definition;

/// <summary>
/// The kind of service method.
/// </summary>
public enum ServiceMethodKind
{
/// <summary>
/// A normal method.
/// </summary>
Normal,

/// <summary>
/// An event, i.e. a method that can indefinitely and repeatedly provide responses.
/// </summary>
Event,
}
2 changes: 2 additions & 0 deletions src/fsdgenfsd/FsdGenFsdApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public sealed class FsdGenFsdApp : CodeGeneratorApp
" Generate file-scoped service.",
];

protected override ServiceParser CreateParser() => new FsdParser(new FsdParserSettings { SupportsEvents = true });

protected override CodeGenerator CreateGenerator() => new FsdGenerator();

protected override FileGeneratorSettings CreateSettings(ArgsReader args) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Globalization;
using System.Net;
using Facility.Definition.Fsd;
using Facility.Definition.Http;
using FluentAssertions;
using NUnit.Framework;
Expand Down Expand Up @@ -675,7 +674,7 @@ public void NonSimpleFieldTypeNotSupported(string type)
public void ByRouteComparer(string leftHttp, string rightHttp, int expected)
{
var fsdText = "service TestApi { [left] method left { id: string; }: {} [right] method right { id: string; }: {} }".Replace("[left]", leftHttp).Replace("[right]", rightHttp);
var service = HttpServiceInfo.Create(new FsdParser().ParseDefinition(new ServiceDefinitionText("", fsdText)));
var service = HttpServiceInfo.Create(TestUtility.CreateParser().ParseDefinition(new ServiceDefinitionText("", fsdText)));
var left = service.Methods.Single(x => x.ServiceMethod.Name == "left");
var right = service.Methods.Single(x => x.ServiceMethod.Name == "right");
var actual = HttpMethodInfo.ByRouteComparer.Compare(left, right);
Expand Down
13 changes: 13 additions & 0 deletions tests/Facility.Definition.UnitTests/Http/HttpServiceInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public void EmptyServiceDefinition()
info.Service.Name.Should().Be("TestApi");
info.Url.Should().BeNull();
info.Methods.Count.Should().Be(0);
info.Events.Count.Should().Be(0);
info.ErrorSets.Count.Should().Be(0);
}

Expand All @@ -23,6 +24,18 @@ public void OneMinimalMethod()
info.Service.Name.Should().Be("TestApi");
info.Url.Should().BeNull();
info.Methods.Count.Should().Be(1);
info.Events.Count.Should().Be(0);
info.ErrorSets.Count.Should().Be(0);
}

[Test]
public void OneMinimalEvent()
{
var info = ParseHttpApi("service TestApi { event do {}: {} }");
info.Service.Name.Should().Be("TestApi");
info.Url.Should().BeNull();
info.Methods.Count.Should().Be(0);
info.Events.Count.Should().Be(1);
info.ErrorSets.Count.Should().Be(0);
}

Expand Down
Loading

0 comments on commit 3ef5774

Please sign in to comment.