Skip to content

Commit

Permalink
Merge pull request #50 from Kentico/feature/DEL-1120_Add_generator_fo…
Browse files Browse the repository at this point in the history
…r_CM_API_models

Add option to generate models for CM API SDK
  • Loading branch information
Jan Lenoch authored Dec 13, 2017
2 parents a37c7ed + 4e99dea commit 2787423
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 51 deletions.
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Kentico Cloud model generator utility for .NET

This utility generates strongly-typed models based on Content Types in a Kentico Cloud project. The models are supposed to be used together with the [Kentico Cloud Delivery SDK for .NET](https://github.com/Kentico/delivery-sdk-net). Please read the [documentation](https://github.com/Kentico/delivery-sdk-net/wiki/Working-with-Strongly-Typed-Models-(aka-Code-First-Approach)#customizing-the-strong-type-binding-logic) to see all benefits of this approach.
This utility generates strongly-typed models based on Content Types in a Kentico Cloud project. The models are supposed to be used together with the [Kentico Cloud Delivery SDK for .NET](https://github.com/Kentico/delivery-sdk-net) or [Kentico Cloud Content Management SDK for .NET](https://github.com/Kentico/content-management-sdk-net). Please read the [documentation](https://github.com/Kentico/delivery-sdk-net/wiki/Working-with-Strongly-Typed-Models-(aka-Code-First-Approach)#customizing-the-strong-type-binding-logic) to see all benefits of this approach.


## Get the tool
Expand All @@ -23,7 +23,7 @@ Note: The application is [self-contained](https://www.hanselman.com/blog/Selfcon

See the [list of all RIDs](https://docs.microsoft.com/en-us/dotnet/articles/core/rid-catalog).

## How to use
## How to use for Delivery SDK

### Windows

Expand All @@ -50,7 +50,7 @@ dotnet run --projectid "<projectid>" [--namespace "<custom-namespace>"] [--outpu
These parameters can also be set via the appSettings.json file located in the same directory as the executable file. Command-line parameters always take precedence.


## Example output
### Example output

```csharp
using System;
Expand All @@ -75,6 +75,59 @@ namespace KenticoCloudModels
}
```

## How to use for Content Management SDK

### Windows

```
CloudModelGenerator.exe --projectid "<projectid>" --contentmanagementapi [--namespace "<custom-namespace>"] [--outputdir "<output-directory>"] [--filenamesuffix "<suffix>"]
```

### Linux, Mac OS and other platforms
```
dotnet run --projectid "<projectid>" --contentmanagementapi [--namespace "<custom-namespace>"] [--outputdir "<output-directory>"] [--filenamesuffix "<suffix>"]
```

### Parameters

| Parameter | Required | Default value | Description |
| --------------------- |:---------:|:--------------:|:-----------:|
| `--projectid` | True | `null` | A GUID that can be found in [Kentico Cloud](https://app.kenticocloud.com) -> API keys -> Project ID |
| `--contentmanagementapi` | True | `false` | Indicates that models should be generated for [Content Management SDK](https://github.com/Kentico/content-management-sdk-net) |
| `--namespace` | False | `KenticoCloudModels` | A name of the [C# namespace](https://msdn.microsoft.com/en-us/library/z2kcy19k.aspx) |
| `--outputdir` | False | `\.` | An output folder path |
| `--filenamesuffix` | False | `null` | Adds a suffix to generated filenames (e.g., News.cs becomes News.Generated.cs) |

These parameters can also be set via the appSettings.json file located in the same directory as the executable file. Command-line parameters always take precedence.


### Example output

```csharp
using System;
using System.Collections.Generic;
using KenticoCloud.ContentManagement.Models.Assets;
using KenticoCloud.ContentManagement.Models.Items;
using Newtonsoft.Json;

namespace KenticoCloudModels
{
public partial class CompleteContentType
{
public string Text { get; set; }
public string RichText { get; set; }
public decimal? Number { get; set; }
public IEnumerable<MultipleChoiceOptionIdentifier> MultipleChoice { get; set; }
public DateTime? DateTime { get; set; }
public IEnumerable<AssetIdentifier> Asset { get; set; }
public IEnumerable<ContentItemIdentifier> ModularContent { get; set; }
public IEnumerable<TaxonomyTermIdentifier> Taxonomy { get; set; }
public string UrlSlug { get; set; }
}
}
```


## Feedback & Contributing
Check out the [contributing](https://github.com/Kentico/cloud-generators-net/blob/master/CONTRIBUTING.md) page to see the best places to file issues, start discussions and begin contributing.

Expand Down
63 changes: 48 additions & 15 deletions src/CloudModelGenerator/ClassCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,53 @@ public ClassCodeGenerator(ClassDefinition classDefinition, string classFilename,
OverwriteExisting = !CustomPartial;
}

public string GenerateCode()
public string GenerateCode(bool cmApi = false)
{
var cmApiUsings = new[]
{
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("Newtonsoft.Json")),
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("KenticoCloud.ContentManagement.Models.Items"))
};

var deliveryUsings = new[]
{
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("KenticoCloud.Delivery"))
};

var usings = new[]
{
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System")),
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System.Collections.Generic")),
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("KenticoCloud.Delivery"))
};
}.Concat(cmApi ? cmApiUsings : deliveryUsings).ToArray();

var properties = ClassDefinition.Properties.Select(element =>
SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(element.TypeName), element.Identifier)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
)
var properties = ClassDefinition.Properties.Select((element, i) =>
{
var property = SyntaxFactory
.PropertyDeclaration(SyntaxFactory.ParseTypeName(element.TypeName), element.Identifier)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
);
if (cmApi && ClassDefinition.PropertyCodenameConstants.Count > i)
{
property = property.AddAttributeLists(
SyntaxFactory.AttributeList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("JsonProperty"))
.WithArgumentList(
SyntaxFactory.AttributeArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(ClassDefinition
.PropertyCodenameConstants[i].Codename)))))))));
}
return property;
}
).ToArray();

var propertyCodenameConstants = ClassDefinition.PropertyCodenameConstants.Select(element =>
Expand All @@ -71,7 +100,7 @@ public string GenerateCode()
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword));

if (!CustomPartial)
if (!CustomPartial && !cmApi)
{
var classCodenameConstant = SyntaxFactory.FieldDeclaration(
SyntaxFactory.VariableDeclaration(
Expand All @@ -91,8 +120,12 @@ public string GenerateCode()
classDeclaration = classDeclaration.AddMembers(classCodenameConstant);
}

classDeclaration = classDeclaration.AddMembers(propertyCodenameConstants)
.AddMembers(properties);
if (!cmApi)
{
classDeclaration = classDeclaration.AddMembers(propertyCodenameConstants);
}

classDeclaration = classDeclaration.AddMembers(properties);

var description = SyntaxFactory.Comment(
@"// This code was generated by a cloud-generators-net tool
Expand Down
21 changes: 12 additions & 9 deletions src/CloudModelGenerator/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void GenerateContentTypeModels(bool structuredModel = false)

foreach (var codeGenerator in classCodeGenerators)
{
SaveToFile(codeGenerator.GenerateCode(), codeGenerator.ClassFilename, codeGenerator.OverwriteExisting);
SaveToFile(codeGenerator.GenerateCode(_options.ContentManagementApi), codeGenerator.ClassFilename, codeGenerator.OverwriteExisting);
}

Console.WriteLine($"{classCodeGenerators.Count()} content type models were successfully created.");
Expand Down Expand Up @@ -113,11 +113,11 @@ private ClassCodeGenerator GetClassCodeGenerator(ContentType contentType, bool s
try
{
var elementType = element.Type;
if (structuredModel && Property.IsContentTypeSupported(elementType + Property.STRUCTURED_SUFFIX))
if (structuredModel && Property.IsContentTypeSupported(elementType + Property.STRUCTURED_SUFFIX, _options.ContentManagementApi))
{
elementType += Property.STRUCTURED_SUFFIX;
}
var property = Property.FromContentType(element.Codename, elementType);
var property = Property.FromContentType(element.Codename, elementType, _options.ContentManagementApi);
classDefinition.AddPropertyCodenameConstant(element);
classDefinition.AddProperty(property);
}
Expand All @@ -135,13 +135,16 @@ private ClassCodeGenerator GetClassCodeGenerator(ContentType contentType, bool s
}
}

try
if (!_options.ContentManagementApi)
{
classDefinition.AddSystemProperty();
}
catch (InvalidOperationException)
{
Console.WriteLine($"Warning: Can't add 'System' property. It's in collision with existing element in Content Type '{classDefinition.ClassName}'.");
try
{
classDefinition.AddSystemProperty();
}
catch (InvalidOperationException)
{
Console.WriteLine($"Warning: Can't add 'System' property. It's in collision with existing element in Content Type '{classDefinition.ClassName}'.");
}
}

string suffix = string.IsNullOrEmpty(_options.FileNameSuffix) ? "" : $".{_options.FileNameSuffix}";
Expand Down
6 changes: 6 additions & 0 deletions src/CloudModelGenerator/Configuration/CodeGeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,11 @@ public class CodeGeneratorOptions
/// Indicates whether the classes should be generated with types that represent structured data model
/// </summary>
public bool StructuredModel { get; set; } = false;


/// <summary>
/// Indicates whether the classes should be generated for CM API SDK
/// </summary>
public bool ContentManagementApi { get; set; } = false;
}
}
3 changes: 2 additions & 1 deletion src/CloudModelGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ static int Main(string[] args)
app.Option("-gp|--generatepartials", "Generate partial classes for customization (if this option is set filename suffix will default to Generated).", CommandOptionType.NoValue);
app.Option("-t|--withtypeprovider", "Indicates whether the CustomTypeProvider class should be generated.", CommandOptionType.NoValue);
app.Option("-s|--structuredmodel", "Indicates whether the classes should be generated with types that represent structured data model.", CommandOptionType.NoValue);
app.Option("-cma|--contentmanagementapi", "Indicates whether the classes should be generated for CM API SDK instead.", CommandOptionType.NoValue);

app.OnExecute(() =>
{
Expand Down Expand Up @@ -52,7 +53,7 @@ static int Main(string[] args)

codeGenerator.GenerateContentTypeModels(options.StructuredModel);

if (options.WithTypeProvider)
if (!options.ContentManagementApi && options.WithTypeProvider)
{
codeGenerator.GenerateTypeProvider();
}
Expand Down
32 changes: 22 additions & 10 deletions src/CloudModelGenerator/Property.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class Property
/// </summary>
public string TypeName { get; private set; }

private static Dictionary<string, string> contentTypeToTypeName = new Dictionary<string, string>()
private static readonly Dictionary<string, string> DeliverTypes = new Dictionary<string, string>
{
{ "text", "string" },
{ "rich_text", "string" },
Expand All @@ -28,29 +28,41 @@ public class Property
{ "url_slug", "string" }
};

private static readonly Dictionary<string, string> ContentManagementTypes = new Dictionary<string, string>
{
{ "text", "string" },
{ "rich_text", "string" },
{ "number", "decimal?" },
{ "multiple_choice", "IEnumerable<MultipleChoiceOptionIdentifier>" },
{ "date_time", "DateTime?" },
{ "asset", "IEnumerable<AssetIdentifier>" },
{ "modular_content", "IEnumerable<ContentItemIdentifier>" },
{ "taxonomy", "IEnumerable<TaxonomyTermIdentifier>" },
{ "url_slug", "string" }
};

private static Dictionary<string, string> contentTypeToTypeName(bool cmApi)
=> cmApi ? ContentManagementTypes : DeliverTypes;

public Property(string codename, string typeName)
{
Identifier = TextHelpers.GetValidPascalCaseIdentifierName(codename);
TypeName = typeName;
}

public static bool IsContentTypeSupported(string contentType)
public static bool IsContentTypeSupported(string contentType, bool cmApi = false)
{
return contentTypeToTypeName.ContainsKey(contentType);
return contentTypeToTypeName(cmApi).ContainsKey(contentType);
}

public static Property FromContentType(string codename, string contentType)
public static Property FromContentType(string codename, string contentType, bool cmApi = false)
{
if (!IsContentTypeSupported(contentType))
if (!IsContentTypeSupported(contentType, cmApi))
{
throw new ArgumentException($"Unknown Content Type {contentType}", nameof(contentType));
}

return new Property(codename, null)
{
Identifier = TextHelpers.GetValidPascalCaseIdentifierName(codename),
TypeName = contentTypeToTypeName[contentType],
};
return new Property(codename, contentTypeToTypeName(cmApi)[contentType]);
}
}
}
5 changes: 4 additions & 1 deletion src/CloudModelGenerator/appSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
"WithTypeProvider": true,

// Indicates whether the classes should be generated with types that represent structured data model
"StructuredModel": false
"StructuredModel": false,

//Indicates whether the classes should be generated for CM API SDK
"ContentManagementApi": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// This code was generated by a cloud-generators-net tool
// (see https://github.com/Kentico/cloud-generators-net).
//
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
// For further modifications of the class, create a separate file with the partial class.

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using KenticoCloud.ContentManagement.Models.Items;

namespace KenticoCloudModels
{
public partial class CompleteContentType
{
public string Text { get; set; }
public string RichText { get; set; }
public decimal? Number { get; set; }
public IEnumerable<MultipleChoiceOptionIdentifier> MultipleChoice { get; set; }
public DateTime? DateTime { get; set; }
public IEnumerable<AssetIdentifier> Asset { get; set; }
public IEnumerable<ContentItemIdentifier> ModularContent { get; set; }
public IEnumerable<TaxonomyTermIdentifier> Taxonomy { get; set; }
public string UrlSlug { get; set; }
}
}
29 changes: 29 additions & 0 deletions test/CloudModelGenerator.Tests/ClassCodeGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using KenticoCloud.Delivery;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
Expand Down Expand Up @@ -59,6 +60,34 @@ public void Build_CreatesClassWithCompleteContentType()
Assert.Equal(expectedCode, compiledCode);
}

[Fact]
public void Build_CreatesClassWithCompleteContentType_CMAPI()
{
var classDefinition = new ClassDefinition("Complete content type");
classDefinition.AddProperty(Property.FromContentType("text", "text", true));
classDefinition.AddProperty(Property.FromContentType("rich_text", "rich_text", true));
classDefinition.AddProperty(Property.FromContentType("number", "number", true));
classDefinition.AddProperty(Property.FromContentType("multiple_choice", "multiple_choice", true));
classDefinition.AddProperty(Property.FromContentType("date_time", "date_time", true));
classDefinition.AddProperty(Property.FromContentType("asset", "asset", true));
classDefinition.AddProperty(Property.FromContentType("modular_content", "modular_content", true));
classDefinition.AddProperty(Property.FromContentType("taxonomy", "taxonomy", true));
classDefinition.AddProperty(Property.FromContentType("url_slug", "url_slug", true));

var classCodeGenerator = new ClassCodeGenerator(classDefinition, classDefinition.ClassName);

var compiledCode = classCodeGenerator.GenerateCode(true);

var executingPath = AppContext.BaseDirectory;
var expectedCode = File.ReadAllText(executingPath + "/Assets/CompleteContentType_CompiledCode_CMAPI.txt");

// Ignore white space
expectedCode = Regex.Replace(expectedCode, @"\s+", "");
compiledCode = Regex.Replace(compiledCode, @"\s+", "");

Assert.Equal(expectedCode, compiledCode);
}

[Fact]
public void IntegrationTest_GeneratedCodeCompilesWithoutErrors()
{
Expand Down
Loading

0 comments on commit 2787423

Please sign in to comment.