Skip to content

Commit 08fb829

Browse files
(dotnet) Migrate gherkin messages to cucumber messages (#426)
This PR enhances the .Net implementation such that the Message events and pickles that are generated use the Cucumber/Messages types instead of the Gherkin.Cucumber.Messages.Types which had been embedded in the Gherkin project. * Add test data for star keywords The starkey word is kinda special because its step type is unknown. How different implementations handle this seems to be inconsistent. This should highlight some problems. Co-authored-by: M.P. Korstanje <[email protected]>
1 parent d22a7f9 commit 08fb829

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+396
-796
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
1616

1717
### Changed
1818
- Fixed Afrikaans translation for "rule" ([#428](https://github.com/cucumber/gherkin/pull/428))
19+
- [.NET] Migrated to the use of Cucumber/Messages. Eliminated the built-in Gherkin.CucumberMessages.Types ([#426](https://github.com/cucumber/gherkin/pull/426))
1920

2021
### Removed
2122
- [Python] Dropped legacy `.egg-info` metadata distribution artifacts
@@ -28,6 +29,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
2829
- [Go] Trim trailing tab characters from title and step lines ([#441](https://github.com/cucumber/gherkin/pull/441))
2930
- [Java] Use a more consistent definition of whitespace ([#442](https://github.com/cucumber/gherkin/pull/442))
3031

32+
3133
## [33.0.0] - 2025-07-07
3234
### Changed
3335
- [Elixir, Go, JavaScript, Java, Perl, Php, Ruby] Update dependency messages to v28 ([#420](https://github.com/cucumber/gherkin/pull/420))

dotnet/Gherkin.Specs/AstBuildingTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using FluentAssertions;
2-
using Gherkin.CucumberMessages.Types;
2+
using Io.Cucumber.Messages.Types;
33
using Gherkin.Specs.Helper;
44
using Xunit;
55

dotnet/Gherkin.Specs/CLI/Program.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Gherkin.CucumberMessages;
22
using Gherkin.Specs.EventStubs;
3+
using Gherkin.Specs.Helper;
34
using Gherkin.Specs.Tokens;
45
using System.Text.Json;
56
using System.Text.Json.Serialization;
@@ -100,11 +101,8 @@ private static int PrintEvents(PrintEventsArgs args)
100101
{
101102
foreach (var evt in gherkinEventsProvider.GetEvents(sourceEventEvent))
102103
{
103-
var jsonString = JsonSerializer.Serialize(evt, new JsonSerializerOptions(JsonSerializerDefaults.Web)
104-
{
105-
Converters = { new JsonStringEnumConverter() },
106-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
107-
});
104+
var jsonString = JsonSerializer.Serialize(evt, NDJsonParser.SerializerOptions);
105+
108106
Console.WriteLine(jsonString);
109107
}
110108
}
Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Gherkin.CucumberMessages;
22
using Gherkin.CucumberMessages.Pickles;
3-
using Gherkin.CucumberMessages.Types;
3+
using Io.Cucumber.Messages.Types;
44

55
namespace Gherkin.Specs.EventStubs;
66

@@ -20,28 +20,18 @@ public IEnumerable<Envelope> GetEvents(Source source)
2020

2121
if (printSource)
2222
{
23-
events.Add(new Envelope
24-
{
25-
Source = source
26-
});
23+
events.Add(Envelope.Create(source));
2724
}
2825
if (printAst)
2926
{
30-
events.Add(new Envelope
31-
{
32-
GherkinDocument =
33-
_astMessagesConverter.ConvertGherkinDocumentToEventArgs(gherkinDocument, source.Uri)
34-
});
27+
events.Add(Envelope.Create(_astMessagesConverter.ConvertGherkinDocumentToEventArgs(gherkinDocument, source.Uri)));
3528
}
3629
if (printPickles)
3730
{
3831
var pickles = _pickleCompiler.Compile(_astMessagesConverter.ConvertGherkinDocumentToEventArgs(gherkinDocument, source.Uri));
3932
foreach (Pickle pickle in pickles)
4033
{
41-
events.Add(new Envelope
42-
{
43-
Pickle = pickle
44-
});
34+
events.Add(Envelope.Create(pickle));
4535
}
4636
}
4737
}
@@ -62,17 +52,21 @@ public IEnumerable<Envelope> GetEvents(Source source)
6252

6353
private void AddParseError(List<Envelope> events, ParserException e, String uri)
6454
{
65-
events.Add(new Envelope
66-
{
67-
ParseError = new ParseError()
68-
{
69-
Message = e.Message,
70-
Source = new SourceReference()
71-
{
72-
Location = e.Location.HasValue ? new Location(e.Location.GetValueOrDefault().Column, e.Location.GetValueOrDefault().Line) : null,
73-
Uri = uri
74-
}
75-
}
76-
});
55+
// This forces the suppression of the Column value in the Location when the Location is zero (the default)
56+
long? col = null;
57+
if (e.Location.HasValue && e.Location.Value.Column != 0)
58+
col = e.Location.Value.Column;
59+
60+
events.Add(Envelope.Create(
61+
new ParseError(
62+
new SourceReference(
63+
uri, // Add the missing 'uri' parameter here
64+
null, // Assuming JavaMethod is not needed
65+
null, // Assuming JavaStackTraceElement is not needed
66+
e.Location.HasValue ? new Location(e.Location.GetValueOrDefault().Line, col) : null
67+
),
68+
e.Message
69+
)
70+
));
7771
}
7872
}
Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1-
using Gherkin.CucumberMessages.Types;
1+
using Io.Cucumber.Messages.Types;
22

33
namespace Gherkin.Specs.EventStubs;
44

55
public class SourceProvider
66
{
7-
private const string GherkinMediaType = "text/x.cucumber.gherkin+plain";
8-
97
public IEnumerable<Source> GetSources(IEnumerable<string> paths)
108
{
119
foreach (var path in paths)
1210
{
1311
string data = File.ReadAllText(path);
14-
yield return new Source
15-
{
16-
Data = data,
17-
Uri = path,
18-
MediaType = GherkinMediaType
19-
};
12+
yield return new Source(path, data, SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN);
2013
}
2114
}
22-
2315
}

dotnet/Gherkin.Specs/EventTestBase.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Text;
33
using FluentAssertions;
44
using Gherkin.CucumberMessages;
5-
using Gherkin.CucumberMessages.Types;
5+
using Io.Cucumber.Messages.Types;
66
using Gherkin.Specs.EventStubs;
77
using Gherkin.Specs.Helper;
88

@@ -18,7 +18,35 @@ protected void AssertEvents(string testFeatureFile, List<Envelope> actualGherkin
1818
config => config
1919
.AllowingInfiniteRecursion()
2020
.IgnoringCyclicReferences()
21-
.Excluding(ghe => ghe.Path.EndsWith("Uri"))
21+
.ComparingByMembers<Background>()
22+
.ComparingByMembers<Comment>()
23+
.ComparingByMembers<Io.Cucumber.Messages.Types.DataTable>()
24+
.ComparingByMembers<DocString>()
25+
.ComparingByMembers<Envelope>()
26+
.ComparingByMembers<Examples>()
27+
.ComparingByMembers<Feature>()
28+
.ComparingByMembers<FeatureChild>()
29+
.ComparingByMembers<GherkinDocument>()
30+
.ComparingByMembers<Location>()
31+
.ComparingByMembers<ParseError>()
32+
.ComparingByMembers<Pickle>()
33+
.ComparingByMembers<PickleDocString>()
34+
.ComparingByMembers<PickleStep>()
35+
.ComparingByMembers<PickleStepArgument>()
36+
.ComparingByMembers<PickleTable>()
37+
.ComparingByMembers<PickleTableCell>()
38+
.ComparingByMembers<PickleTableRow>()
39+
.ComparingByMembers<PickleTag>()
40+
.ComparingByMembers<Rule>()
41+
.ComparingByMembers<RuleChild>()
42+
.ComparingByMembers<Scenario>()
43+
.ComparingByMembers<Source>()
44+
.ComparingByMembers<SourceReference>()
45+
.ComparingByMembers<Step>()
46+
.ComparingByMembers<TableCell>()
47+
.ComparingByMembers<TableRow>()
48+
.ComparingByMembers<Tag>()
49+
.Excluding(ghe => ghe.Path.EndsWith("uri", StringComparison.InvariantCultureIgnoreCase))
2250
.Using<string>(ctx =>
2351
{
2452
var replacedSubject = NormalizeNewLines(ctx.Subject);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
8+
9+
namespace Gherkin.Specs.Helper
10+
{
11+
public class CucumberMessagesEnumConverterFactory : JsonConverterFactory
12+
{
13+
private static readonly ConcurrentDictionary<Type, JsonConverter> _cache = new();
14+
private static readonly HashSet<Type> _enumTypes;
15+
16+
static CucumberMessagesEnumConverterFactory()
17+
{
18+
// Discover all enums in Io.Cucumber.Messages.Types
19+
var typesNamespace = "Io.Cucumber.Messages.Types";
20+
_enumTypes = AppDomain.CurrentDomain.GetAssemblies()
21+
.SelectMany(a => SafeGetTypes(a))
22+
.Where(t => t.IsEnum && t.Namespace == typesNamespace)
23+
.ToHashSet();
24+
}
25+
26+
private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
27+
{
28+
try { return assembly.GetTypes(); } catch { return Array.Empty<Type>(); }
29+
}
30+
31+
public override bool CanConvert(Type typeToConvert)
32+
{
33+
return _enumTypes.Contains(typeToConvert);
34+
}
35+
36+
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
37+
{
38+
return _cache.GetOrAdd(typeToConvert, t =>
39+
{
40+
var converterType = typeof(DescriptionEnumConverter<>).MakeGenericType(t);
41+
return (JsonConverter)Activator.CreateInstance(converterType);
42+
});
43+
}
44+
}
45+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Reflection;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
8+
namespace Gherkin.Specs.Helper
9+
{
10+
public class DescriptionEnumConverter<T> : JsonConverter<T> where T : struct, Enum
11+
{
12+
private readonly Dictionary<T, string> _enumToString = new();
13+
private readonly Dictionary<string, T> _stringToEnum = new();
14+
15+
public DescriptionEnumConverter()
16+
{
17+
var type = typeof(T);
18+
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static))
19+
{
20+
#pragma warning disable CS8605 // Unboxing a possibly null value.
21+
var value = (T)field.GetValue(null);
22+
#pragma warning restore CS8605 // Unboxing a possibly null value.
23+
var attribute = field.GetCustomAttribute<DescriptionAttribute>();
24+
if (attribute == null || string.IsNullOrEmpty(attribute.Description))
25+
throw new InvalidOperationException($"Enum {type.Name} field {field.Name} does not have a Description attribute or the Description attribute is empty.");
26+
var name = attribute.Description;
27+
_enumToString[value] = name;
28+
_stringToEnum[name] = value;
29+
}
30+
}
31+
32+
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
33+
{
34+
var stringValue = reader.GetString();
35+
return _stringToEnum.TryGetValue(stringValue!, out var enumValue) ? enumValue : default;
36+
}
37+
38+
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
39+
{
40+
writer.WriteStringValue(_enumToString.TryGetValue(value, out var stringValue) ? stringValue : value.ToString());
41+
}
42+
}
43+
44+
}

dotnet/Gherkin.Specs/Helper/NDJsonParser.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1+
using MessageTypes = Io.Cucumber.Messages.Types;
12
using System.Text.Json;
23
using System.Text.Json.Serialization;
34

45
namespace Gherkin.Specs.Helper;
56

67
public class NDJsonParser
78
{
8-
static readonly JsonSerializerOptions s_SerializerOptions = new(JsonSerializerDefaults.Web)
9+
private static readonly Lazy<JsonSerializerOptions> _serializerOptions = new(() =>
910
{
10-
Converters =
11-
{
12-
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
13-
}
14-
};
11+
var options = new JsonSerializerOptions();
12+
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
13+
options.Converters.Add(new CucumberMessagesEnumConverterFactory());
14+
options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
15+
options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
16+
return options;
17+
});
18+
19+
public static JsonSerializerOptions SerializerOptions => _serializerOptions.Value;
1520

1621
public static async Task<List<T>> DeserializeAsync<T>(string expectedFile)
1722
{
1823
var result = new List<T>();
1924
using var contentStream = File.OpenRead(expectedFile);
2025

21-
await foreach (var deserializedObject in JsonSerializer.DeserializeAsyncEnumerable<T>(contentStream, true, s_SerializerOptions))
26+
await foreach (var deserializedObject in JsonSerializer.DeserializeAsyncEnumerable<T>(contentStream, true, SerializerOptions))
2227
{
2328
result.Add(deserializedObject);
2429
}

dotnet/Gherkin.Specs/PicklesTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using FluentAssertions;
2-
using Gherkin.CucumberMessages.Types;
2+
using Io.Cucumber.Messages.Types;
33
using Gherkin.Specs.Helper;
44
using Xunit;
55

0 commit comments

Comments
 (0)