Skip to content

Commit 30fa6a7

Browse files
Add a feature to output a schema file for provisioning libraries (#54401)
1 parent c087903 commit 30fa6a7

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed

sdk/provisioning/Generator/src/GenerateOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ internal class GenerateOptions
99
{
1010
[Option(longName: "filter", shortName: 'f', Required = false, Hidden = false)]
1111
public string? Filter { get; set; }
12+
13+
[Option(longName: "generate-schema", shortName: 's', Required = false, Hidden = false, Default = true)]
14+
public bool GenerateSchema { get; set; }
1215
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
8+
namespace Azure.Provisioning.Generator.Model;
9+
10+
public abstract partial class Specification
11+
{
12+
private void GenerateSchema()
13+
{
14+
string? generationPath = GetGenerationPath();
15+
if (generationPath == null)
16+
{
17+
return;
18+
}
19+
20+
string path = Path.Combine(generationPath, "schema.log");
21+
22+
IndentWriter writer = new()
23+
{
24+
IndentText = " "
25+
};
26+
foreach (Resource resource in Resources)
27+
{
28+
using (writer.Scope($"resource {resource.Name} \"{resource.ResourceType}@{resource.DefaultResourceVersion}\" = {{", "}"))
29+
{
30+
var root = BuildSchemaTree(resource.Properties);
31+
Dictionary<ModelBase, string> visitedTypes = new()
32+
{
33+
[resource] = resource.Name
34+
};
35+
WriteSchemaObject(writer, root, resource.Name, visitedTypes);
36+
}
37+
writer.WriteLine();
38+
}
39+
File.WriteAllText(path, writer.ToString());
40+
}
41+
42+
private SchemaObject BuildSchemaTree(IList<Property> properties)
43+
{
44+
var root = new SchemaObject();
45+
foreach (Property property in properties)
46+
{
47+
// If path is null, use Name (camelCase)
48+
IList<string> path = property.Path ?? [property.Name.ToCamelCase()!];
49+
50+
SchemaObject current = root;
51+
for (int i = 0; i < path.Count; i++)
52+
{
53+
string segment = path[i];
54+
if (i == path.Count - 1)
55+
{
56+
current.Properties[segment] = new SchemaProperty(property);
57+
}
58+
else
59+
{
60+
if (!current.Properties.TryGetValue(segment, out var node))
61+
{
62+
node = new SchemaObject();
63+
current.Properties[segment] = node;
64+
}
65+
66+
if (node is SchemaObject obj)
67+
{
68+
current = obj;
69+
}
70+
else
71+
{
72+
throw new InvalidOperationException($"Conflict detected in schema generation. Path segment '{segment}' in property '{property.Name}' (Path: {string.Join(".", path)}) is already defined as a leaf property.");
73+
}
74+
}
75+
}
76+
}
77+
return root;
78+
}
79+
80+
private void WriteSchemaObject(IndentWriter writer, SchemaObject obj, string currentPath, Dictionary<ModelBase, string> visitedTypes)
81+
{
82+
foreach (var (name, node) in obj.Properties)
83+
{
84+
string childPath = $"{currentPath}.{name}";
85+
86+
if (node is SchemaObject childObj)
87+
{
88+
using (writer.Scope($"{name}: {{", "}"))
89+
{
90+
WriteSchemaObject(writer, childObj, childPath, visitedTypes);
91+
}
92+
}
93+
else if (node is SchemaProperty prop)
94+
{
95+
WritePropertySchema(writer, prop.Property, name, childPath, visitedTypes);
96+
}
97+
else
98+
{
99+
throw new InvalidOperationException($"Unknown schema node type: {node.GetType()}");
100+
}
101+
}
102+
}
103+
104+
private void WritePropertySchema(IndentWriter writer, Property property, string name, string currentPath, Dictionary<ModelBase, string> visitedTypes)
105+
{
106+
writer.Write($"{name}: ");
107+
if (property.IsReadOnly)
108+
{
109+
writer.Write("readonly ");
110+
}
111+
112+
switch (property.PropertyType)
113+
{
114+
case TypeModel complexType:
115+
if (visitedTypes.TryGetValue(complexType, out string? refPath))
116+
{
117+
writer.WriteLine($"&{refPath}");
118+
}
119+
else
120+
{
121+
visitedTypes[complexType] = currentPath;
122+
using (writer.Scope("{", "}"))
123+
{
124+
var root = BuildSchemaTree(complexType.Properties);
125+
WriteSchemaObject(writer, root, currentPath, visitedTypes);
126+
}
127+
}
128+
break;
129+
130+
case ListModel list when list.ElementType is TypeModel elementComplex:
131+
using (writer.Scope("[", "]"))
132+
{
133+
if (visitedTypes.TryGetValue(elementComplex, out string? listRefPath))
134+
{
135+
writer.WriteLine($"&{listRefPath}");
136+
}
137+
else
138+
{
139+
visitedTypes[elementComplex] = $"{currentPath}[]";
140+
using (writer.Scope("{", "}"))
141+
{
142+
var root = BuildSchemaTree(elementComplex.Properties);
143+
string elementPath = $"{currentPath}[]";
144+
WriteSchemaObject(writer, root, elementPath, visitedTypes);
145+
}
146+
}
147+
}
148+
break;
149+
150+
case ListModel listSimple:
151+
using (writer.Scope("[", "]"))
152+
{
153+
writer.WriteLine(GetSchemaType(listSimple.ElementType));
154+
}
155+
break;
156+
157+
case DictionaryModel dictionary:
158+
using (writer.Scope("{", "}"))
159+
{
160+
if (dictionary.ElementType is TypeModel dictComplexType)
161+
{
162+
if (visitedTypes.TryGetValue(dictComplexType, out string? dictRefPath))
163+
{
164+
writer.WriteLine($"{{customized property}}: &{dictRefPath}");
165+
}
166+
else
167+
{
168+
visitedTypes[dictComplexType] = $"{currentPath}.*";
169+
writer.WriteLine("{customized property}: {");
170+
using (writer.Scope())
171+
{
172+
var root = BuildSchemaTree(dictComplexType.Properties);
173+
string elementPath = $"{currentPath}.*";
174+
WriteSchemaObject(writer, root, elementPath, visitedTypes);
175+
}
176+
writer.WriteLine("}");
177+
}
178+
}
179+
else
180+
{
181+
writer.WriteLine($"{{customized property}}: {GetSchemaType(dictionary.ElementType)}");
182+
}
183+
}
184+
break;
185+
186+
default:
187+
writer.WriteLine(GetSchemaType(property.PropertyType));
188+
break;
189+
}
190+
}
191+
192+
private string GetSchemaType(ModelBase? type)
193+
{
194+
if (type is ExternalModel external)
195+
{
196+
string refName = external.GetTypeReference();
197+
return refName switch
198+
{
199+
"AzureLocation" => "'string'",
200+
"ETag" => "'string'",
201+
"Guid" => "'string'",
202+
"Uri" => "'string'",
203+
"DateTimeOffset" => "'string'",
204+
"ResourceIdentifier" => "'string'",
205+
"ResourceType" => "'string'",
206+
"object" => "any",
207+
"string" => "'string'",
208+
_ => refName
209+
};
210+
}
211+
if (type is EnumModel) return "'string'";
212+
if (type is SimpleModel || type is TypeModel) return "object";
213+
if (type is ListModel list) return $"{GetSchemaType(list.ElementType)}[]";
214+
if (type is DictionaryModel) return "object";
215+
if (type is Resource) return "object"; // Should not happen as property type usually
216+
return "any";
217+
}
218+
219+
private abstract class SchemaNode { }
220+
221+
private class SchemaObject : SchemaNode
222+
{
223+
public Dictionary<string, SchemaNode> Properties { get; } = [];
224+
}
225+
226+
private class SchemaProperty(Property property) : SchemaNode
227+
{
228+
public Property Property { get; } = property;
229+
}
230+
}

sdk/provisioning/Generator/src/Model/Specification.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public abstract partial class Specification : ModelBase
2626
// because it's merged with another spec that'll handle that for us
2727
public bool SkipCleaning { get; protected set; } = false;
2828

29+
public bool ShouldGenerateSchema { get; set; } = true;
30+
2931
public IList<Resource> Resources { get; private set; } = [];
3032

3133
public IList<Role> Roles { get; private set; } = [];
@@ -81,6 +83,11 @@ public void Build()
8183
{
8284
GenerateBuiltInRoles();
8385
}
86+
87+
if (ShouldGenerateSchema)
88+
{
89+
GenerateSchema();
90+
}
8491
});
8592
}
8693

sdk/provisioning/Generator/src/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ private static int Generate(GenerateOptions options)
6565
Dictionary<string, string> failures = [];
6666
foreach (Specification spec in baselineSpecs)
6767
{
68+
spec.ShouldGenerateSchema = options.GenerateSchema;
6869
try
6970
{
7071
Console.WriteLine($"Generating {spec.Name}...");
@@ -88,6 +89,7 @@ private static int Generate(GenerateOptions options)
8889
Console.WriteLine($"Skipping {spec.Name}...");
8990
continue;
9091
}
92+
spec.ShouldGenerateSchema = options.GenerateSchema;
9193
try
9294
{
9395
Console.WriteLine($"Generating {spec.Name}...");

0 commit comments

Comments
 (0)