Skip to content

Commit 62f4015

Browse files
committed
Add JSON converter for ApplicableTo and corresponding unit tests
1 parent 0d0910d commit 62f4015

File tree

3 files changed

+691
-0
lines changed

3 files changed

+691
-0
lines changed

src/Elastic.Documentation/AppliesTo/ApplicableTo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public interface IApplicableToElement
3434
}
3535

3636
[YamlSerializable]
37+
[JsonConverter(typeof(ApplicableToJsonConverter))]
3738
public record ApplicableTo
3839
{
3940
[YamlMember(Alias = "stack")]
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Reflection;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
8+
using YamlDotNet.Serialization;
9+
10+
namespace Elastic.Documentation.AppliesTo;
11+
12+
/// <summary>
13+
/// JSON converter for ApplicableTo that serializes to a flat array of objects with:
14+
/// - type: stack, deployment, serverless, or product
15+
/// - sub-type: the property name (e.g., "self", "ece", "elasticsearch", "ecctl")
16+
/// - lifecycle: the lifecycle value (if applicable)
17+
/// - version: the version value (if applicable)
18+
/// </summary>
19+
public class ApplicableToJsonConverter : JsonConverter<ApplicableTo>
20+
{
21+
public override ApplicableTo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
22+
{
23+
if (reader.TokenType == JsonTokenType.Null)
24+
return null;
25+
26+
if (reader.TokenType != JsonTokenType.StartArray)
27+
throw new JsonException("Expected array");
28+
29+
var result = new ApplicableTo();
30+
var deploymentProps = new Dictionary<string, List<Applicability>>();
31+
var serverlessProps = new Dictionary<string, List<Applicability>>();
32+
var productProps = new Dictionary<string, List<Applicability>>();
33+
var stackItems = new List<Applicability>();
34+
var productItems = new List<Applicability>();
35+
36+
while (reader.Read())
37+
{
38+
if (reader.TokenType == JsonTokenType.EndArray)
39+
break;
40+
41+
if (reader.TokenType != JsonTokenType.StartObject)
42+
throw new JsonException("Expected object");
43+
44+
string? type = null;
45+
string? subType = null;
46+
var lifecycle = ProductLifecycle.GenerallyAvailable;
47+
SemVersion? version = null;
48+
49+
while (reader.Read())
50+
{
51+
if (reader.TokenType == JsonTokenType.EndObject)
52+
break;
53+
54+
if (reader.TokenType != JsonTokenType.PropertyName)
55+
throw new JsonException("Expected property name");
56+
57+
var propertyName = reader.GetString();
58+
_ = reader.Read();
59+
60+
switch (propertyName)
61+
{
62+
case "type":
63+
type = reader.GetString();
64+
break;
65+
case "sub-type":
66+
subType = reader.GetString();
67+
break;
68+
case "lifecycle":
69+
var lifecycleStr = reader.GetString();
70+
if (lifecycleStr != null)
71+
lifecycle = ParseLifecycle(lifecycleStr);
72+
break;
73+
case "version":
74+
var versionStr = reader.GetString();
75+
if (versionStr != null && SemVersionConverter.TryParse(versionStr, out var v))
76+
version = v;
77+
else if (versionStr == "all" || string.IsNullOrEmpty(versionStr))
78+
version = AllVersions.Instance;
79+
break;
80+
}
81+
}
82+
83+
if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(subType))
84+
throw new JsonException("Missing type or sub-type");
85+
86+
var applicability = new Applicability { Lifecycle = lifecycle, Version = version };
87+
88+
switch (type)
89+
{
90+
case "stack":
91+
stackItems.Add(applicability);
92+
break;
93+
case "deployment":
94+
if (!deploymentProps.ContainsKey(subType))
95+
deploymentProps[subType] = [];
96+
deploymentProps[subType].Add(applicability);
97+
break;
98+
case "serverless":
99+
if (!serverlessProps.ContainsKey(subType))
100+
serverlessProps[subType] = [];
101+
serverlessProps[subType].Add(applicability);
102+
break;
103+
case "product" when subType == "product":
104+
productItems.Add(applicability);
105+
break;
106+
case "product":
107+
if (!productProps.ContainsKey(subType))
108+
productProps[subType] = [];
109+
productProps[subType].Add(applicability);
110+
break;
111+
}
112+
}
113+
114+
// Create Stack collection
115+
if (stackItems.Count > 0)
116+
result.Stack = new AppliesCollection(stackItems.ToArray());
117+
118+
// Create Product collection
119+
if (productItems.Count > 0)
120+
result.Product = new AppliesCollection(productItems.ToArray());
121+
122+
// Reconstruct DeploymentApplicability
123+
if (deploymentProps.Count > 0)
124+
{
125+
result.Deployment = new DeploymentApplicability
126+
{
127+
Self = deploymentProps.TryGetValue("self", out var self) ? new AppliesCollection(self.ToArray()) : null,
128+
Ece = deploymentProps.TryGetValue("ece", out var ece) ? new AppliesCollection(ece.ToArray()) : null,
129+
Eck = deploymentProps.TryGetValue("eck", out var eck) ? new AppliesCollection(eck.ToArray()) : null,
130+
Ess = deploymentProps.TryGetValue("ess", out var ess) ? new AppliesCollection(ess.ToArray()) : null
131+
};
132+
}
133+
134+
// Reconstruct ServerlessProjectApplicability
135+
if (serverlessProps.Count > 0)
136+
{
137+
result.Serverless = new ServerlessProjectApplicability
138+
{
139+
Elasticsearch = serverlessProps.TryGetValue("elasticsearch", out var es) ? new AppliesCollection(es.ToArray()) : null,
140+
Observability = serverlessProps.TryGetValue("observability", out var obs) ? new AppliesCollection(obs.ToArray()) : null,
141+
Security = serverlessProps.TryGetValue("security", out var sec) ? new AppliesCollection(sec.ToArray()) : null
142+
};
143+
}
144+
145+
// Reconstruct ProductApplicability
146+
if (productProps.Count > 0)
147+
{
148+
var productApplicability = new ProductApplicability();
149+
var productType = typeof(ProductApplicability);
150+
151+
foreach (var (key, items) in productProps)
152+
{
153+
// Find the property by YamlMember alias
154+
var property = productType.GetProperties()
155+
.FirstOrDefault(p => p.GetCustomAttribute<YamlMemberAttribute>()?.Alias == key);
156+
157+
property?.SetValue(productApplicability, new AppliesCollection(items.ToArray()));
158+
}
159+
160+
result.ProductApplicability = productApplicability;
161+
}
162+
163+
return result;
164+
}
165+
166+
public override void Write(Utf8JsonWriter writer, ApplicableTo value, JsonSerializerOptions options)
167+
{
168+
writer.WriteStartArray();
169+
170+
// Stack
171+
if (value.Stack != null)
172+
{
173+
WriteApplicabilityEntries(writer, "stack", "stack", value.Stack);
174+
}
175+
176+
// Deployment
177+
if (value.Deployment != null)
178+
{
179+
if (value.Deployment.Self != null)
180+
WriteApplicabilityEntries(writer, "deployment", "self", value.Deployment.Self);
181+
if (value.Deployment.Ece != null)
182+
WriteApplicabilityEntries(writer, "deployment", "ece", value.Deployment.Ece);
183+
if (value.Deployment.Eck != null)
184+
WriteApplicabilityEntries(writer, "deployment", "eck", value.Deployment.Eck);
185+
if (value.Deployment.Ess != null)
186+
WriteApplicabilityEntries(writer, "deployment", "ess", value.Deployment.Ess);
187+
}
188+
189+
// Serverless
190+
if (value.Serverless != null)
191+
{
192+
if (value.Serverless.Elasticsearch != null)
193+
WriteApplicabilityEntries(writer, "serverless", "elasticsearch", value.Serverless.Elasticsearch);
194+
if (value.Serverless.Observability != null)
195+
WriteApplicabilityEntries(writer, "serverless", "observability", value.Serverless.Observability);
196+
if (value.Serverless.Security != null)
197+
WriteApplicabilityEntries(writer, "serverless", "security", value.Serverless.Security);
198+
}
199+
200+
// Product (simple)
201+
if (value.Product != null)
202+
{
203+
WriteApplicabilityEntries(writer, "product", "product", value.Product);
204+
}
205+
206+
// ProductApplicability (specific products)
207+
if (value.ProductApplicability != null)
208+
{
209+
var productType = typeof(ProductApplicability);
210+
foreach (var property in productType.GetProperties())
211+
{
212+
var yamlAlias = property.GetCustomAttribute<YamlMemberAttribute>()?.Alias;
213+
if (yamlAlias != null)
214+
{
215+
var propertyValue = property.GetValue(value.ProductApplicability) as AppliesCollection;
216+
if (propertyValue != null)
217+
WriteApplicabilityEntries(writer, "product", yamlAlias, propertyValue);
218+
}
219+
}
220+
}
221+
222+
writer.WriteEndArray();
223+
}
224+
225+
private static ProductLifecycle ParseLifecycle(string lifecycleStr)
226+
{
227+
return lifecycleStr.ToLowerInvariant() switch
228+
{
229+
"preview" => ProductLifecycle.TechnicalPreview,
230+
"beta" => ProductLifecycle.Beta,
231+
"ga" => ProductLifecycle.GenerallyAvailable,
232+
"deprecated" => ProductLifecycle.Deprecated,
233+
"removed" => ProductLifecycle.Removed,
234+
"unavailable" => ProductLifecycle.Unavailable,
235+
"development" => ProductLifecycle.Development,
236+
"planned" => ProductLifecycle.Planned,
237+
"discontinued" => ProductLifecycle.Discontinued,
238+
_ => ProductLifecycle.GenerallyAvailable
239+
};
240+
}
241+
242+
private static void WriteApplicabilityEntries(Utf8JsonWriter writer, string type, string subType, AppliesCollection collection)
243+
{
244+
foreach (var applicability in collection)
245+
{
246+
writer.WriteStartObject();
247+
writer.WriteString("type", type);
248+
writer.WriteString("sub-type", subType);
249+
250+
// Write lifecycle
251+
var lifecycleName = applicability.Lifecycle switch
252+
{
253+
ProductLifecycle.TechnicalPreview => "preview",
254+
ProductLifecycle.Beta => "beta",
255+
ProductLifecycle.GenerallyAvailable => "ga",
256+
ProductLifecycle.Deprecated => "deprecated",
257+
ProductLifecycle.Removed => "removed",
258+
ProductLifecycle.Unavailable => "unavailable",
259+
ProductLifecycle.Development => "development",
260+
ProductLifecycle.Planned => "planned",
261+
ProductLifecycle.Discontinued => "discontinued",
262+
_ => "ga"
263+
};
264+
writer.WriteString("lifecycle", lifecycleName);
265+
266+
// Write version
267+
var isAllVersions = applicability.Version is null || ReferenceEquals(applicability.Version, AllVersions.Instance);
268+
if (!isAllVersions)
269+
writer.WriteString("version", applicability.Version!.ToString());
270+
else
271+
writer.WriteString("version", "all");
272+
273+
writer.WriteEndObject();
274+
}
275+
}
276+
}

0 commit comments

Comments
 (0)