├── lib
├── Directory.Build.props
├── Directory.Build.targets
├── csharp-models-to-json
│ ├── Config.cs
│ ├── ExtraInfo.cs
│ ├── csharp-models-to-json.csproj
│ ├── EnumCollector.cs
│ ├── Program.cs
│ ├── Util.cs
│ └── ModelCollector.cs
└── csharp-models-to-json_test
│ ├── csharp-models-to-json_test.csproj
│ ├── EnumCollector_test.cs
│ └── ModelCollector_test.cs
├── .gitignore
├── test-files
├── test-config.json
├── test-file-exists.js
└── TestFile.cs
├── package.json
├── .github
└── workflows
│ ├── node.tests.yml
│ └── csharp.tests.yml
├── README.md
├── index.js
└── converter.js
/lib/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
5 |
--------------------------------------------------------------------------------
/lib/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | lib/csharp-models-to-json/bin
4 | lib/csharp-models-to-json/obj
5 | lib/csharp-models-to-json_test/obj/
6 | lib/csharp-models-to-json_test/bin/
7 | /test-files/api.d.ts
8 |
9 | **/.vs/*
10 | .vscode/*
11 | .idea/*
12 | *.user
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/Config.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace CSharpModelsToJson
4 | {
5 | public class Config
6 | {
7 | public List Include { get; set; }
8 | public List Exclude { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/ExtraInfo.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace CSharpModelsToJson
3 | {
4 | public class ExtraInfo
5 | {
6 | public bool Obsolete { get; set; }
7 | public string ObsoleteMessage { get; set; }
8 | public string Summary { get; set; }
9 | public string Remarks { get; set; }
10 | public bool EmitDefaultValue { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/csharp-models-to-json.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test-files/test-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "test-files/*.cs"
4 | ],
5 | "exclude": [ ],
6 | "output": "./test-files/api.d.ts",
7 | "camelCase": false,
8 | "camelCaseEnums": false,
9 | "camelCaseOptions": {
10 | "pascalCase": false,
11 | "preserveConsecutiveUppercase": false,
12 | "locale": "en-US"
13 | },
14 | "includeComments": true,
15 | "numericEnums": true,
16 | "validateEmitDefaultValue": true,
17 | "omitSemicolon": true,
18 | "omitFilePathComment": true,
19 | "stringLiteralTypesInsteadOfEnums": false,
20 | "customTypeTranslations": {
21 | "ProductName": "string",
22 | "ProductNumber": "string",
23 | "byte": "number",
24 | "uint": "number"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csharp-models-to-typescript",
3 | "version": "1.2.0",
4 | "title": "C# models to TypeScript",
5 | "author": "Jonathan Svenheden ",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/svenheden/csharp-models-to-typescript.git"
10 | },
11 | "engines": {
12 | "node": ">=6.0.0"
13 | },
14 | "bin": {
15 | "csharp-models-to-typescript": "./index.js"
16 | },
17 | "scripts": {
18 | "test": "rimraf -I ./test-files/api.d.ts && node index.js --config=./test-files/test-config.json && node ./test-files/test-file-exists.js --config=./test-files/test-config.json"
19 | },
20 | "dependencies": {
21 | "camelcase": "^6.0.0"
22 | },
23 | "devDependencies": {
24 | "rimraf": "^5.0.7"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json_test/csharp-models-to-json_test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net8.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {BAEFF23F-0CE8-48B5-899A-56251439C30C}
15 | csharp-models-to-json
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.github/workflows/node.tests.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI
5 |
6 | on: [push, pull_request]
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [16.x, 20.x]
16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | cache: 'npm'
25 | - run: npm ci
26 | - run: npm run test
27 |
--------------------------------------------------------------------------------
/.github/workflows/csharp.tests.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: C# CI
5 |
6 | on: [push, pull_request]
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | defaults:
14 | run:
15 | working-directory: ./lib/csharp-models-to-json_test
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 8.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
30 |
--------------------------------------------------------------------------------
/test-files/test-file-exists.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const configArg = process.argv.find(x => x.startsWith('--config='));
4 |
5 | if (!configArg) {
6 | throw Error('No configuration file for `csharp-models-to-typescript` provided.');
7 | }
8 |
9 | const configPath = configArg.substr('--config='.length);
10 | let config;
11 |
12 | try {
13 | unparsedConfig = fs.readFileSync(configPath, 'utf8');
14 | } catch (error) {
15 | throw Error(`Configuration file "${configPath}" not found.\n${error.message}`);
16 | }
17 |
18 | try {
19 | config = JSON.parse(unparsedConfig);
20 | } catch (error) {
21 | throw Error(`Configuration file "${configPath}" contains invalid JSON.\n${error.message}`);
22 | }
23 |
24 | const output = config.output || 'types.d.ts';
25 |
26 | if (!fs.existsSync(output))
27 | throw Error(`Can't find output file: ${output}`)
28 |
29 | const file = fs.readFileSync(output, 'utf8')
30 | if (file.length === 0)
31 | throw Error(`File '${output}' is empty`)
--------------------------------------------------------------------------------
/test-files/TestFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Runtime.Serialization;
5 |
6 | namespace TestNamespace
7 | {
8 | ///
9 | /// Sample class comment.
10 | ///
11 | public class TestClass
12 | {
13 | ///
14 | /// Sample comment.
15 | ///
16 | public int IntProperty { get; set; }
17 |
18 | [Obsolete("obsolete test prop")]
19 | public string StringProperty { get; set; }
20 |
21 | [DataMember(EmitDefaultValue = false)]
22 | public DateTime DateTimeProperty { get; set; }
23 |
24 | public bool BooleanProperty { get; set; }
25 | }
26 |
27 | public enum TestEnum {
28 | A = 1, // decimal: 1
29 | B = 1_002, // decimal: 1002
30 | C = 0b011, // binary: 3 in decimal
31 | D = 0b_0000_0100, // binary: 4 in decimal
32 | E = 0x005, // hexadecimal: 5 in decimal
33 | F = 0x000_01a, // hexadecimal: 26 in decimal
34 | [Obsolete("obsolete test enum")]
35 | G // 27 in decimal
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json_test/EnumCollector_test.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using NUnit.Framework;
5 |
6 | namespace CSharpModelsToJson.Tests
7 | {
8 | [TestFixture]
9 | public class EnumCollectorTest
10 | {
11 | [Test]
12 | public void ReturnEnumWithMissingValues()
13 | {
14 | var tree = CSharpSyntaxTree.ParseText(@"
15 | public enum SampleEnum
16 | {
17 | A,
18 | B = 7,
19 | C,
20 | D = 4,
21 | E
22 | }"
23 | );
24 |
25 | var root = (CompilationUnitSyntax)tree.GetRoot();
26 |
27 | var enumCollector = new EnumCollector();
28 | enumCollector.VisitEnumDeclaration(root.DescendantNodes().OfType().First());
29 |
30 | var model = enumCollector.Enums.First();
31 |
32 | Assert.That(model, Is.Not.Null);
33 | Assert.That(model.Values, Is.Not.Null);
34 |
35 | Assert.That(model.Values["A"].Value, Is.Null);
36 | Assert.That(model.Values["B"].Value, Is.EqualTo("7"));
37 | Assert.That(model.Values["C"].Value, Is.Null);
38 | Assert.That(model.Values["D"].Value, Is.EqualTo("4"));
39 | Assert.That(model.Values["E"].Value, Is.Null);
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # C# models to TypeScript
2 |
3 | This is a tool that consumes your C# domain models and types and creates TypeScript declaration files from them. There's other tools that does this but what makes this one different is that it internally uses [Roslyn (the .NET compiler platform)](https://github.com/dotnet/roslyn) to parse the source files, which removes the need to create and maintain our own parser.
4 |
5 |
6 | [![NPM version][npm-image]][npm-url]
7 |
8 |
9 | ## Dependencies
10 |
11 | * [.NET Core SDK](https://www.microsoft.com/net/download/macos)
12 |
13 |
14 | ## Install
15 |
16 | ```
17 | $ npm install --save csharp-models-to-typescript
18 | ```
19 |
20 | ## How to use
21 |
22 | 1. Add a config file to your project that contains for example...
23 |
24 | ```
25 | {
26 | "include": [
27 | "./models/**/*.cs",
28 | "./enums/**/*.cs"
29 | ],
30 | "exclude": [
31 | "./models/foo/bar.cs"
32 | ],
33 | "namespace": "Api",
34 | "output": "./api.d.ts",
35 | "includeComments": true,
36 | "camelCase": false,
37 | "camelCaseEnums": false,
38 | "camelCaseOptions": {
39 | "pascalCase": false,
40 | "preserveConsecutiveUppercase": false,
41 | "locale": "en-US"
42 | },
43 | "numericEnums": false,
44 | "validateEmitDefaultValue": false,
45 | "omitFilePathComment": false,
46 | "omitSemicolon": false,
47 | "stringLiteralTypesInsteadOfEnums": false,
48 | "customTypeTranslations": {
49 | "ProductName": "string",
50 | "ProductNumber": "string"
51 | }
52 | }
53 | ```
54 |
55 | 2. Add a npm script to your package.json that references your config file...
56 |
57 | ```
58 | "scripts": {
59 | "generate-types": "csharp-models-to-typescript --config=your-config-file.json"
60 | },
61 | ```
62 |
63 | 3. Run the npm script `generate-types` and the output file specified in your config should be created and populated with your models.
64 |
65 |
66 | ## License
67 |
68 | MIT © [Jonathan Svenheden](https://github.com/svenheden)
69 |
70 | [npm-image]: https://img.shields.io/npm/v/csharp-models-to-typescript.svg
71 | [npm-url]: https://npmjs.org/package/csharp-models-to-typescript
72 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/EnumCollector.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 |
5 | namespace CSharpModelsToJson
6 | {
7 | public class Enum
8 | {
9 | public string Identifier { get; set; }
10 | public ExtraInfo ExtraInfo { get; set; }
11 | public Dictionary Values { get; set; }
12 | }
13 |
14 | public class EnumValue
15 | {
16 | public string Value { get; set; }
17 | public ExtraInfo ExtraInfo { get; set; }
18 | }
19 |
20 |
21 | public class EnumCollector: CSharpSyntaxWalker
22 | {
23 | public readonly List Enums = new List();
24 |
25 | public override void VisitEnumDeclaration(EnumDeclarationSyntax node)
26 | {
27 | var values = new Dictionary();
28 |
29 | foreach (var member in node.Members) {
30 | var equalsValue = member.EqualsValue != null
31 | ? member.EqualsValue.Value.ToString()
32 | : null;
33 |
34 | var value = new EnumValue
35 | {
36 | Value = equalsValue?.Replace("_", ""),
37 | ExtraInfo = new ExtraInfo
38 | {
39 | Obsolete = Util.IsObsolete(member.AttributeLists),
40 | ObsoleteMessage = Util.GetObsoleteMessage(member.AttributeLists),
41 | Summary = Util.GetSummaryMessage(member),
42 | Remarks = Util.GetRemarksMessage(member),
43 | }
44 | };
45 |
46 | values[member.Identifier.ToString()] = value;
47 | }
48 |
49 | this.Enums.Add(new Enum() {
50 | Identifier = node.Identifier.ToString(),
51 | ExtraInfo = new ExtraInfo
52 | {
53 | Obsolete = Util.IsObsolete(node.AttributeLists),
54 | ObsoleteMessage = Util.GetObsoleteMessage(node.AttributeLists),
55 | Summary = Util.GetSummaryMessage(node),
56 | Remarks = Util.GetRemarksMessage(node),
57 | },
58 | Values = values
59 | });
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const process = require('process');
5 | const path = require('path');
6 | const { spawn } = require('child_process');
7 |
8 | const createConverter = require('./converter');
9 |
10 | const configArg = process.argv.find(x => x.startsWith('--config='));
11 |
12 | if (!configArg) {
13 | return console.error('No configuration file for `csharp-models-to-typescript` provided.');
14 | }
15 |
16 | const configPath = configArg.substr('--config='.length);
17 | let config;
18 |
19 | try {
20 | unparsedConfig = fs.readFileSync(configPath, 'utf8');
21 | } catch (error) {
22 | return console.error(`Configuration file "${configPath}" not found.`);
23 | }
24 |
25 | try {
26 | config = JSON.parse(unparsedConfig);
27 | } catch (error) {
28 | return console.error(`Configuration file "${configPath}" contains invalid JSON.`);
29 | }
30 |
31 | const output = config.output || 'types.d.ts';
32 |
33 | const converter = createConverter({
34 | customTypeTranslations: config.customTypeTranslations || {},
35 | namespace: config.namespace,
36 | includeComments: config.includeComments ?? true,
37 | camelCase: config.camelCase || false,
38 | camelCaseOptions: config.camelCaseOptions || {},
39 | camelCaseEnums: config.camelCaseEnums || false,
40 | numericEnums: config.numericEnums || false,
41 | validateEmitDefaultValue: config.validateEmitDefaultValue || false,
42 | omitFilePathComment: config.omitFilePathComment || false,
43 | omitSemicolon: config.omitSemicolon || false,
44 | stringLiteralTypesInsteadOfEnums: config.stringLiteralTypesInsteadOfEnums || false
45 | });
46 |
47 | let timer = process.hrtime();
48 |
49 | const dotnetProject = path.join(__dirname, 'lib/csharp-models-to-json');
50 | const dotnetProcess = spawn('dotnet', ['run', `--project "${dotnetProject}"`, `"${path.resolve(configPath)}"`], { shell: true });
51 |
52 | let stdout = '';
53 |
54 | dotnetProcess.stdout.on('data', data => {
55 | stdout += data;
56 | });
57 |
58 | dotnetProcess.stderr.on('data', err => {
59 | console.error(err.toString());
60 | });
61 |
62 | dotnetProcess.stdout.on('end', () => {
63 | let json;
64 |
65 | //console.log(stdout);
66 |
67 | try {
68 | // Extract the JSON content between the markers
69 | const startMarker = '<<<<<>>>>>';
70 | const endMarker = '<<<<<>>>>>';
71 | const startIndex = stdout.indexOf(startMarker);
72 | const endIndex = stdout.indexOf(endMarker);
73 |
74 | if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
75 | const jsonString = stdout.substring(startIndex + startMarker.length, endIndex).trim();
76 | json = JSON.parse(jsonString);
77 | } else {
78 | throw new Error('JSON markers not found or invalid order of markers.');
79 | }
80 | } catch (error) {
81 | return console.error([
82 | 'The output from `csharp-models-to-json` contains invalid JSON.',
83 | error.message,
84 | stdout
85 | ].join('\n\n'));
86 | }
87 |
88 | const types = converter(json);
89 |
90 | fs.writeFile(output, types, err => {
91 | if (err) {
92 | return console.error(err);
93 | }
94 |
95 | timer = process.hrtime(timer);
96 | console.log('Done in %d.%d seconds.', timer[0], timer[1]);
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.CSharp;
7 | using Microsoft.CodeAnalysis.CSharp.Syntax;
8 | using Ganss.IO;
9 |
10 | namespace CSharpModelsToJson
11 | {
12 | class File
13 | {
14 | public string FileName { get; set; }
15 | public IEnumerable Models { get; set; }
16 | public IEnumerable Enums { get; set; }
17 | }
18 |
19 | class Program
20 | {
21 | static void Main(string[] args)
22 | {
23 | Config? config = null;
24 | if (System.IO.File.Exists(args[0])) {
25 | var configJson = System.IO.File.ReadAllText(args[0]);
26 | var opts = new JsonSerializerOptions {
27 | PropertyNameCaseInsensitive = true
28 | };
29 | config = JsonSerializer.Deserialize(configJson, opts);
30 | }
31 |
32 | var includes = config?.Include ?? [];
33 | var excludes = config?.Exclude ?? [];
34 |
35 | List files = new List();
36 |
37 | foreach (string fileName in getFileNames(includes, excludes)) {
38 | files.Add(parseFile(fileName));
39 | }
40 |
41 | JsonSerializerOptions options = new()
42 | {
43 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
44 | };
45 |
46 | string json = JsonSerializer.Serialize(files, options);
47 |
48 | var sb = new StringBuilder();
49 | sb.AppendLine("<<<<<>>>>>");
50 | sb.AppendLine(json);
51 | sb.AppendLine("<<<<<>>>>>");
52 |
53 | System.Console.OutputEncoding = System.Text.Encoding.UTF8;
54 | System.Console.WriteLine(sb.ToString());
55 | }
56 |
57 | static List getFileNames(List includes, List excludes) {
58 | List fileNames = new List();
59 |
60 | foreach (var path in expandGlobPatterns(includes)) {
61 | fileNames.Add(path);
62 | }
63 |
64 | foreach (var path in expandGlobPatterns(excludes)) {
65 | fileNames.Remove(path);
66 | }
67 |
68 | return fileNames;
69 | }
70 |
71 | static List expandGlobPatterns(List globPatterns) {
72 | List fileNames = new List();
73 |
74 | foreach (string pattern in globPatterns) {
75 | var paths = Glob.Expand(pattern);
76 |
77 | foreach (var path in paths) {
78 | fileNames.Add(path.FullName);
79 | }
80 | }
81 |
82 | return fileNames;
83 | }
84 |
85 | static File parseFile(string path) {
86 | string source = System.IO.File.ReadAllText(path);
87 | SyntaxTree tree = CSharpSyntaxTree.ParseText(source);
88 | var root = (CompilationUnitSyntax) tree.GetRoot();
89 |
90 | var modelCollector = new ModelCollector();
91 | var enumCollector = new EnumCollector();
92 |
93 | modelCollector.Visit(root);
94 | enumCollector.Visit(root);
95 |
96 | return new File() {
97 | FileName = System.IO.Path.GetFullPath(path),
98 | Models = modelCollector.Models,
99 | Enums = enumCollector.Enums
100 | };
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/Util.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using System;
5 | using System.Linq;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace CSharpModelsToJson
9 | {
10 | internal static class Util
11 | {
12 | internal static bool IsObsolete(SyntaxList attributeLists) =>
13 | attributeLists.Any(attributeList =>
14 | attributeList.Attributes.Any(attribute =>
15 | attribute.Name.ToString().Equals("Obsolete") || attribute.Name.ToString().Equals("ObsoleteAttribute")));
16 |
17 | internal static string GetObsoleteMessage(SyntaxList attributeLists)
18 | {
19 | foreach (var attributeList in attributeLists)
20 | {
21 | var obsoleteAttribute =
22 | attributeList.Attributes.FirstOrDefault(attribute =>
23 | attribute.Name.ToString().Equals("Obsolete") || attribute.Name.ToString().Equals("ObsoleteAttribute"));
24 |
25 | if (obsoleteAttribute != null)
26 | {
27 | return obsoleteAttribute.ArgumentList == null
28 | ? null
29 | : obsoleteAttribute.ArgumentList.Arguments.ToString()?.TrimStart('@').Trim('"');
30 | }
31 | }
32 |
33 | return null;
34 | }
35 |
36 | internal static string GetSummaryMessage(SyntaxNode classItem)
37 | {
38 | return GetCommentTag(classItem, "summary");
39 | }
40 |
41 | internal static string GetRemarksMessage(SyntaxNode classItem)
42 | {
43 | return GetCommentTag(classItem, "remarks");
44 | }
45 |
46 | private static string GetCommentTag(SyntaxNode classItem, string xmlTag)
47 | {
48 | var documentComment = classItem.GetDocumentationCommentTriviaSyntax();
49 |
50 | if (documentComment == null)
51 | return null;
52 |
53 | var summaryElement = documentComment.Content
54 | .OfType()
55 | .FirstOrDefault(_ => _.StartTag.Name.LocalName.Text == xmlTag);
56 |
57 | if (summaryElement == null)
58 | return null;
59 |
60 | var summaryText = summaryElement.DescendantTokens()
61 | .Where(_ => _.Kind() == SyntaxKind.XmlTextLiteralToken)
62 | .Select(_ => _.Text.Trim())
63 | .ToList();
64 |
65 | var summaryContent = summaryElement.Content.ToString();
66 | summaryContent = Regex.Replace(summaryContent, @"^\s*///\s*", string.Empty, RegexOptions.Multiline);
67 | summaryContent = Regex.Replace(summaryContent, "^", Environment.NewLine, RegexOptions.Multiline);
68 | summaryContent = Regex.Replace(summaryContent, "", string.Empty);
69 |
70 | return summaryContent.Trim();
71 | }
72 |
73 | public static DocumentationCommentTriviaSyntax GetDocumentationCommentTriviaSyntax(this SyntaxNode node)
74 | {
75 | if (node == null)
76 | {
77 | return null;
78 | }
79 |
80 | foreach (var leadingTrivia in node.GetLeadingTrivia())
81 | {
82 | var structure = leadingTrivia.GetStructure() as DocumentationCommentTriviaSyntax;
83 |
84 | if (structure != null)
85 | {
86 | return structure;
87 | }
88 | }
89 |
90 | return null;
91 | }
92 |
93 | internal static bool GetEmitDefaultValue(SyntaxList attributeLists)
94 | {
95 | var dataMemberAttribute = attributeLists
96 | .SelectMany(attributeList => attributeList.Attributes)
97 | .FirstOrDefault(attribute => attribute.Name.ToString().Equals("DataMember") || attribute.Name.ToString().Equals("DataMemberAttribute"));
98 |
99 | if (dataMemberAttribute?.ArgumentList == null)
100 | return true;
101 |
102 | var emitDefaultValueArgument = dataMemberAttribute.ArgumentList.Arguments.FirstOrDefault(x => x.ToString().StartsWith("EmitDefaultValue"));
103 |
104 | if (emitDefaultValueArgument == null)
105 | return true;
106 |
107 | return !emitDefaultValueArgument.ToString().EndsWith("false");
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json/ModelCollector.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Microsoft.CodeAnalysis;
4 | using Microsoft.CodeAnalysis.CSharp;
5 | using Microsoft.CodeAnalysis.CSharp.Syntax;
6 |
7 | namespace CSharpModelsToJson
8 | {
9 | public class Model
10 | {
11 | public string ModelName { get; set; }
12 | public IEnumerable Fields { get; set; }
13 | public IEnumerable Properties { get; set; }
14 | public IEnumerable BaseClasses { get; set; }
15 | public ExtraInfo ExtraInfo { get; set; }
16 | }
17 |
18 | public class Field
19 | {
20 | public string Identifier { get; set; }
21 | public string Type { get; set; }
22 | }
23 |
24 | public class Property
25 | {
26 | public string Identifier { get; set; }
27 | public string Type { get; set; }
28 | public ExtraInfo ExtraInfo { get; set; }
29 | }
30 |
31 | public class ModelCollector : CSharpSyntaxWalker
32 | {
33 | public readonly List Models = new List();
34 |
35 | public override void VisitClassDeclaration(ClassDeclarationSyntax node)
36 | {
37 | var model = CreateModel(node);
38 |
39 | Models.Add(model);
40 | }
41 |
42 | public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
43 | {
44 | var model = CreateModel(node);
45 |
46 | Models.Add(model);
47 | }
48 |
49 | public override void VisitRecordDeclaration(RecordDeclarationSyntax node)
50 | {
51 | var model = new Model()
52 | {
53 | ModelName = $"{node.Identifier.ToString()}{node.TypeParameterList?.ToString()}",
54 | Fields = node.ParameterList?.Parameters
55 | .Where(field => IsAccessible(field.Modifiers))
56 | .Where(property => !IsIgnored(property.AttributeLists))
57 | .Select((field) => new Field
58 | {
59 | Identifier = field.Identifier.ToString(),
60 | Type = field.Type.ToString(),
61 | }),
62 | Properties = node.Members.OfType()
63 | .Where(property => IsAccessible(property.Modifiers))
64 | .Where(property => !IsIgnored(property.AttributeLists))
65 | .Select(ConvertProperty),
66 | BaseClasses = new List(),
67 | };
68 |
69 | Models.Add(model);
70 | }
71 |
72 | private static Model CreateModel(TypeDeclarationSyntax node)
73 | {
74 | return new Model()
75 | {
76 | ModelName = $"{node.Identifier.ToString()}{node.TypeParameterList?.ToString()}",
77 | Fields = node.Members.OfType()
78 | .Where(field => IsAccessible(field.Modifiers))
79 | .Where(property => !IsIgnored(property.AttributeLists))
80 | .Select(ConvertField),
81 | Properties = node.Members.OfType()
82 | .Where(property => IsAccessible(property.Modifiers))
83 | .Where(property => !IsIgnored(property.AttributeLists))
84 | .Select(ConvertProperty),
85 | BaseClasses = node.BaseList?.Types.Select(s => s.ToString()),
86 | ExtraInfo = new ExtraInfo
87 | {
88 | Obsolete = Util.IsObsolete(node.AttributeLists),
89 | ObsoleteMessage = Util.GetObsoleteMessage(node.AttributeLists),
90 | Summary = Util.GetSummaryMessage(node),
91 | Remarks = Util.GetRemarksMessage(node),
92 | }
93 | };
94 | }
95 |
96 | private static bool IsIgnored(SyntaxList propertyAttributeLists) =>
97 | propertyAttributeLists.Any(attributeList =>
98 | attributeList.Attributes.Any(attribute =>
99 | attribute.Name.ToString().Equals("JsonIgnore") ||
100 | attribute.Name.ToString().Equals("IgnoreDataMember")));
101 |
102 | private static bool IsAccessible(SyntaxTokenList modifiers) => modifiers.All(modifier =>
103 | modifier.ToString() != "const" &&
104 | modifier.ToString() != "static" &&
105 | modifier.ToString() != "private"
106 | );
107 |
108 | private static Field ConvertField(FieldDeclarationSyntax field) => new Field
109 | {
110 | Identifier = field.Declaration.Variables.First().GetText().ToString(),
111 | Type = field.Declaration.Type.ToString(),
112 | };
113 |
114 | private static Property ConvertProperty(PropertyDeclarationSyntax property) => new Property
115 | {
116 | Identifier = property.Identifier.ToString(),
117 | Type = property.Type.ToString(),
118 | ExtraInfo = new ExtraInfo
119 | {
120 | Obsolete = Util.IsObsolete(property.AttributeLists),
121 | ObsoleteMessage = Util.GetObsoleteMessage(property.AttributeLists),
122 | Summary = Util.GetSummaryMessage(property),
123 | Remarks = Util.GetRemarksMessage(property),
124 | EmitDefaultValue = Util.GetEmitDefaultValue(property.AttributeLists),
125 | }
126 | };
127 | }
128 | }
--------------------------------------------------------------------------------
/converter.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const camelcase = require('camelcase');
3 |
4 | const flatten = arr => arr.reduce((a, b) => a.concat(b), []);
5 |
6 | const arrayRegex = /^(.+)\[\]$/;
7 | const simpleCollectionRegex = /^(?:I?List|IReadOnlyList|IEnumerable|ICollection|IReadOnlyCollection|HashSet)<([\w\d]+)>\??$/;
8 | const collectionRegex = /^(?:I?List|IReadOnlyList|IEnumerable|ICollection|IReadOnlyCollection|HashSet)<(.+)>\??$/;
9 | const simpleDictionaryRegex = /^(?:I?Dictionary|SortedDictionary|IReadOnlyDictionary)<([\w\d]+)\s*,\s*([\w\d]+)>\??$/;
10 | const dictionaryRegex = /^(?:I?Dictionary|SortedDictionary|IReadOnlyDictionary)<([\w\d]+)\s*,\s*(.+)>\??$/;
11 |
12 | const defaultTypeTranslations = {
13 | int: 'number',
14 | double: 'number',
15 | float: 'number',
16 | Int32: 'number',
17 | Int64: 'number',
18 | short: 'number',
19 | long: 'number',
20 | decimal: 'number',
21 | bool: 'boolean',
22 | DateTime: 'string',
23 | DateTimeOffset: 'string',
24 | Guid: 'string',
25 | dynamic: 'any',
26 | object: 'any',
27 | };
28 |
29 | const createConverter = config => {
30 | const typeTranslations = Object.assign({}, defaultTypeTranslations, config.customTypeTranslations);
31 |
32 | const convert = json => {
33 | const content = json.map(file => {
34 | const filename = path.relative(process.cwd(), file.FileName);
35 |
36 | const rows = flatten([
37 | ...file.Models.map(model => convertModel(model, filename)),
38 | ...file.Enums.map(enum_ => convertEnum(enum_, filename)),
39 | ]);
40 |
41 | return rows
42 | .map(row => config.namespace ? ` ${row}` : row)
43 | .join('\n');
44 | });
45 |
46 | const filteredContent = content.filter(x => x.length > 0);
47 |
48 | if (config.namespace) {
49 | return [
50 | `declare module ${config.namespace} {`,
51 | ...filteredContent,
52 | '}',
53 | ].join('\n');
54 | } else {
55 | return filteredContent.join('\n');
56 | }
57 | };
58 |
59 | const convertModel = (model, filename) => {
60 | const rows = [];
61 |
62 | if (model.BaseClasses) {
63 | model.IndexSignature = model.BaseClasses.find(type => type.match(dictionaryRegex));
64 | model.BaseClasses = model.BaseClasses.filter(type => !type.match(dictionaryRegex));
65 | }
66 |
67 | const members = [...(model.Fields || []), ...(model.Properties || [])];
68 | const baseClasses = model.BaseClasses && model.BaseClasses.length ? ` extends ${model.BaseClasses.join(', ')}` : '';
69 |
70 | if (!config.omitFilePathComment) {
71 | rows.push(`// ${filename}`);
72 | }
73 | let classCommentRows = formatComment(model.ExtraInfo, '')
74 | if (classCommentRows) {
75 | rows.push(classCommentRows);
76 | }
77 |
78 | rows.push(`export interface ${model.ModelName}${baseClasses} {`);
79 |
80 | const propertySemicolon = config.omitSemicolon ? '' : ';';
81 |
82 | if (model.IndexSignature) {
83 | rows.push(` ${convertIndexType(model.IndexSignature)}${propertySemicolon}`);
84 | }
85 |
86 | members.forEach(member => {
87 | let memberCommentRows = formatComment(member.ExtraInfo, ' ')
88 | if (memberCommentRows) {
89 | rows.push(memberCommentRows);
90 | }
91 |
92 | rows.push(` ${convertProperty(member)}${propertySemicolon}`);
93 | });
94 |
95 | rows.push(`}\n`);
96 |
97 | return rows;
98 | };
99 |
100 | const convertEnum = (enum_, filename) => {
101 | const rows = [];
102 | if (!config.omitFilePathComment) {
103 | rows.push(`// ${filename}`);
104 | }
105 |
106 | const entries = Object.entries(enum_.Values);
107 |
108 | let classCommentRows = formatComment(enum_.ExtraInfo, '')
109 | if (classCommentRows) {
110 | rows.push(classCommentRows);
111 | }
112 |
113 | const getEnumStringValue = (value) => config.camelCaseEnums
114 | ? camelcase(value)
115 | : value;
116 |
117 | const lastValueSemicolon = config.omitSemicolon ? '' : ';';
118 |
119 | if (config.stringLiteralTypesInsteadOfEnums) {
120 | rows.push(`export type ${enum_.Identifier} =`);
121 |
122 | entries.forEach(([key], i) => {
123 | const delimiter = (i === entries.length - 1) ? lastValueSemicolon : ' |';
124 | rows.push(` '${getEnumStringValue(key)}'${delimiter}`);
125 | });
126 |
127 | rows.push('');
128 | } else {
129 | rows.push(`export enum ${enum_.Identifier} {`);
130 |
131 | entries.forEach(([key, entry]) => {
132 | let classCommentRows = formatComment(entry.ExtraInfo, ' ')
133 | if (classCommentRows) {
134 | rows.push(classCommentRows);
135 | }
136 | if (config.numericEnums) {
137 | if (entry.Value == null) {
138 | rows.push(` ${key},`);
139 | } else {
140 | rows.push(` ${key} = ${entry.Value},`);
141 | }
142 | } else {
143 | rows.push(` ${key} = '${getEnumStringValue(key)}',`);
144 | }
145 | });
146 |
147 | rows.push(`}\n`);
148 | }
149 |
150 | return rows;
151 | };
152 |
153 | const formatComment = (extraInfo, indentation) => {
154 | if (!config.includeComments || !extraInfo || (!extraInfo.Obsolete && !extraInfo.Summary)) {
155 | return undefined;
156 | }
157 |
158 | let comment = '';
159 | comment += `${indentation}/**\n`;
160 |
161 | if (extraInfo.Summary) {
162 | let commentLines = extraInfo.Summary.split(/\r?\n/);
163 | commentLines = commentLines.map((e) => {
164 | return `${indentation} * ${replaceCommentTags(e)}\n`;
165 | })
166 | comment += commentLines.join('');
167 | }
168 | if (extraInfo.Remarks) {
169 | comment += `${indentation} *\n`;
170 | comment += `${indentation} * @remarks\n`;
171 | let commentLines = extraInfo.Remarks.split(/\r?\n/);
172 | commentLines = commentLines.map((e) => {
173 | return `${indentation} * ${replaceCommentTags(e)}\n`;
174 | })
175 | comment += commentLines.join('');
176 | }
177 |
178 | if (extraInfo.Obsolete) {
179 | if (extraInfo.Summary) {
180 | comment += `${indentation} *\n`;
181 | }
182 |
183 | let obsoleteMessage = '';
184 | if (extraInfo.ObsoleteMessage) {
185 | obsoleteMessage = ' ' + replaceCommentTags(extraInfo.ObsoleteMessage);
186 | }
187 | comment += `${indentation} * @deprecated${obsoleteMessage}\n`;
188 | }
189 |
190 | comment += `${indentation} */`;
191 |
192 | return comment;
193 | }
194 |
195 | const replaceCommentTags = comment => {
196 | return comment
197 | .replace(//gi, '{@link $1}')
198 | .replace(/(.+)<\/see>/gi, '{@link $1 | $2}')
199 | .replace('', '@inheritDoc');
200 | }
201 |
202 | const convertProperty = property => {
203 | const optional = property.Type.endsWith('?') || (config.validateEmitDefaultValue &&
204 | property.ExtraInfo != null &&
205 | !property.ExtraInfo.EmitDefaultValue);
206 | const identifier = convertIdentifier(optional ? `${property.Identifier.split(' ')[0]}?` : property.Identifier.split(' ')[0]);
207 |
208 | const type = parseType(property.Type);
209 |
210 | return `${identifier}: ${type}`;
211 | };
212 |
213 | const convertIndexType = indexType => {
214 | const dictionary = indexType.match(dictionaryRegex);
215 | const simpleDictionary = indexType.match(simpleDictionaryRegex);
216 |
217 | propType = simpleDictionary ? dictionary[2] : parseType(dictionary[2]);
218 |
219 | return `[key: ${convertType(dictionary[1])}]: ${convertType(propType)}`;
220 | };
221 |
222 | const convertRecord = indexType => {
223 | const dictionary = indexType.match(dictionaryRegex);
224 | const simpleDictionary = indexType.match(simpleDictionaryRegex);
225 |
226 | propType = simpleDictionary ? dictionary[2] : parseType(dictionary[2]);
227 |
228 | return `Record<${convertType(dictionary[1])}, ${convertType(propType)}>`;
229 | };
230 |
231 | const parseType = propType => {
232 | const array = propType.match(arrayRegex);
233 | if (array) {
234 | propType = array[1];
235 | }
236 |
237 | const collection = propType.match(collectionRegex);
238 | const dictionary = propType.match(dictionaryRegex);
239 |
240 | let type;
241 |
242 | if (collection) {
243 | const simpleCollection = propType.match(simpleCollectionRegex);
244 | propType = simpleCollection ? collection[1] : parseType(collection[1]);
245 | type = `${convertType(propType)}[]`;
246 | } else if (dictionary) {
247 | type = `${convertRecord(propType)}`;
248 | } else {
249 | const optional = propType.endsWith('?');
250 | type = convertType(optional ? propType.slice(0, propType.length - 1) : propType);
251 | }
252 |
253 | return array ? `${type}[]` : type;
254 | };
255 |
256 | const convertIdentifier = identifier => config.camelCase ? camelcase(identifier, config.camelCaseOptions) : identifier;
257 | const convertType = type => type in typeTranslations ? typeTranslations[type] : type;
258 |
259 | return convert;
260 | };
261 |
262 | module.exports = createConverter;
263 |
--------------------------------------------------------------------------------
/lib/csharp-models-to-json_test/ModelCollector_test.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using NUnit.Framework;
5 |
6 | namespace CSharpModelsToJson.Tests
7 | {
8 | [TestFixture]
9 | public class ModelCollectorTest
10 | {
11 | [Test]
12 | public void BasicInheritance_ReturnsInheritedClass()
13 | {
14 | var tree = CSharpSyntaxTree.ParseText(@"
15 | public class A : B, C, D
16 | {
17 | public void AMember()
18 | {
19 | }
20 | }"
21 | );
22 |
23 | var root = (CompilationUnitSyntax)tree.GetRoot();
24 |
25 | var modelCollector = new ModelCollector();
26 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First());
27 |
28 | Assert.That(modelCollector.Models, Is.Not.Null);
29 | Assert.That(modelCollector.Models.First().BaseClasses, Is.EqualTo(new[] { "B", "C", "D" }));
30 | }
31 |
32 | [Test]
33 | public void InterfaceImport_ReturnsSyntaxClassFromInterface()
34 | {
35 | var tree = CSharpSyntaxTree.ParseText(@"
36 | public interface IPhoneNumber {
37 | string Label { get; set; }
38 | string Number { get; set; }
39 | int MyProperty { get; set; }
40 | }
41 |
42 | public interface IPoint
43 | {
44 | // Property signatures:
45 | int x
46 | {
47 | get;
48 | set;
49 | }
50 |
51 | int y
52 | {
53 | get;
54 | set;
55 | }
56 | }
57 |
58 |
59 | public class X {
60 | public IPhoneNumber test { get; set; }
61 | public IPoint test2 { get; set; }
62 | }"
63 | );
64 |
65 | var root = (CompilationUnitSyntax)tree.GetRoot();
66 |
67 | var modelCollector = new ModelCollector();
68 | modelCollector.Visit(root);
69 |
70 | Assert.That(modelCollector.Models, Is.Not.Null);
71 | Assert.That(modelCollector.Models.Count, Is.EqualTo(3));
72 | Assert.That(modelCollector.Models.First().Properties.Count(), Is.EqualTo(3));
73 | }
74 |
75 |
76 | [Test]
77 | public void TypedInheritance_ReturnsInheritance()
78 | {
79 | var tree = CSharpSyntaxTree.ParseText(@"
80 | public class A : IController
81 | {
82 | public void AMember()
83 | {
84 | }
85 | }"
86 | );
87 |
88 | var root = (CompilationUnitSyntax)tree.GetRoot();
89 |
90 | var modelCollector = new ModelCollector();
91 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First());
92 |
93 | Assert.That(modelCollector.Models, Is.Not.Null);
94 | Assert.That(modelCollector.Models.First().BaseClasses, Is.EqualTo(new[] { "IController" }));
95 | }
96 |
97 | [Test]
98 | public void AccessibilityRespected_ReturnsPublicOnly()
99 | {
100 | var tree = CSharpSyntaxTree.ParseText(@"
101 | public class A : IController
102 | {
103 | const int A_Constant = 0;
104 |
105 | private string B { get; set }
106 |
107 | static string C { get; set }
108 |
109 | public string Included { get; set }
110 |
111 | public void AMember()
112 | {
113 | }
114 | }"
115 | );
116 |
117 | var root = (CompilationUnitSyntax)tree.GetRoot();
118 |
119 | var modelCollector = new ModelCollector();
120 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First());
121 |
122 | Assert.That(modelCollector.Models, Is.Not.Null);
123 | Assert.That(modelCollector.Models.First().Properties, Is.Not.Null);
124 | Assert.That(modelCollector.Models.First().Properties.Count(), Is.EqualTo(1));
125 | }
126 |
127 | [Test]
128 | public void IgnoresJsonIgnored_ReturnsOnlyNotIgnored()
129 | {
130 | var tree = CSharpSyntaxTree.ParseText(@"
131 | public class A : IController
132 | {
133 | const int A_Constant = 0;
134 |
135 | private string B { get; set }
136 |
137 | static string C { get; set }
138 |
139 | public string Included { get; set }
140 |
141 | [JsonIgnore]
142 | public string Ignored { get; set; }
143 |
144 | [IgnoreDataMember]
145 | public string Ignored2 { get; set; }
146 |
147 | public void AMember()
148 | {
149 | }
150 | }"
151 | );
152 |
153 | var root = (CompilationUnitSyntax)tree.GetRoot();
154 |
155 | var modelCollector = new ModelCollector();
156 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First());
157 |
158 | Assert.That(modelCollector.Models, Is.Not.Null);
159 | Assert.That(modelCollector.Models.First().Properties, Is.Not.Null);
160 | Assert.That(modelCollector.Models.First().Properties.Count(), Is.EqualTo(1));
161 |
162 | }
163 |
164 | [Test]
165 | public void DictionaryInheritance_ReturnsIndexAccessor()
166 | {
167 | var tree = CSharpSyntaxTree.ParseText(@"public class A : Dictionary { }");
168 |
169 | var root = (CompilationUnitSyntax)tree.GetRoot();
170 |
171 | var modelCollector = new ModelCollector();
172 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First());
173 |
174 | Assert.That(modelCollector.Models, Is.Not.Null);
175 | Assert.That(modelCollector.Models.First().BaseClasses, Is.Not.Null);
176 | Assert.That(modelCollector.Models.First().BaseClasses, Is.EqualTo(new[] { "Dictionary" }));
177 | }
178 |
179 | [Test]
180 | public void ReturnObsoleteClassInfo()
181 | {
182 | var tree = CSharpSyntaxTree.ParseText(@"
183 | [Obsolete(@""test"")]
184 | public class A
185 | {
186 | [Obsolete(@""test prop"")]
187 | public string A { get; set }
188 |
189 | public string B { get; set }
190 | }"
191 | );
192 |
193 | var root = (CompilationUnitSyntax)tree.GetRoot();
194 |
195 | var modelCollector = new ModelCollector();
196 | modelCollector.VisitClassDeclaration(root.DescendantNodes().OfType().First());
197 |
198 | var model = modelCollector.Models.First();
199 |
200 | Assert.That(model, Is.Not.Null);
201 | Assert.That(model.Properties, Is.Not.Null);
202 |
203 | Assert.That(model.ExtraInfo.Obsolete, Is.True);
204 | Assert.That(model.ExtraInfo.ObsoleteMessage, Is.EqualTo("test"));
205 |
206 | Assert.That(model.Properties.First(x => x.Identifier.Equals("A")).ExtraInfo.Obsolete, Is.True);
207 | Assert.That(model.Properties.First(x => x.Identifier.Equals("A")).ExtraInfo.ObsoleteMessage, Is.EqualTo("test prop"));
208 |
209 | Assert.That(model.Properties.First(x => x.Identifier.Equals("B")).ExtraInfo.Obsolete, Is.False);
210 | Assert.That(model.Properties.First(x => x.Identifier.Equals("B")).ExtraInfo.ObsoleteMessage, Is.Null);
211 | }
212 |
213 | [Test]
214 | public void ReturnObsoleteEnumInfo()
215 | {
216 | var tree = CSharpSyntaxTree.ParseText(@"
217 | [Obsolete(@""test"")]
218 | public enum A
219 | {
220 | A = 0,
221 | B = 1,
222 | }"
223 | );
224 |
225 | var root = (CompilationUnitSyntax)tree.GetRoot();
226 |
227 | var enumCollector = new EnumCollector();
228 | enumCollector.VisitEnumDeclaration(root.DescendantNodes().OfType().First());
229 |
230 | var model = enumCollector.Enums.First();
231 |
232 | Assert.That(model, Is.Not.Null) ;
233 | Assert.That(model.Values, Is.Not.Null);
234 |
235 | Assert.That(model.ExtraInfo.Obsolete, Is.True);
236 | Assert.That(model.ExtraInfo.ObsoleteMessage, Is.EqualTo("test"));
237 | }
238 |
239 | [Test]
240 | public void EnumBinaryValue()
241 | {
242 | var tree = CSharpSyntaxTree.ParseText(@"
243 | public enum A {
244 | A = 1, // decimal: 1
245 | B = 1_002, // decimal: 1002
246 | C = 0b011, // binary: 3 in decimal
247 | D = 0b_0000_0100, // binary: 4 in decimal
248 | E = 0x005, // hexadecimal: 5 in decimal
249 | F = 0x000_01a, // hexadecimal: 26 in decimal
250 | }"
251 | );
252 |
253 | var root = (CompilationUnitSyntax)tree.GetRoot();
254 |
255 | var enumCollector = new EnumCollector();
256 | enumCollector.VisitEnumDeclaration(root.DescendantNodes().OfType().First());
257 |
258 | var model = enumCollector.Enums.First();
259 |
260 | Assert.That(model, Is.Not.Null);
261 | Assert.That(model.Values, Is.Not.Null);
262 |
263 | Assert.That(model.Values["A"].Value, Is.EqualTo("1"));
264 | Assert.That(model.Values["B"].Value, Is.EqualTo("1002"));
265 | Assert.That(model.Values["C"].Value, Is.EqualTo("0b011"));
266 | Assert.That(model.Values["D"].Value, Is.EqualTo("0b00000100"));
267 | Assert.That(model.Values["E"].Value, Is.EqualTo("0x005"));
268 | Assert.That(model.Values["F"].Value, Is.EqualTo("0x00001a"));
269 | }
270 |
271 | [Test]
272 | public void ReturnEmmitDefaultValueInfo()
273 | {
274 | var tree = CSharpSyntaxTree.ParseText(@"
275 | public class A
276 | {
277 | [DataMember(EmitDefaultValue = false)]
278 | public bool Prop1 { get; set; }
279 |
280 | [DataMember(EmitDefaultValue = true)]
281 | public bool Prop2 { get; set; }
282 |
283 | [DataMember( EmitDefaultValue = false )]
284 | public bool Prop3 { get; set; }
285 |
286 | [DataMember]
287 | public bool Prop4 { get; set; }
288 |
289 | public bool Prop5 { get; set; }
290 | }"
291 | );
292 |
293 | var root = (CompilationUnitSyntax)tree.GetRoot();
294 |
295 | var modelCollector = new ModelCollector();
296 | modelCollector.Visit(root);
297 |
298 | Assert.That(modelCollector.Models, Is.Not.Null);
299 | Assert.That(modelCollector.Models.Count, Is.EqualTo(1));
300 |
301 | var properties = modelCollector.Models.First().Properties;
302 |
303 | Assert.That(properties.First(x => x.Identifier == "Prop1").ExtraInfo.EmitDefaultValue, Is.False);
304 | Assert.That(properties.First(x => x.Identifier == "Prop2").ExtraInfo.EmitDefaultValue, Is.True);
305 | Assert.That(properties.First(x => x.Identifier == "Prop3").ExtraInfo.EmitDefaultValue, Is.False);
306 | Assert.That(properties.First(x => x.Identifier == "Prop4").ExtraInfo.EmitDefaultValue, Is.True);
307 | Assert.That(properties.First(x => x.Identifier == "Prop5").ExtraInfo.EmitDefaultValue, Is.True);
308 | }
309 |
310 | }
311 | }
--------------------------------------------------------------------------------