diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2b3d9a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,82 @@ +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx}] +indent_size = 4 + +# Xml project files +[*.{csproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# proto +[*.proto] +indent_size = 2 + +# Dotnet code style settings: +[*.{cs}] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# CSharp code style settings: +[*.cs] +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml new file mode 100644 index 0000000..ce91b3e --- /dev/null +++ b/.github/workflows/Build.yml @@ -0,0 +1,85 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [CustomGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_Build --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: Build + +on: + push: + branches: + - main + - dev + - alpha + - beta + paths: + - '!docs/**/*' + - '!package.json' + - '!README.md' + pull_request: + branches: + - main + - dev + - alpha + - beta + +jobs: + windows-latest: + name: windows-latest + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.* + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.* + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('global.json', 'SchemaGenerator/**/*.csproj') }} + - name: 'Run: Compile' + run: ./build.cmd Compile + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.* + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.* + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('global.json', 'SchemaGenerator/**/*.csproj') }} + - name: 'Run: Compile' + run: ./build.cmd Compile + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/Cancel.yml b/.github/workflows/Cancel.yml new file mode 100644 index 0000000..ea9b36b --- /dev/null +++ b/.github/workflows/Cancel.yml @@ -0,0 +1,12 @@ +name: Cancel +on: [push] +jobs: + cancel: + name: 'Cancel Previous Runs' + runs-on: ubuntu-latest + timeout-minutes: 3 + steps: + - uses: styfle/cancel-workflow-action@0.7.0 + with: + workflow_id: 5724590,5725340 + access_token: ${{ github.token }} \ No newline at end of file diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml new file mode 100644 index 0000000..1da9142 --- /dev/null +++ b/.github/workflows/Release.yml @@ -0,0 +1,49 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [CustomGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_Release --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: Release + +on: + push: + tags: + - '*' + +jobs: + windows-latest: + name: windows-latest + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.* + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.* + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('global.json', 'SchemaGenerator/**/*.csproj') }} + - name: 'Run: Release' + run: ./build.cmd Release + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml new file mode 100644 index 0000000..a6e38c5 --- /dev/null +++ b/.github/workflows/Tests.yml @@ -0,0 +1,77 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [CustomGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_Tests --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: Tests + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + windows-latest: + name: windows-latest + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.* + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.* + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('global.json', 'SchemaGenerator/**/*.csproj') }} + - name: 'Run: Test' + run: ./build.cmd Test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.* + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.* + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('global.json', 'SchemaGenerator/**/*.csproj') }} + - name: 'Run: Test' + run: ./build.cmd Test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ce6fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,340 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..d810b91 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,129 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/build", + "title": "Build Schema", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "NuGetApiKey": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "NugetApiUrl": { + "type": "string" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "AuthenticatedGitHubClient", + "Clean", + "Compile", + "GitHubRelease", + "Pack", + "Release", + "Restore", + "RunChangelog", + "Test" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "AuthenticatedGitHubClient", + "Clean", + "Compile", + "GitHubRelease", + "Pack", + "Release", + "Restore", + "RunChangelog", + "Test" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..67f5352 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "SchemaGenerator.sln" +} \ No newline at end of file diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..e98a683 --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// + using System.Reflection; + [assembly: AssemblyMetadataAttribute("githash","31fd699076fb0c88f0bee74ed639fc85fdf2c633")] + namespace System { + internal static class AssemblyVersionInformation { + internal const System.String AssemblyMetadata_githash = "31fd699076fb0c88f0bee74ed639fc85fdf2c633"; + } + } + \ No newline at end of file diff --git a/AvroSchemaGenerator.Tests/AvroSchemaGenerator.Tests.csproj b/AvroSchemaGenerator.Tests/AvroSchemaGenerator.Tests.csproj new file mode 100644 index 0000000..3723203 --- /dev/null +++ b/AvroSchemaGenerator.Tests/AvroSchemaGenerator.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + $(DefineConstants);NETCORE + + diff --git a/AvroSchemaGenerator.Tests/FullSchemaTest.cs b/AvroSchemaGenerator.Tests/FullSchemaTest.cs new file mode 100644 index 0000000..db3b5bc --- /dev/null +++ b/AvroSchemaGenerator.Tests/FullSchemaTest.cs @@ -0,0 +1,306 @@ +using Avro; +using Avro.IO; +using Avro.Reflect; +using AvroSchemaGenerator.Attributes; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.IO; +using Xunit; +using Xunit.Abstractions; + +namespace AvroSchemaGenerator.Tests +{ + public class FullSchemaTest + { + + private readonly ITestOutputHelper _output; + + public FullSchemaTest(ITestOutputHelper output) + { + this._output = output; + + } + + class IntTest + { + public int Age { get; set; } + [Ignore] + public bool IsTrue => Age > 40; + } + + [Fact] + public void TestInt() + { + var actualSchema = typeof(IntTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"IntTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"int\"}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class IntNullableTest + { + public int? Age { get; set; } + } + + [Fact] + public void TestIntNullable() + { + var actualSchema = typeof(IntNullableTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"IntNullableTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":[\"null\",\"int\"],\"default\":null}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class IntDefaultTest + { + [DefaultValue(100)] + public int Age { get; set; } + } + + [Fact] + public void TestIntDefault() + { + var actualSchema = typeof(IntDefaultTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"IntDefaultTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"int\",\"default\":100}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class StringTest + { + public string Age { get; set; } + } + + [Fact] + public void TestString() + { + var actualSchema = typeof(StringTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"StringTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":[\"null\",\"string\"],\"default\":null}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class StringRequiredTest + { + [Required] + public string Age { get; set; } + [Ignore] + public bool IsTrue => Age == "45"; + } + + [Fact] + public void TestStringRequired() + { + var actualSchema = typeof(StringRequiredTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"StringRequiredTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"string\"}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class StringRequiredDefaultTest + { + [Required] + [DefaultValue("100")] + public string Age { get; set; } + + [Ignore] + public bool IsTrue => Age == "45"; + } + + [Fact] + public void TestStringRequiredDefault() + { + var actualSchema = typeof(StringRequiredDefaultTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"StringRequiredDefaultTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"string\",\"default\":\"100\"}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + struct StructTest + { + public int Age { get; set; } + } + + [Fact] + public void TestStructTest() + { + var actualSchema = typeof(StructTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"StructTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"int\"}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + + class ClassFieldTest + { + [DefaultValue(null)] + public IntTest Age { get; set; } + [Ignore] + public IntTest AgeIgnore { get; set; } + } + + [Fact] + public void TestClassField() + { + var actualSchema = typeof(ClassFieldTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"ClassFieldTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"IntTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"int\"}]}],\"default\":null}]}"; + //"{"type":"record","namespace":"AvroSchemaGenerator.Tests","name":"ClassFieldTest","fields":[{"name":"Age","type":["null",{"type":"record","namespace":"AvroSchemaGenerator.Tests","name":"Age","fields":[{"name":"Age","type":"int"}]}],"default":null}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class ClassFieldRequiredTest + { + [Required] + public IntTest Age { get; set; } + [Ignore] + public DateTime TimestampAsDateTime => DateTime.FromBinary(long.MaxValue); + } + + [Fact] + public void TestClassFieldRequired() + { + var actualSchema = typeof(ClassFieldRequiredTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"ClassFieldRequiredTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"IntTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"int\"}]}}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + class StructFieldTest + { + [Required] + public StructTest Age { get; set; } + } + + [Fact] + public void TestStructField() + { + var actualSchema = typeof(StructFieldTest).GetSchema(); + _output.WriteLine(actualSchema); + var expectedSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"StructFieldTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"StructTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Age\",\"type\":\"int\"}]}}]}"; + Assert.Equal(expectedSchema, actualSchema); + } + + public class RecType + { + public string Name { get; set; } + public RecType Child { get; set; } + } + public class RecTypeNestedRecursion + { + public string Name { get; set; } + public RecTypeNestedRecursion Child { get; set; } + public RecTypeRecursive Recursive { get; set; } + } + public class RecTypeRecursive + { + public string Name { get; set; } + public RecTypeRecursive Child { get; set; } + } + + public class NestedSchema + { + public RecType Foo { get; set; } + public RecType Bar { get; set; } + } + public class NestedSchemaWithDifferentSchemaButSamePropertyName + { + public RecTypeNestedRecursion Foo { get; set; } + public RecTypeNestedRecursion Bar { get; set; } + } + class RecTypeRequired + { + [Required] + public string Name { get; set; } + [Required] + public RecTypeRequired Child { get; set; } + } + [Fact] + public void NestedTypesProduceValidAvroSchema() + { + try + { + var simple = typeof(NestedSchema).GetSchema(); + _output.WriteLine(simple); + var schema = Schema.Parse(simple); + var data = new NestedSchema + { + Foo = new RecType + { + Name = "Foo-Name", + Child = new RecType + { + Name = "Foo Grand Child" + } + }, + Bar = new RecType + { + Name = "Bar-Name", + Child = new RecType + { + Name = "Bar Grand Child" + } + } + }; + var reader = new ReflectReader(schema, schema); + var writer = new ReflectWriter(schema); + var msgBytes = Write(data, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + Assert.True(msg.Foo.Name == "Foo-Name"); + } + catch(Exception ex) + { + _output.WriteLine(ex.ToString()); + } + } + [Fact] + public void TestRecType() + { + var expected = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"RecType\",\"type\":\"record\",\"fields\":[{\"name\":\"Name\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Child\",\"type\":[\"null\",\"RecType\"],\"default\":null}]}"; + var actual = typeof(RecType).GetSchema(); + _output.WriteLine(actual); + Assert.Equal(expected, actual); + } + [Fact] + public void TestRecTypeRequired() + { + var expected = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"RecTypeRequired\",\"type\":\"record\",\"fields\":[{\"name\":\"Name\",\"type\":\"string\"},{\"name\":\"Child\",\"type\":\"RecTypeRequired\"}]}"; + var actual = typeof(RecTypeRequired).GetSchema(); + _output.WriteLine(actual); + Assert.Equal(expected, actual); + } + + class WithGuidType + { + [Required] + public Guid Id { get; set; } + } + [Fact] + public void TestGuidType() + { + var expected = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"WithGuidType\",\"type\":\"record\",\"fields\":[{\"name\":\"Id\",\"type\":{\"type\":\"string\",\"logicalType\":\"uuid\"}}]}"; + var actual = typeof(WithGuidType).GetSchema(); + _output.WriteLine(actual); + Assert.Equal(expected, actual); + + } + private sbyte[] Write(T message, ReflectWriter writer) + { + var ms = new MemoryStream(); + Avro.IO.Encoder e = new BinaryEncoder(ms); + writer.Write(message, e); + ms.Flush(); + ms.Position = 0; + var b = ms.ToArray(); + return (sbyte[])(object)b; + } + public T Read(Stream stream, ReflectReader reader) + { + stream.Seek(0, SeekOrigin.Begin); + return reader.Read(default, new BinaryDecoder(stream)); + } + } +} \ No newline at end of file diff --git a/AvroSchemaGenerator.Tests/GetSchemaTest.cs b/AvroSchemaGenerator.Tests/GetSchemaTest.cs new file mode 100644 index 0000000..fffe828 --- /dev/null +++ b/AvroSchemaGenerator.Tests/GetSchemaTest.cs @@ -0,0 +1,660 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Numerics; +using Avro; +using Avro.IO; +using Avro.Reflect; +using AvroSchemaGenerator.Attributes; +using AvroSchemaGenerator.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace AvroSchemaGenerator.Tests +{ + [Collection("GetSchemaTest")] + public class GetSchemaTest + { + private readonly ITestOutputHelper _output; + + public GetSchemaTest(ITestOutputHelper output) + { + this._output = output; + } + [Fact] + public void TestSimpleFoo() + { + var simple = typeof(SimpleFoo).GetSchema(); + _output.WriteLine(simple); + var schema = Schema.Parse(simple); + var writer = new ReflectWriter(schema); + Assert.Contains("Age", simple); + } + [Fact] + public void TestSimpleRecord() + { + var simple = typeof(SimpleRecord).GetSchema(); + _output.WriteLine(simple); + var schema = Schema.Parse(simple); + var writer = new ReflectWriter(schema); + Assert.Contains("Age", simple); + } + + [Fact] + public void TestFoo() + { + var simple = typeof(Foo).GetSchema(); + _output.WriteLine(simple); + var schema = Schema.Parse(simple); + var writer = new ReflectWriter(schema); + Assert.Contains("Age", simple); + } + [Fact] + public void TestFooCustom() + { + var simplecu = typeof(FooCustom).GetSchema(); + _output.WriteLine(simplecu); + var schema = Schema.Parse(simplecu); + var writer = new ReflectWriter(schema); + Assert.Contains("EntryYear", simplecu); + Assert.Contains("Level", simplecu); + } + [Fact] + public void TestRecursiveArray() + { + var actual = typeof(Family).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.True(true); + } + [Fact] + public void TestDictionaryRecursiveArray() + { + var actual = typeof(Dictionary).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.True(true); + } + [Fact] + public void TestDictionaryRecursive() + { + try + { + var actual = typeof(DictionaryRecursive).GetSchema(); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + _output.WriteLine(actual); + } + catch (Exception ex) + { + _output.WriteLine(ex.ToString()); + Assert.True(false); + } + } + [Fact] + public void TestRecursiveSchema() + { + try + { + var actual = typeof(Recursive).GetSchema(); + var schema = Schema.Parse(actual); + _output.WriteLine(actual); + var recursive = new Recursive + { + Fo = new SimpleFoo + { + Age = 67, + Attending = true, + FactTime = 90909099L, + Id = new byte[0] { }, + Name = "Ebere", + Point = 888D, + Precision = 787F + }, + Recurse = new Recursive + { + Fo = new SimpleFoo + { + Age = 6, + Attending = false, + FactTime = 90L, + Id = new byte[0] { }, + Name = "Ebere Abanonu", + Point = 88D, + Precision = 78F + }, + } + }; + var writer = new ReflectWriter(schema); + var reader = new ReflectReader(schema, schema); + var msgBytes = Write(recursive, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + } + catch (Exception ex) + { + _output.WriteLine(ex.ToString()); + Assert.True(false); + } + } + [Fact] + public void TestEnums() + { + var expectSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"MediaStream\",\"type\":\"record\",\"fields\":[{\"name\":\"Id\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Title\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Type\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"MediaType\",\"symbols\":[\"Video\",\"Audio\"]}},{\"name\":\"Container\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"MediaContainer\",\"symbols\":[\"Flv\",\"Mp3\",\"Avi\",\"Mp4\"]}},{\"name\":\"Media\",\"type\":[\"null\",\"bytes\"],\"default\":null}]}"; + var actual = typeof(MediaStream).GetSchema(); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + _output.WriteLine(actual); + + Assert.Equal(expectSchema, actual); + } + + [Fact] + public void TestMultipleEnums() + { + var expectSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"MultipleEnums\",\"type\":\"record\",\"fields\":[{\"name\":\"Type\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"MediaType\",\"symbols\":[\"Video\",\"Audio\"]}},{\"name\":\"TypeTo\",\"type\":\"MediaType\"},{\"name\":\"Month\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"Month\",\"symbols\":[\"January\",\"February\",\"March\",\"April\",\"June\",\"July\"]}},{\"name\":\"Container\",\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"MediaContainer\",\"symbols\":[\"Flv\",\"Mp3\",\"Avi\",\"Mp4\"]}},{\"name\":\"InnerAliases\",\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"InnerAliases\",\"type\":\"record\",\"fields\":[{\"name\":\"Container\",\"type\":[\"null\",\"MediaContainer\"]},{\"name\":\"Title\",\"type\":[\"null\",\"string\"],\"default\":null}]}]},{\"name\":\"YearlyMovies\",\"type\":{\"type\":\"map\",\"values\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"MovieAliase\",\"type\":\"record\",\"aliases\":[\"Movies_Aliase\"],\"fields\":[{\"name\":\"Dated\",\"aliases\":[\"DateCreated\"],\"type\":\"long\"},{\"name\":\"Year\",\"aliases\":[\"ReleaseYear\"],\"type\":\"int\"},{\"name\":\"Month\",\"aliases\":[\"ReleaseMonth\"],\"type\":\"Month\"}]}},\"default\":{}}]}"; + var actual = typeof(MultipleEnums).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + + Assert.Equal(expectSchema, actual); + Assert.NotNull(schema); + } + + [Fact] + public void TestClassWhichHasFieldsWithSameType() + { + var expectSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"ClassWhichHasFieldsWithSameType\",\"type\":\"record\",\"fields\":[{\"name\":\"FirstBook\",\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"Book\",\"type\":\"record\",\"fields\":[{\"name\":\"Author\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Title\",\"type\":[\"null\",\"string\"],\"default\":null}]}]},{\"name\":\"SecondBook\",\"type\":[\"null\",\"Book\"]}]}"; + var actual = typeof(ClassWhichHasFieldsWithSameType).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + + Assert.Equal(expectSchema, actual); + Assert.NotNull(schema); + } + + [Fact] + public void TestAliasesList() + { + var expectSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"ClassWithAliasesWithList\",\"aliases\":[\"InterLives\",\"CountrySide\"],\"type\":\"record\",\"fields\":[{\"name\":\"City\",\"aliases\":[\"TownHall\",\"Province\"],\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"State\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Movie\",\"aliases\":[\"PopularMovie\"],\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"InnerAliases\",\"type\":\"record\",\"fields\":[{\"name\":\"Container\",\"aliases\":[\"Media\"],\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"MediaContainer\",\"symbols\":[\"Flv\",\"Mp3\",\"Avi\",\"Mp4\"]}},{\"name\":\"Title\",\"type\":[\"null\",\"string\"],\"default\":null}]}]},{\"name\":\"Popular\",\"aliases\":[\"PopularMediaType\"],\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"MediaType\",\"symbols\":[\"Video\",\"Audio\"]}},{\"name\":\"Movies\",\"aliases\":[\"MovieCollection\"],\"type\":[\"null\",{\"type\":\"array\",\"items\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"MovieAliase\",\"type\":\"record\",\"aliases\":[\"Movies_Aliase\"],\"fields\":[{\"name\":\"Dated\",\"aliases\":[\"DateCreated\"],\"type\":\"long\"},{\"name\":\"Year\",\"aliases\":[\"ReleaseYear\"],\"type\":\"int\"},{\"name\":\"Month\",\"aliases\":[\"ReleaseMonth\"],\"type\":{\"namespace\":\"AvroSchemaGenerator.Tests\",\"type\":\"enum\",\"name\":\"Month\",\"symbols\":[\"January\",\"February\",\"March\",\"April\",\"June\",\"July\"]}}]}}]}]}"; + var actual = typeof(ClassWithAliasesWithList).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.Equal(expectSchema, actual); + } + [Fact] + public void TestAliasesDictionary() + { + var actual = typeof(ClassWithAliasesWithDictionary).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.True(true); + } + [Fact] + public void TestStaticFieldsAreIgnored() + { + var expectSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"SimpleWithStaticFieldsAndNestedClass\",\"type\":\"record\",\"fields\":[{\"name\":\"FactTime\",\"type\":\"long\"},{\"name\":\"SimpleStaticInner\",\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"SimpleWithStaticFields\",\"type\":\"record\",\"fields\":[{\"name\":\"Name\",\"type\":[\"null\",\"string\"],\"default\":null}]}]}]}"; + var actual = typeof(SimpleWithStaticFieldsAndNestedClass).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.Equal(expectSchema, actual); + } + [Fact] + public void TestEnumType() + { + var expectSchema = "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"Month\",\"type\":\"enum\",\"symbols\":[\"January\",\"February\",\"March\",\"April\",\"June\",\"July\"]}"; + var actual = typeof(Month).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.Equal(expectSchema, actual); + } + [Fact] + public void TestNoNamespaceType() + { + var expectSchema = "{\"name\":\"ClassWithoutNamespace\",\"type\":\"record\",\"fields\":[{\"name\":\"ForReal\",\"type\":\"boolean\"},{\"name\":\"Comment\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Book\",\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"Book\",\"type\":\"record\",\"fields\":[{\"name\":\"Author\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"Title\",\"type\":[\"null\",\"string\"],\"default\":null}]}]}]}"; + + var actual = typeof(ClassWithoutNamespace).GetSchema(); + _output.WriteLine(actual); + + Assert.Equal(expectSchema, actual); + var schema = Schema.Parse(actual); + var data = new ClassWithoutNamespace + { + ForReal = true, + Comment = "Harry Pull Requests", + Book = new Book + { + Author = "Ebere Abanonu", + Title = "How to skin a PR!!!" + } + }; + var writer = new ReflectWriter(schema); + var reader = new ReflectReader(schema, schema); + var msgBytes = Write(data, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + //Assert.True(msg[0] == 10); + //Assert.True(msg[1] == 100); + } + [Theory] + [InlineData(typeof(List))] + [InlineData(typeof(IEnumerable))] + public void TestListType(Type type) + { + var expectSchema = "{\"namespace\":\"System.Collections.Generic\",\"type\":\"array\",\"items\":\"long\",\"default\":[]}"; + var actual = type.GetSchema(); + _output.WriteLine(actual); + + Assert.Equal(expectSchema, actual); + var schema = Schema.Parse(actual); + var data = new List() + { + 10, 100 + }; + var writer = new ReflectWriter>(schema); + var reader = new ReflectReader>(schema, schema); + var msgBytes = Write(data, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + Assert.True(msg[0] == 10); + Assert.True(msg[1] == 100); + } + [Fact] + public void TestUserListType() + { + var actual = typeof(List).GetSchema(); + _output.WriteLine(actual); + + var schema = Schema.Parse(actual); + var data = new List() + { + new Book{ Author = "Ebere Abanonu", Title="Avro 101" }, + new Book{ Author = "Apache Avro", Title = "Avro Schema" } + }; + var writer = new ReflectWriter>(schema); + var reader = new ReflectReader>(schema, schema); + var msgBytes = Write(data, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + Assert.True(msg[0].Author == "Ebere Abanonu"); + } + [Fact] + public void TestDictionaryType() + { + var expectSchema = "{\"namespace\":\"System.Collections.Generic\",\"type\":\"map\",\"values\":\"long\",\"default\":{}}"; + var actual = typeof(Dictionary).GetSchema(); + _output.WriteLine(actual); + + Assert.Equal(expectSchema, actual); + var schema = Schema.Parse(actual); + var data = new Dictionary() + { + {"Index", 10 }, + {"Pos", 100 } + }; + var writer = new ReflectWriter>(schema); + var reader = new ReflectReader>(schema, schema); + var msgBytes = Write(data, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + Assert.True(msg["Index"] == 10); + Assert.True(msg["Pos"] == 100); + } + [Fact] + public void TestUserDictionaryType() + { + var actual = typeof(Dictionary).GetSchema(); + _output.WriteLine(actual); + + var schema = Schema.Parse(actual); + var data = new Dictionary() + { + {"First", new Book{ Author = "Ebere Abanonu", Title="Avro 101" } }, + {"Second", new Book{ Author = "Apache Avro", Title = "Avro Schema" } } + }; + var writer = new ReflectWriter>(schema); + var reader = new ReflectReader>(schema, schema); + var msgBytes = Write(data, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + } + + public class CustomDefinitionTest + { + [AvroSchema("{\n" + + " \"type\": \"bytes\",\n" + + " \"logicalType\": \"decimal\",\n" + + " \"precision\": 10,\n" + + " \"scale\": 6\n" + + "}")] + public AvroDecimal DecimalAvro { get; set; } + } + + public class NestedCustomDefinitionTest + { + public InnerClass Inner { get; set; } + + public class InnerClass + { + [AvroSchema("{\n" + + " \"type\": \"bytes\",\n" + + " \"logicalType\": \"decimal\",\n" + + " \"precision\": 10,\n" + + " \"scale\": 6\n" + + "}")] + public BigInteger BI { get; set; } + } + } + + public class FailedCustomDefinitionTest + { + [AvroSchema("\n" + + " \"type\" \"bytes\",\n" + + " \"logicalType\": \"decimal\",\n" + + " \"precision\": 10,\n" + + " \"scale\": 6\n" + + "}")] + public AvroDecimal DecimalAvro { get; set; } + } + + [Fact] + public void TestCustomDefinition() + { + var expectSchema = + "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"CustomDefinitionTest\",\"type\":\"record\",\"fields\":[{\"name\":\"DecimalAvro\",\"type\":[\"null\",{\"type\":\"bytes\",\"logicalType\":\"decimal\",\"precision\":10,\"scale\":6}],\"default\":null}]}"; + var actual = typeof(CustomDefinitionTest).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.Equal(expectSchema, actual); + } + + [Fact] + public void TestNestedCustomDefinition() + { + var expectSchema = + "{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"NestedCustomDefinitionTest\",\"type\":\"record\",\"fields\":[{\"name\":\"Inner\",\"type\":[\"null\",{\"namespace\":\"AvroSchemaGenerator.Tests\",\"name\":\"InnerClass\",\"type\":\"record\",\"fields\":[{\"name\":\"BI\",\"type\":[\"null\",{\"type\":\"bytes\",\"logicalType\":\"decimal\",\"precision\":10,\"scale\":6}],\"default\":null}]}]}]}"; + var actual = typeof(NestedCustomDefinitionTest).GetSchema(); + _output.WriteLine(actual); + var schema = Schema.Parse(actual); + var writer = new ReflectWriter(schema); + + Assert.Equal(expectSchema, actual); + } + + [Fact] + public void TestFailedCustomDefinition() + { + Assert.Throws(() => typeof(FailedCustomDefinitionTest).GetSchema()); + } + + private sbyte[] Write(T message, ReflectWriter writer) + { + var ms = new MemoryStream(); + Encoder e = new BinaryEncoder(ms); + writer.Write(message, e); + ms.Flush(); + ms.Position = 0; + var b = ms.ToArray(); + return (sbyte[])(object)b; + } + public T Read(Stream stream, ReflectReader reader) + { + stream.Seek(0, SeekOrigin.Begin); + return reader.Read(default, new BinaryDecoder(stream)); + } + } + public class SimpleFoo + { + [Required] + public int Age { get; set; } + [DefaultValue(null)] + public string Name { get; set; } + public long FactTime { get; set; } + public double Point { get; set; } + public float Precision { get; set; } + public bool Attending { get; set; } + public byte[] Id { get; set; } + } + public class Foo + { + public SimpleFoo Fo { get; set; } + [Required] + public List Courses { get; set; } + [Required] + public IEnumerable Lessons { get; set; } + [Required] + public Dictionary Normal { get; set; } + + } + public class Dictionary + { + public SimpleFoo Fo { get; set; } + [Required] + public List Courses { get; set; } + [Required] + public Dictionary Families { get; set; } + + } + public class DictionaryRecursive + { + public SimpleFoo Fo { get; set; } + [Required] + public List Courses { get; set; } + [Required] + public Dictionary Diction { get; set; } + + } + public class FooCustom + { + public SimpleFoo Fo { get; set; } + public List Courses { get; set; } + public Dictionary Departments { get; set; } + } + public class Recursive + { + public SimpleFoo Fo { get; set; } + public Recursive Recurse { get; set; } + } + + public class Course + { + [DefaultValue("200")] + [Required] + public string Level { get; set; } + + [Required] + public int Year { get; set; } + + [DefaultValue("Closed")] + public string State { get; set; } + + public string Gender { get; set; } + } + + public class School + { + public string State { get; set; } + public string Year { get; set; } + public List Schools { get; set; } + } + public class Lecturers + { + public int EntryYear { get; set; } + public string Name { get; set; } + } + + public class Book + { + public string Author { get; set; } + public string Title { get; set; } + } + public class Family + { + [Required] + public List Members { get; set; } + } + + public class Person + { + [Required] + public string FirstName { get; set; } + [Required] + public string LastName { get; set; } + public School Schools { get; set; } + public List Books { get; set; } + [Required] + public List Children { get; set; } + } + + public class ClassWhichHasFieldsWithSameType + { + public Book FirstBook { get; set; } + public Book SecondBook { get; set; } + } + + [Aliases("InterLives", "CountrySide")] + public sealed class ClassWithAliasesWithList + { + [Aliases("TownHall", "Province")] + public string City { get; set; } + public string State { get; set; } + [Aliases("PopularMovie")] + public InnerAliases Movie { get; set; } + + [Aliases("PopularMediaType")] + public MediaType Popular { get; set; } + + [Aliases("MovieCollection")] + public List Movies { get; set; } + } + + [Aliases("InterLives", "CountrySide")] + public sealed class ClassWithAliasesWithDictionary + { + [Aliases("TownHall", "Province")] + public string City { get; set; } + public string State { get; set; } + [Aliases("PopularMovie")] + public InnerAliases Movie { get; set; } + + [Aliases("PopularMediaType")] + public MediaType Popular { get; set; } + + [Aliases("MoviesByYear")] + public Dictionary YearlyMovies { get; set; } + } + + public sealed class InnerAliases + { + [Aliases("Media")] + public MediaContainer Container { get; set; } + public string Title { get; set; } + } + + [Aliases("Movies_Aliase")] + public sealed class MovieAliase + { + [Aliases("DateCreated")] + public long Dated { get; set; } + [Aliases("ReleaseYear")] + public int Year { get; set; } + [Aliases("ReleaseMonth")] + public Month Month { get; set; } + } + public class MediaStream + { + public string Id { get; set; } + + public string Title { get; set; } + public MediaType Type { get; set; } + public MediaContainer Container { get; set; } + public byte[] Media { get; set; } + } + + public class MultipleEnums + { + public MediaType Type { get; set; } + public MediaType TypeTo { get; set; } + public Month Month { get; set; } + public MediaContainer Container { get; set; } + public InnerAliases InnerAliases { get; set; } + public Dictionary YearlyMovies { get; set; } + } + + public enum Month + { + January, + February, + March, + April, + June, + July + } + + public enum MediaType + { + Video, + Audio + } + + public enum MediaContainer + { + Flv, + Mp3, + Avi, + Mp4 + } + + public class SimpleWithStaticFields + { + private static readonly string _staticFieldOnInner = "a field"; + public static string StaticFieldOnInner => _staticFieldOnInner; + + public string Name { get; set; } + } + + public class SimpleWithStaticFieldsAndNestedClass + { + private static readonly string _staticField = "a field"; + public static string StaticField => _staticField; + + public long FactTime { get; set; } + + public SimpleWithStaticFields SimpleStaticInner { get; set; } + } + + public class SimpleRecord + { + public string Name { get; set; } + public int Age { get; set; } + } +} + +public class ClassWithoutNamespace +{ + public bool ForReal { get; set; } + public string Comment { get; set; } + public Book Book { get; set; } +} \ No newline at end of file diff --git a/AvroSchemaGenerator.Tests/LogicalTypeTests.cs b/AvroSchemaGenerator.Tests/LogicalTypeTests.cs new file mode 100644 index 0000000..221f05d --- /dev/null +++ b/AvroSchemaGenerator.Tests/LogicalTypeTests.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using Avro; +using Avro.IO; +using Avro.Specific; +using Avro.Util; +using AvroSchemaGenerator.Attributes; +using Xunit; +using Xunit.Abstractions; + +namespace AvroSchemaGenerator.Tests +{ + [Collection("LogicalTypeTests")] + public class LogicalTypeTests + { + private readonly ITestOutputHelper _output; + + public LogicalTypeTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void LogicalDate() + { + SpecificDatumReader reader; + SpecificDatumWriter writer; + var simple = typeof(MessageDateKind).GetSchema(); + _output.WriteLine(simple); + var schema = (RecordSchema)Schema.Parse(simple); + reader = new SpecificDatumReader(schema, schema); + writer = new SpecificDatumWriter(schema); + var msgBytes = + Write( + new MessageDateKind + { + Schema = schema, + CreatedTime = DateTime.Now, + DayOfWeek = "Saturday", + Size = new AvroDecimal(102.65M), + CustomDecimal = new AvroDecimal(102.6578M) + }, writer); + using var stream = new MemoryStream((byte[]) (object) msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + Assert.True(msg.Size == 102.65M); + Assert.True(msg.CustomDecimal == 102.6578M); + } + [Fact] + public void LogicalTime() + { + SpecificDatumReader reader; + SpecificDatumWriter writer; + var simple = typeof(MessageTimeKind).GetSchema(); + _output.WriteLine(simple); + var schema = (RecordSchema)Schema.Parse(simple); + reader = new SpecificDatumReader(schema, schema); + writer = new SpecificDatumWriter(schema); + var msgBytes = Write(new MessageTimeKind { Schema = schema, TimeMicros = TimeSpan.FromSeconds(60), TimeMillis = TimeSpan.FromSeconds(300) }, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + } + [Fact] + public void LogicalTimeStamp() + { + SpecificDatumReader reader; + SpecificDatumWriter writer; + var simple = typeof(MessageTimestampKind).GetSchema(); + _output.WriteLine(simple); + var schema = (RecordSchema)Schema.Parse(simple); + reader = new SpecificDatumReader(schema, schema); + writer = new SpecificDatumWriter(schema); + var msgBytes = Write(new MessageTimestampKind { Schema = schema, StampMicros = DateTime.Now, StampMillis = DateTime.Now.AddDays(20)}, writer); + using var stream = new MemoryStream((byte[])(object)msgBytes); + var msg = Read(stream, reader); + Assert.NotNull(msg); + } + private sbyte[] Write(T message, SpecificDatumWriter writer) + { + var ms = new MemoryStream(); + Encoder e = new BinaryEncoder(ms); + writer.Write(message, e); + ms.Flush(); + ms.Position = 0; + var b = ms.ToArray(); + return (sbyte[])(object)b; + } + public T Read(Stream stream, SpecificDatumReader reader) + { + stream.Seek(0, SeekOrigin.Begin); + return reader.Read(default, new BinaryDecoder(stream)); + } + } + public class MessageTimeKind: ISpecificRecord + { + [LogicalType(LogicalTypeKind.TimeMicrosecond)] + public TimeSpan TimeMicros { get; set; } + + [LogicalType(LogicalTypeKind.TimeMillisecond)] + public TimeSpan TimeMillis { get; set; } + + [Ignore] + public Schema Schema { get; set; } + + public object Get(int fieldPos) + { + switch (fieldPos) + { + case 0: return TimeMicros; + case 1: return TimeMillis; + default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Get()"); + }; + } + + public void Put(int fieldPos, object fieldValue) + { + switch (fieldPos) + { + case 0: TimeMicros = (TimeSpan)fieldValue; break; + case 1: TimeMillis = (TimeSpan)fieldValue; break; + default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Put()"); + }; + } + } + public class MessageTimestampKind: ISpecificRecord + { + [LogicalType(LogicalTypeKind.TimestampMicrosecond)] + public DateTime StampMicros { get; set; } + + [LogicalType(LogicalTypeKind.TimestampMillisecond)] + public DateTime StampMillis { get; set; } + + [Ignore] + public Schema Schema { get; set; } + + public object Get(int fieldPos) + { + switch (fieldPos) + { + case 0: return StampMicros; + case 1: return StampMillis; + default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Get()"); + }; + } + + public void Put(int fieldPos, object fieldValue) + { + switch (fieldPos) + { + case 0: StampMicros = (DateTime)fieldValue; break; + case 1: StampMillis = (DateTime)fieldValue; break; + default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Put()"); + }; + } + } + public class MessageDateKind: ISpecificRecord + { + [LogicalType(LogicalTypeKind.Date)] + public DateTime CreatedTime { get; set; } + public AvroDecimal Size { get; set; } + public string DayOfWeek { get; set; } + [AvroSchema("{\n" + + " \"type\": \"bytes\",\n" + + " \"logicalType\": \"decimal\",\n" + + " \"precision\": 8,\n" + + " \"scale\": 4\n" + + "}")] + public AvroDecimal CustomDecimal { get; set; } + + [Ignore] + public Schema Schema { get; set; } + + public object Get(int fieldPos) + { + switch (fieldPos) + { + case 0: return CreatedTime; + case 1: return Size; + case 2: return DayOfWeek; + case 3: return CustomDecimal; + default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Get()"); + }; + } + + public void Put(int fieldPos, object fieldValue) + { + switch (fieldPos) + { + case 0: CreatedTime = (DateTime)fieldValue; break; + case 1: Size = (AvroDecimal)fieldValue; break; + case 2: DayOfWeek = (System.String)fieldValue; break; + case 3: CustomDecimal = (AvroDecimal) fieldValue; break; + default: throw new AvroRuntimeException("Bad index " + fieldPos + " in Put()"); + }; + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1142534 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +## [vNext] + +## [2.10.0] / 2023-12-16 +- [Add] `net8.0` +- [update] NUKE 7.0.6 +- [Added] `CODE OF CONDUCT` + +## [2.9.2] / 2023-07-10 +- Add `netstandard2.0` +- [update] NUKE 7.0.2 + +## [2.9.1] / 2023-03-19 +- Package License + +## [2.9.0] / 2023-02-04 +- The Guid type in .NET logically maps the the AVRO UUID type @addisonj [https://github.com/eaba/AvroSchemaGenerator/commit/a4cd2255306531fbf24064947377789dc730f2c6] + +## [2.8.0] / 2022-12-24 +- [update] .NET 7 +- [fix] Resolves #64 Fixed that the same enum can be used multiple times in the schema @danny-krueger [https://github.com/eaba/AvroSchemaGenerator/pull/74/commits/ba622efc263f89ed8d5bc826493cf8a33637b580] + +## [2.7.0] / 2022-07-20 +- Add Custom AVRO definition support @RobertIndie [https://github.com/eaba/AvroSchemaGenerator/commit/81272d5e24e63e152369c14b6c564333fc3aa9ac] + +## [2.6.0] / 2022-02-20 +- Add back support for NET standard 2.0 +- Add `IEnumerabl<>` support - @ThomasHarzer [https://github.com/eaba/AvroSchemaGenerator/commit/38c38308d183ecda416ce5b8b946ebe681c2f171] + +## [2.5.2-Alpha.1] / 2022-01-16 +- Improved `CI` + +## [2.5.1] / 2022-01-14 +- Add `.NET 6` support + +[vNext]: https://github.com/eaba/AvroSchemaGenerator/compare/2.10.0...HEAD +[2.10.0]: https://github.com/eaba/AvroSchemaGenerator/compare/2.9.2...2.10.0 +[2.9.2]: https://github.com/eaba/AvroSchemaGenerator/compare/2.9.1...2.9.2 +[2.9.1]: https://github.com/eaba/AvroSchemaGenerator/compare/2.9.0...2.9.1 +[2.9.0]: https://github.com/eaba/AvroSchemaGenerator/compare/2.8.0...2.9.0 +[2.8.0]: https://github.com/eaba/AvroSchemaGenerator/compare/2.7.0...2.8.0 +[2.7.0]: https://github.com/eaba/AvroSchemaGenerator/compare/2.6.0...2.7.0 +[2.6.0]: https://github.com/eaba/AvroSchemaGenerator/compare/2.5.2-Alpha.1...2.6.0 +[2.5.2-Alpha.1]: https://github.com/eaba/AvroSchemaGenerator/compare/2.5.1...2.5.2-Alpha.1 +[2.5.1]: https://github.com/eaba/AvroSchemaGenerator/tree/2.5.1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..68add25 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant +to clarify expected behavior in our community. +For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..e147d63 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..b55263c --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,50 @@ +assembly-versioning-scheme: MajorMinorPatch +assembly-file-versioning-scheme: MajorMinorPatch +mode: ContinuousDelivery +next-version: 2.9.1 +branches: # https://gitversion.net/docs/learn/branching-strategies/gitflow/examples + main: + regex: ^master$|^main$ + mode: ContinuousDelivery + tag: '' + prevent-increment-of-merged-branch-version: true + track-merge-target: false + tracks-release-branches: false + is-release-branch: false + is-mainline: true + pre-release-weight: 55000 + develop: + regex: ^dev(elop)?(ment)?$ + mode: ContinuousDeployment + tag: rc + increment: Minor + prevent-increment-of-merged-branch-version: false + track-merge-target: true + tracks-release-branches: true + is-release-branch: false + is-mainline: false + pre-release-weight: 0 + release: + regex: release?$ + mode: ContinuousDelivery + tag: beta + increment: Patch + prevent-increment-of-merged-branch-version: true + track-merge-target: false + tracks-release-branches: false + is-release-branch: true + is-mainline: false + pre-release-weight: 30000 + feature: + regex: ^features?[/-] + mode: ContinuousDelivery + tag: useBranchName + increment: Inherit + prevent-increment-of-merged-branch-version: false + track-merge-target: false + tracks-release-branches: false + is-release-branch: false + is-mainline: false + pre-release-weight: 30000 +ignore: + sha: [] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2951a49 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# AvroSchemaGenerator +Use to generate Avro Schema with support for **RECURSIVE SCHEMA** + +## Getting Started +Install the NuGet package [AvroSchemaGenerator](https://www.nuget.org/packages/AvroSchemaGenerator/) and copy/paste the code below + +```csharp +using AvroSchemaGenerator; +public class Course +{ + public string Level { get; set; } + + public int Year { get; set; } + + public string State { get; set; } + + public string Gender { get; set; } +} +var avroSchema = typeof(Course).GetSchema(); +``` + +By default, `AvroSchemaGenerator` generates schema with optional fields. The code below is an example of how to mark fields as required + +```csharp +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using AvroSchemaGenerator; +public class Course +{ + [Required] + public string Level { get; set; } + + [Required] + public int Year { get; set; } + + [Required] + public string State { get; set; } + + [Required] + public string Gender { get; set; } +} +var avroSchema = typeof(Course).GetSchema(); +``` + +You can assign a default value as well +```csharp +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using AvroSchemaGenerator; +public class Course +{ + [DefaultValue("200")] + [Required] + public string Level { get; set; } + + [Required] + public int Year { get; set; } + + [DefaultValue("Closed")] + public string State { get; set; } + + public string Gender { get; set; } +} +var avroSchema = typeof(Course).GetSchema(); +``` +## Aliases +```csharp +[Aliases("OldCourse")] +public class Course +{ + [Aliases("Level")] + public string NewLevel { get; set; } +} +``` +## Logical Types +```csharp +public class MessageTimeKind +{ + [LogicalType(LogicalTypeKind.TimeMicrosecond)] + public TimeSpan TimeMicros { get; set; } + + [LogicalType(LogicalTypeKind.TimeMillisecond)] + public TimeSpan TimeMillis { get; set; } +} + +public class MessageTimestampKind +{ + [LogicalType(LogicalTypeKind.TimestampMicrosecond)] + public DateTime StampMicros { get; set; } + + [LogicalType(LogicalTypeKind.TimestampMillisecond)] + public DateTime StampMillis { get; set; } +} + +public class MessageDateKind +{ + [LogicalType(LogicalTypeKind.Date)] + public DateTime CreatedTime { get; set; } + + public AvroDecimal Size { get; set; } + + public string DayOfWeek { get; set; } +} +``` + +## Custom Avro Definition +```csharp +public class CustomDefinition +{ + [AvroSchema("{\n" + + " \"type\": \"bytes\",\n" + + " \"logicalType\": \"decimal\",\n" + + " \"precision\": 10,\n" + + " \"scale\": 6\n" + + "}")] + public AvroDecimal DecimalAvro { get; set; } +} +``` + +## NOTE +- Don't use same declaring type as dictionary value +- Don't use same declaring type as list argument +- Dictionary key must be a string type + +## License + +This project is licensed under the Apache License Version 2.0 - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/SchemaGenerator.sln b/SchemaGenerator.sln new file mode 100644 index 0000000..98d8949 --- /dev/null +++ b/SchemaGenerator.sln @@ -0,0 +1,42 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvroSchemaGenerator", "SchemaGenerator\AvroSchemaGenerator.csproj", "{F13C8E45-F5C8-48EB-B879-63DD464FEFF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvroSchemaGenerator.Tests", "AvroSchemaGenerator.Tests\AvroSchemaGenerator.Tests.csproj", "{30808D11-0F77-4CCD-82C0-10946CA397D4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{DF485363-E6D3-47E3-9A82-EC2D0E50C491}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FC014F32-B268-4452-8C08-4FE19A2BDBF3}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + GitVersion.yml = GitVersion.yml + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F13C8E45-F5C8-48EB-B879-63DD464FEFF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F13C8E45-F5C8-48EB-B879-63DD464FEFF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F13C8E45-F5C8-48EB-B879-63DD464FEFF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F13C8E45-F5C8-48EB-B879-63DD464FEFF0}.Release|Any CPU.Build.0 = Release|Any CPU + {30808D11-0F77-4CCD-82C0-10946CA397D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30808D11-0F77-4CCD-82C0-10946CA397D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30808D11-0F77-4CCD-82C0-10946CA397D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30808D11-0F77-4CCD-82C0-10946CA397D4}.Release|Any CPU.Build.0 = Release|Any CPU + {DF485363-E6D3-47E3-9A82-EC2D0E50C491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF485363-E6D3-47E3-9A82-EC2D0E50C491}.Release|Any CPU.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3437C2B5-7977-4B47-94B6-31328FD6847D} + EndGlobalSection +EndGlobal diff --git a/SchemaGenerator/Attributes/Aliases.cs b/SchemaGenerator/Attributes/Aliases.cs new file mode 100644 index 0000000..f45ea63 --- /dev/null +++ b/SchemaGenerator/Attributes/Aliases.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AvroSchemaGenerator.Attributes +{ + [AttributeUsage(AttributeTargets.All)] + public class AliasesAttribute : Attribute + { + private readonly List _values; + + public AliasesAttribute(params string[] value) + { + _values = value.ToList(); + } + public virtual List Values => _values; + } +} diff --git a/SchemaGenerator/Attributes/AvroSchema.cs b/SchemaGenerator/Attributes/AvroSchema.cs new file mode 100644 index 0000000..18da0d4 --- /dev/null +++ b/SchemaGenerator/Attributes/AvroSchema.cs @@ -0,0 +1,15 @@ +using System; + +namespace AvroSchemaGenerator.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class AvroSchemaAttribute : Attribute + { + public string Value { get; } + + public AvroSchemaAttribute(string value) + { + Value = value; + } + } +} \ No newline at end of file diff --git a/SchemaGenerator/Attributes/Ignore.cs b/SchemaGenerator/Attributes/Ignore.cs new file mode 100644 index 0000000..a238b89 --- /dev/null +++ b/SchemaGenerator/Attributes/Ignore.cs @@ -0,0 +1,9 @@ +using System; + +namespace AvroSchemaGenerator.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class IgnoreAttribute : Attribute + { + } +} diff --git a/SchemaGenerator/Attributes/LogicalType.cs b/SchemaGenerator/Attributes/LogicalType.cs new file mode 100644 index 0000000..c4897c0 --- /dev/null +++ b/SchemaGenerator/Attributes/LogicalType.cs @@ -0,0 +1,22 @@ +using System; + +namespace AvroSchemaGenerator.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class LogicalTypeAttribute : Attribute + { + public LogicalTypeKind Kind { get; } + public LogicalTypeAttribute(LogicalTypeKind kind) + { + Kind = kind; + } + } + public enum LogicalTypeKind + { + Date, + TimeMicrosecond, + TimeMillisecond, + TimestampMicrosecond, + TimestampMillisecond + } +} diff --git a/SchemaGenerator/AvroSchemaGenerator.csproj b/SchemaGenerator/AvroSchemaGenerator.csproj new file mode 100644 index 0000000..72c43b4 --- /dev/null +++ b/SchemaGenerator/AvroSchemaGenerator.csproj @@ -0,0 +1,33 @@ + + + + netstandard2.0;net8.0 + https://github.com/eaba/SchemaGenerator + https://github.com/eaba/SchemaGenerator + README.md + AvroSchemaGenerator + en-US + Apache Avro, Avro Schema + avro-schema.png + Apache-2.0 + + false + + + + + + + + + + + Always + + + True + + + + + diff --git a/SchemaGenerator/DefaultValue.cs b/SchemaGenerator/DefaultValue.cs new file mode 100644 index 0000000..c4cd39e --- /dev/null +++ b/SchemaGenerator/DefaultValue.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using AvroSchemaGenerator.Attributes; + +namespace AvroSchemaGenerator +{ + public static class DefaultValue + { + public static (bool required, bool hasDefault, object defaultValue) GetSchemaCustomAttributes(this PropertyInfo property) + { + var required = IsRequiredProperty(property); + var dv = GetDefaultValueForProperty(property); + return (required, dv.hasDefault, dv.defaultValue); + } + private static (bool hasDefault, object defaultValue) GetDefaultValueForProperty(PropertyInfo property) + { + var defaultAttr = (DefaultValueAttribute)property.GetCustomAttribute(typeof(DefaultValueAttribute)); + return defaultAttr != null ? (true, defaultAttr.Value) : (false, null); + } + private static bool IsRequiredProperty(PropertyInfo property) + { + return property.GetCustomAttribute(typeof(RequiredAttribute)) != null; + } + + public static string GetAvroCustomDefinition(this PropertyInfo property) + { + return ((AvroSchemaAttribute) property.GetCustomAttribute(typeof(AvroSchemaAttribute)))?.Value; + } + } +} diff --git a/SchemaGenerator/GenerateSchema.cs b/SchemaGenerator/GenerateSchema.cs new file mode 100644 index 0000000..24c9468 --- /dev/null +++ b/SchemaGenerator/GenerateSchema.cs @@ -0,0 +1,1607 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using AvroSchemaGenerator.Attributes; + +namespace AvroSchemaGenerator +{ + //https://docs.oracle.com/database/nosql-12.1.3.0/GettingStartedGuide/avroschemas.html + //https://www.jsonschemavalidator.net/ + //https://json-schema-validator.herokuapp.com/avro.jsp + //https://stackoverflow.com/questions/13522132/how-to-exclude-static-property-when-using-getproperties-method + // ReSharper disable once UnusedType.Global + public static class GenerateSchema + { + // ReSharper disable once UnusedMember.Global + public static Dictionary GetSchemaObject(this Type type) + { + if (IsDictionary(type)) + { + var sch = new Dictionary(); + if (!string.IsNullOrEmpty(type.Namespace)) + { + sch.Add("namespace", type.Namespace); + } + sch["type"] = "map"; + var arg = type.GetGenericArguments()[1]; + if (IsUserDefined(arg)) + { + sch["values"] = GetGenericUserDefinedProperties(arg, true, new List()); + } + else + { + sch["values"] = ToAvroDataType(arg.Name, null); + } + sch["default"] = new Dictionary(); + return sch; + } + if (IsList(type)) + { + var sch = new Dictionary(); + if (!string.IsNullOrEmpty(type.Namespace)) + { + sch.Add("namespace", type.Namespace); + } + sch["type"] = "array"; + var arg = type.GetGenericArguments()[0]; + if (IsUserDefined(arg)) + { + sch["items"] = GetGenericUserDefinedProperties(arg, true, new List()); + } + else + { + sch["items"] = ToAvroDataType(arg.Name, null); + } + sch["default"] = new List(); + return sch; + } + var schema = new Dictionary(); + if (!string.IsNullOrEmpty(type.Namespace)) + { + schema.Add("namespace", type.Namespace); + } + schema["name"] = type.Name; + var aliases = GetAliases(type); + if (aliases != null) + { + schema["aliases"] = aliases; + } + + if (type.IsEnum) + { + schema["type"] = "enum"; + schema["symbols"] = GetEnumValues(type); + return schema; + } + + // record type + schema["type"] = "record"; + schema["fields"] = new List>(); + var existingTypes = new List(); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var p in properties) + { + + if (!ShouldIgnore(p)) + PropertyInfo(p, schema, existingTypes); + } + + return schema; + } + + // ReSharper disable once UnusedMember.Global + public static string GetSchema(this Type type) + { + return JsonSerializer.Serialize(type.GetSchemaObject()); + } + + private static void PropertyInfo(PropertyInfo property, Dictionary finalSchema, + List existingTypes) + { + var p = property; + + var customDefinition = CheckAndHandleCustomSchema(p); + if (customDefinition != null) + { + var field = (List>) finalSchema["fields"]; + field.Add(customDefinition); + finalSchema["fields"] = field; + return; + } + + if (p.PropertyType.FullName != null && p.PropertyType.FullName.StartsWith("Avro.")) + { + AddFields(p, finalSchema); + } + else + { + if (IsUserDefined(p)) + { + if (p.PropertyType.GetSubTypeIfNullable().IsEnum) + { + var aliases = GetAliases(p); + var row = GetEnumField(p, existingTypes); + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var fd = (List>) finalSchema["fields"]; + fd.Add(row); + finalSchema["fields"] = fd; + return; + } + + var t = p.PropertyType.Name; + var dt = p.DeclaringType?.Name; + var recursive = t.Equals(dt); + + if (existingTypes.Contains(t)) + { + List lt = null; + if (!p.GetSchemaCustomAttributes().required) + { + lt = new List() {"null", t}; + } + + var row = new Dictionary + { + {"name", p.Name}, {"type", lt == null ? t : (object) lt} + }; + var fd = (List>) finalSchema["fields"]; + fd.Add(row); + finalSchema["fields"] = fd; + return; + } + + if (recursive) + AddReuseType(p, finalSchema); + else + GetUserDefinedProperties(p, finalSchema, existingTypes); + + existingTypes.Add(t); + return; + } + + if (IsFieldListType(p)) + { + var required = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[0]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + var aliases = GetAliases(p); + Dictionary row; + if (recursive) + AddReuseType(p, finalSchema); + else if (IsUserDefined(v)) + { + var schema = GetGenericUserDefinedProperties(v, required, existingTypes); + row = required + ? new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "array"}, {"items", schema}}} + } + : new Dictionary + { + {"name", p.Name}, + { + "type", + new List + { + "null", + new Dictionary {{"type", "array"}, {"items", schema}} + } + } + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var field = (List>) finalSchema["fields"]; + field.Add(row); + finalSchema["fields"] = field; + } + else + { + row = required + ? new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + : new Dictionary + { + {"name", p.Name}, + { + "type", new List + { + "null", + new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var fd = (List>) finalSchema["fields"]; + fd.Add(row); + finalSchema["fields"] = fd; + } + + return; + } + + if (IsFieldDictionaryType(p)) + { + var required = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[1]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + var aliases = GetAliases(p); + Dictionary row; + if (recursive) + AddReuseType(p, finalSchema, v.Name); + else if (IsUserDefined(v)) + { + var schema = GetGenericUserDefinedProperties(v, required, existingTypes); + row = new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "map"}, {"values", schema}}}, + {"default", new Dictionary() } + }; + + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var field = (List>) finalSchema["fields"]; + field.Add(row); + finalSchema["fields"] = field; + } + else + { + row = new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "map"}, + { + "values", ToAvroDataType(v.Name, + LogicalKind(p)) + } + } + }, + { "default", new Dictionary() } + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var fd = (List>) finalSchema["fields"]; + fd.Add(row); + finalSchema["fields"] = fd; + } + + return; + } + + if (p.PropertyType.IsEnum) + { + var aliases = GetAliases(p); + var row = GetEnumField(p, existingTypes); + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var fd = (List>) finalSchema["fields"]; + fd.Add(row); + finalSchema["fields"] = fd; + return; + } + + AddFields(p, finalSchema); + + } + } + + private static Dictionary PropertyInfo(PropertyInfo property, List existingTypes, bool init) + { + var p = property; + if (p.PropertyType.FullName != null && p.PropertyType.FullName.StartsWith("Avro.")) + { + return GetField(p); + } + else + { + if (IsUserDefined(p)) + { + if (p.PropertyType.IsEnum) + { + var aliases = GetAliases(p); + var row = GetEnumField(p, existingTypes); + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + else + { + + var t = p.PropertyType.Name; + var dt = p.DeclaringType?.Name; + var recursive = t.Equals(dt); + + if (existingTypes.Contains(t) && !init) + { + return Reuse(p, t); + } + else if (recursive) + { + return Reuse(p); + } else + if (init) + { + var props = GetPropertyInfo(p, existingTypes); + existingTypes.Add(t); + return props; + } + else + { + var mentionFirstTime = !existingTypes.Contains(t); + if (mentionFirstTime) + { + existingTypes.Add(t); + } + + return PropertyInfo(p, existingTypes, init: mentionFirstTime); //throw new Exception($"The limit for user defined property type has been reached: [{dt}] public {p.PropertyType.Name} {p.Name} {{get; set;}}"); + + } + + } + } + + if (IsFieldListType(p)) + { + var required = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[0]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + var aliases = GetAliases(p); + Dictionary row; + if (recursive) + return Reuse(p); + else if (IsUserDefined(v)) + { + var schema = GetGenericUserDefinedProperties(v, required, existingTypes); + row = required + ? new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "array"}, {"items", schema}}} + } + : new Dictionary + { + {"name", p.Name}, + { + "type", + new List + { + "null", + new Dictionary {{"type", "array"}, {"items", schema}} + } + } + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + else + { + row = required + ? new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + : new Dictionary + { + {"name", p.Name}, + { + "type", new List + { + "null", + new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + } + + if (IsFieldDictionaryType(p)) + { + var required = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[1]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + var aliases = GetAliases(p); + Dictionary row; + if (recursive) + return Reuse(p, v.Name); + else if (IsUserDefined(v)) + { + var schema = GetGenericUserDefinedProperties(v, required, existingTypes); + row = new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "map"}, {"values", schema}}}, + { "default", new Dictionary()} + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + else + { + row = new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "map"}, + { + "values", ToAvroDataType(p.PropertyType.GetGenericArguments()[1].Name, + LogicalKind(p)) + } + } + }, + { "default", new Dictionary() } + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + } + + if (p.PropertyType.IsEnum) + { + var aliases = GetAliases(p); + var row = GetEnumField(p, existingTypes); + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + + return GetField(p); + + } + } + + private static Dictionary GetPropertyInfo(PropertyInfo property, List existingTypes) + { + var p = property; + var aliases = GetAliases(p); + Dictionary row; + if (IsUserDefined(p)) + { + if (p.PropertyType.GetSubTypeIfNullable().IsEnum) + { + row = GetEnumField(p, existingTypes); + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + return rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + + var t = p.PropertyType.Name; + var dt = p.DeclaringType?.Name; + var recursive = t.Equals(dt); + if (recursive || existingTypes.Contains(t)) + { + return ReuseType(p); + } + else + { + existingTypes.Add(t); + return UserDefinedProperties(p, existingTypes); + } + } + + if (IsFieldListType(p)) + { + var required = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[0]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + if (recursive) + return ReuseType(p); + if (IsUserDefined(v)) + { + var schema = GetGenericUserDefinedProperties(v, required, existingTypes); + row = required + ? new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "array"}, {"items", schema}}} + } + : new Dictionary + { + {"name", p.Name}, + { + "type", + new List + { + "null", + new Dictionary {{"type", "array"}, {"items", schema}} + } + } + }; + } + else + row = required + ? new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + : new Dictionary + { + {"name", p.Name}, + { + "type", new List + { + "null", + new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + }; + + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + return rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + else + { + if (IsFieldDictionaryType(p)) + { + var required = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[1]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + if (recursive) + return ReuseType(p); + if (IsUserDefined(v)) + { + var schema = new Dictionary(); + if (!string.IsNullOrEmpty(v.Namespace)) + { + schema.Add("namespace", v.Namespace); + } + schema["name"] = v.Name; + schema["type"] = "record"; + schema["fields"] = GetGenericUserDefinedProperties(v, required, existingTypes); + row = new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "map"}, {"values", schema}} }, + { "default", new Dictionary() } + }; + } + else + row = new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "map"}, + { + "values", ToAvroDataType(p.PropertyType.GetGenericArguments()[1].Name, + LogicalKind(p)) + } + } + }, + { "default", new Dictionary() } + }; + + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + return rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + } + + return GetFields(p); + } + + private static void AddFields(PropertyInfo p, Dictionary finalSchema) + { + var row = GetField(p); + var field = (List>) finalSchema["fields"]; + field.Add(row); + finalSchema["fields"] = field; + } + + private static void AddReuseType(PropertyInfo p, Dictionary finalSchema, string dt = "") + { + var row = Reuse(p, dt); + var field = (List>) finalSchema["fields"]; + field.Add(row); + finalSchema["fields"] = field; + } + + private static Dictionary ReuseType(PropertyInfo p) + { + return Reuse(p); + } + + private static Dictionary GetFields(PropertyInfo p) + { + return GetField(p); + } + + private static Dictionary Reuse(PropertyInfo p, string type = "") + { + (bool isNullable, string name) dt; + if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var t = p.PropertyType.GetGenericArguments()[0]; + dt = (true, t.Name); + } + else + { + dt = (false, p.PropertyType.Name); + } + + var customAttributes = p.GetSchemaCustomAttributes(); + var field = Field(string.IsNullOrWhiteSpace(type) ? dt.name : type, p.Name, customAttributes.required, + customAttributes.hasDefault, customAttributes.defaultValue, dt.isNullable, GetAliases(p)); + return field; + } + + private static Dictionary ReUseSchema(string name, string type, bool required, bool hasDefault, + object defaultValue, bool isNullable, List aliases) + { + var field = Field(type, name, required, hasDefault, defaultValue, isNullable, aliases); + return field; + } + + private static Dictionary CheckAndHandleCustomSchema(PropertyInfo p) + { + var customDefinition = p.GetAvroCustomDefinition(); + if (string.IsNullOrEmpty(customDefinition)) return null; + Dictionary customDict; + try + { + customDict = JsonSerializer.Deserialize>(customDefinition); + } + catch (JsonException jsonException) + { + throw new ArgumentException( + $"Failed to read the custom avro definition: {customDefinition}. Please check if the definition follows the json format.", + jsonException); + } + + var customAttributes = p.GetSchemaCustomAttributes(); + var aliases = GetAliases(p); + var field = Field(customDict, p.Name, customAttributes.required, customAttributes.hasDefault, + customAttributes.defaultValue, true, aliases); + return field; + } + + private static Dictionary GetField(PropertyInfo p) + { + (bool isNullable, string name) dt; + if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var t = p.PropertyType.GetGenericArguments()[0]; + dt = (true, t.Name); + } + else + { + dt = (false, p.PropertyType.Name); + } + + var dT = ToAvroDataType(dt.name, LogicalKind(p)); + var customAttributes = p.GetSchemaCustomAttributes(); + var aliases = GetAliases(p); + var field = Field(dT, p.Name, customAttributes.required, customAttributes.hasDefault, + customAttributes.defaultValue, dt.isNullable, aliases); + return field; + } + + private static Dictionary Field(object type, string name, bool required, bool hasDefault, + object dfault, bool nullable, List aliases) + { + Dictionary fields; + if (required && hasDefault) + { + //required and does have default value + fields = new Dictionary {{"name", name}, {"type", type}, {"default", dfault}}; + } + + else if (required) + { + //required and does not have default value + fields = new Dictionary {{"name", name}, {"type", type}}; + } + else if (hasDefault) + { + //not required and does have default value + fields = nullable + ? new Dictionary + { + {"name", name}, {"type", new List {"null", type}}, {"default", dfault} + } + : new Dictionary {{"name", name}, {"type", type}, {"default", dfault}}; + } + else + //not required and does not have default value + fields = nullable + ? new Dictionary + { + {"name", name}, {"type", new List {"null", type}}, { "default", null } + } + : RequiredOrNullableField(type, name); + + if (aliases != null) + { + var rows = fields.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + return rows.ToDictionary(x => x.Key, x => x.Value); + } + + return fields; + } + + private static Dictionary RequiredOrNullableField(object type, string name) + { + switch (type) + { + case "int": + case "long": + case "double": + case "float": + case "boolean": + return new Dictionary {{"name", name}, {"type", type}}; + default: + return new Dictionary + { + {"name", name}, {"type", new List {"null", type} }, { "default", null} + }; + } + } + + private static List GetEnumValues(Type type) + { + var list = new List(); + var values = Enum.GetValues(type); + foreach (var v in values) + { + list.Add(v.ToString()); + } + + return list; + } + + private static List GetAliases(Type type) + { + var aliases = (AliasesAttribute) type.GetCustomAttribute(typeof(AliasesAttribute)); + return aliases?.Values; + } + + private static bool ShouldIgnore(MemberInfo property) + { + var ignore = (IgnoreAttribute) property.GetCustomAttribute(typeof(IgnoreAttribute)); + return ignore != null; + } + + private static List GetAliases(MemberInfo property) + { + var aliases = (AliasesAttribute) property.GetCustomAttribute(typeof(AliasesAttribute)); + return aliases?.Values; + } + + private static LogicalTypeKind? LogicalKind(MemberInfo property) + { + var kind = (LogicalTypeAttribute) property.GetCustomAttribute(typeof(LogicalTypeAttribute)); + return kind?.Kind; + } + + private static bool IsUserDefined(PropertyInfo p) + { + var type = p.PropertyType.GetSubTypeIfNullable(); + + return type.Namespace != null + && ((type.IsClass || type.IsValueType) + && !type.Namespace.StartsWith("System")); + } + + private static Type GetSubTypeIfNullable(this Type t) + { + var subType = t.IsNullable() ? t.GetGenericArguments()[0] : null; + + return subType ?? t; + } + + private static bool IsNullable(this Type t) + { + return t.IsGenericType && + t.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + private static bool IsUserDefined(Type p) + { + return p.Namespace != null && ((p.IsClass || p.IsValueType) && !p.Namespace.StartsWith("System")); + } + + private static void GetUserDefinedProperties(PropertyInfo property, Dictionary finalSchema, + List existingTypes) + { + var schema = new Dictionary(); + if (!string.IsNullOrEmpty(property.PropertyType.Namespace)) + { + schema.Add("namespace", property.PropertyType.Namespace); + } + schema["name"] = property.PropertyType.Name; + schema["type"] = "record"; + + var aliases = GetAliases(property); + var properties = property.PropertyType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var fieldProperties = new List>(); + foreach (var p in properties) + { + if (ShouldIgnore(p)) + continue; + + var customDefinition = CheckAndHandleCustomSchema(p); + if (customDefinition != null) + { + fieldProperties.Add(customDefinition); + continue; + } + + if (IsUserDefined(p)) + { + var t = p.PropertyType.Name; + var dt = p.DeclaringType?.Name; + var recursive = t.Equals(dt); + if (existingTypes.Contains(t)) + { + var pRequired = p.GetSchemaCustomAttributes().required; + var rw = pRequired + ? new Dictionary {{"name", p.Name}, {"type", t}} + : new Dictionary {{"name", p.Name}, {"type", new List {"null", t}}}; + fieldProperties.Add(rw); + } + else if (recursive) + { + existingTypes.Add(t); + fieldProperties.Add(Reuse(p)); + } + else if (p.PropertyType.IsEnum) + { + var pAli = GetAliases(p); + if (aliases != null) + { + var rows = GetEnumField(p, existingTypes).ToList(); + rows.Insert(1, new KeyValuePair("aliases", pAli)); + fieldProperties.Add(rows.ToDictionary(x => x.Key, x => x.Value)); + } + else fieldProperties.Add(GetEnumField(p, existingTypes)); + + existingTypes.Add(t); + } + else + { + //existing.Add(t); + var rows = PropertyInfo(p, existingTypes, !existingTypes.Contains(t)); + fieldProperties.Add(rows); + } + } + else if (IsFieldListType(p)) + { + var require = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[0]; + var dt = p.DeclaringType?.Name; + var recursive = v.Name.Equals(dt); + if (recursive) + AddReuseType(p, finalSchema); + else if (IsUserDefined(v)) + { + var schem = GetGenericUserDefinedProperties(v, require, existingTypes); + var row1 = require + ? new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "array"}, {"items", schem}}} + } + : new Dictionary + { + {"name", p.Name}, + { + "type", + new List + { + "null", + new Dictionary {{"type", "array"}, {"items", schem}} + } + } + }; + fieldProperties.Add(row1); + } + else + { + var rw = require + ? new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + : new Dictionary + { + {"name", p.Name}, + { + "type", new List + { + "null", + new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + }; + fieldProperties.Add(rw); + } + } + else + { + fieldProperties.Add(GetFields(p)); + } + } + + var required = property.GetSchemaCustomAttributes().required; + schema["fields"] = fieldProperties; + var row = required + ? new Dictionary { { "name", property.Name }, { "type", schema } } + : SetDefaultValue(property, schema); + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + } + + var field = (List>) finalSchema["fields"]; + field.Add(row); + finalSchema["fields"] = field; + } + //Moved this here so that we can call 'GetSchemaCustomAttributes' only when it is needed + private static Dictionary SetDefaultValue(PropertyInfo p, Dictionary schema) + { + var customAttributes = p.GetSchemaCustomAttributes(); + return customAttributes.hasDefault + ? + new Dictionary + { + {"name", p.Name}, {"type", new List {"null", schema}}, { "default", customAttributes.defaultValue } + } + : + new Dictionary + { + {"name", p.Name}, {"type", new List {"null", schema}} + }; + } + private static Dictionary UserDefinedProperties(PropertyInfo property, List existingTypes) + { + var schema = new Dictionary(); + if (!string.IsNullOrEmpty(property.PropertyType.Namespace)) + { + schema.Add("namespace", property.PropertyType.Namespace); + } + schema["name"] = property.PropertyType.Name; + schema["type"] = "record"; + var properties = property.PropertyType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var fieldProperties = new List>(); + foreach (var p in properties) + { + if (ShouldIgnore(p)) + continue; + if (IsUserDefined(p)) + { + var t = p.PropertyType.GetSubTypeIfNullable().Name; + var dt = p.DeclaringType?.Name; + var recursive = t.Equals(dt); + fieldProperties.Add(recursive || existingTypes.Contains(t) ? Reuse(p) : GetPropertyInfo(p, existingTypes)); + } + else if (IsFieldListType(p)) + { + var require = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[0]; + var dt = p.DeclaringType?.Name; + var isnullable = p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>); + var customAttributes = p.GetSchemaCustomAttributes(); + var recursive = v.Name.Equals(dt); + if (recursive) + { + fieldProperties.Add(ReUseSchema(p.Name, v.Name, customAttributes.required, + customAttributes.hasDefault, customAttributes.defaultValue, isnullable, GetAliases(p))); + } + else + { + Dictionary r; + var ali = GetAliases(v); + if (IsUserDefined(v)) + { + var schem = GetGenericUserDefinedProperties(v, require, existingTypes); + r = require + ? new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "array"}, {"items", schem}}} + } + : new Dictionary + { + {"name", p.Name}, + { + "type", + new List + { + "null", + new Dictionary {{"type", "array"}, {"items", schem}} + } + } + }; + + } + else + { + r = require + ? new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + : new Dictionary + { + {"name", p.Name}, + { + "type", new List + { + "null", + new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType( + p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + }; + + } + + if (ali != null) + { + var rows = r.ToList(); + rows.Insert(1, new KeyValuePair("aliases", ali)); + fieldProperties.Add(rows.ToDictionary(x => x.Key, x => x.Value)); + } + else fieldProperties.Add(r); + } + } + else + { + fieldProperties.Add(GetFields(p)); + } + } + + var required = property.GetSchemaCustomAttributes().required; + schema["fields"] = fieldProperties; + var aliases = GetAliases(property); + var row = required + ? new Dictionary {{"name", property.Name}, {"type", schema}} + : new Dictionary + { + {"name", property.Name}, {"type", new List {"null", schema}} + }; + if (aliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", aliases)); + return rows.ToDictionary(x => x.Key, x => x.Value); + } + + return row; + } + + private static Dictionary GetGenericUserDefinedProperties(Type property, bool required, List existingTypes) + { + var schema = new Dictionary(); + if (!string.IsNullOrEmpty(property.Namespace)) + { + schema.Add("namespace", property.Namespace); + } + schema["name"] = property.Name; + schema["type"] = "record"; + var aliases = GetAliases(property); + if (aliases != null) + schema["aliases"] = aliases; + var properties = property.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var fieldProperties = new List>(); + foreach (var p in properties) + { + if (ShouldIgnore(p)) + continue; + if (IsUserDefined(p)) + { + var t = p.PropertyType.Name; + var dt = p.DeclaringType?.Name; + var recursive = t.Equals(dt); + if (recursive) + fieldProperties.Add(Reuse(p)); + else + fieldProperties.Add(GetPropertyInfo(p, existingTypes)); //treat + } + else if (IsFieldListType(p)) + { + var require = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[0]; + var dt = p.DeclaringType?.Name; + var isnullable = p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>); + var customAttributes = p.GetSchemaCustomAttributes(); + var recursive = v.Name.Equals(dt); + if (recursive) + { + fieldProperties.Add(ReUseSchema(p.Name, v.Name, customAttributes.required, + customAttributes.hasDefault, customAttributes.defaultValue, isnullable, GetAliases(p))); + } + else if (IsUserDefined(v)) + { + var pAliases = GetAliases(p); + var schema2 = GetGenericUserDefinedProperties(v, require, existingTypes); + var row = required + ? new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "array"}, {"items", schema2}}} + } + : new Dictionary + { + {"name", p.Name}, + { + "type", + new List + { + "null", + new Dictionary {{"type", "array"}, {"items", schema2}} + } + } + }; + if (pAliases != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", pAliases)); + fieldProperties.Add(rows.ToDictionary(x => x.Key, x => x.Value)); + } + else fieldProperties.Add(row); + } + else + { + var pAliases = GetAliases(p); + var rw = required + ? new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + : new Dictionary + { + {"name", p.Name}, + { + "type", new List + { + "null", + new Dictionary + { + {"type", "array"}, + { + "items", ToAvroDataType(p.PropertyType.GetGenericArguments()[0].Name, + LogicalKind(p)) + } + } + } + } + }; + if (pAliases != null) + { + var rows = rw.ToList(); + rows.Insert(1, new KeyValuePair("aliases", pAliases)); + fieldProperties.Add(rows.ToDictionary(x => x.Key, x => x.Value)); + } + else fieldProperties.Add(rw); + } + } + else if (IsFieldDictionaryType(p)) + { + var require = p.GetSchemaCustomAttributes().required; + var v = p.PropertyType.GetGenericArguments()[1]; + var dt = p.DeclaringType?.Name; + var isnullable = p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>); + var customAttributes = p.GetSchemaCustomAttributes(); + + var recursive = v.Name.Equals(dt); + if (recursive) + fieldProperties.Add(ReUseSchema(p.Name, v.Name, customAttributes.required, + customAttributes.hasDefault, customAttributes.defaultValue, isnullable, GetAliases(p))); + else + { + var ali = GetAliases(p); + Dictionary row; + if (IsUserDefined(v)) + { + var schemaD = GetGenericUserDefinedProperties(v, require, existingTypes); + row = new Dictionary + { + {"name", p.Name}, + {"type", new Dictionary {{"type", "map"}, {"values", schemaD}}}, + { "default", new Dictionary() } + }; + } + else + { + row = new Dictionary + { + {"name", p.Name}, + { + "type", new Dictionary + { + {"type", "map"}, + { + "values", ToAvroDataType(p.PropertyType.GetGenericArguments()[1].Name, + LogicalKind(p)) + } + } + }, + { "default", new Dictionary() } + }; + } + + if (ali != null) + { + var rows = row.ToList(); + rows.Insert(1, new KeyValuePair("aliases", ali)); + row = rows.ToDictionary(x => x.Key, x => x.Value); + fieldProperties.Add(row); + } + else fieldProperties.Add(row); + } + } + else + { + fieldProperties.Add(GetFields(p)); + } + } + + schema["fields"] = fieldProperties; + return schema; + + } + + private static bool IsFieldListType(PropertyInfo p) + { + if (!p.PropertyType.IsGenericType) + { + return false; + } + + var typeDefinition = p.PropertyType.GetGenericTypeDefinition(); + + return typeDefinition.IsAssignableFrom(typeof(List<>)) || typeDefinition.IsAssignableFrom(typeof(IEnumerable<>)); + } + + private static bool IsList(Type t) + { + if (!t.IsGenericType) + { + return false; + } + + var typeDefinition = t.GetGenericTypeDefinition(); + + return typeDefinition.IsAssignableFrom(typeof(List<>)) || typeDefinition.IsAssignableFrom(typeof(IEnumerable<>)); + } + + private static bool IsFieldDictionaryType(PropertyInfo p) + { + return p.PropertyType.IsGenericType && + p.PropertyType.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>)); + } + + private static bool IsDictionary(Type t) + { + return t.IsGenericType && + t.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>)); + } + + private static Dictionary GetEnumField(PropertyInfo p, ICollection existingTypes) + { + var propertyTypeName = p.PropertyType.Name; + if (existingTypes.Contains(propertyTypeName)) + { + List lt = null; + if (p.PropertyType.IsNullable()) + { + lt = new List() {"null", propertyTypeName}; + } + + return new Dictionary + { + {"name", p.Name}, {"type", lt == null ? propertyTypeName : (object) lt} + }; + } + + existingTypes.Add(propertyTypeName); + + var pt = p.PropertyType.GetSubTypeIfNullable(); + var dp = new Dictionary(); + if (!string.IsNullOrEmpty(pt.Namespace)) { + dp.Add("namespace", pt.Namespace); + } + dp["type"] = "enum"; + dp["name"] = pt.Name; + dp["symbols"] = GetEnumValues(pt); + List lp = null; + if (p.PropertyType.IsNullable()) + { + lp = new List() {"null", dp}; + } + + var row = new Dictionary + { + {"name", p.Name}, {"type", lp == null ? dp : (object) lp} + }; + + return row; + } + + private static object ToAvroDataType(string type, LogicalTypeKind? kind) + { + switch (type) + { + case "Int32": + return "int"; + case "Int64": + return "long"; + case "String": + return "string"; + case "Double": + return "double"; + case "Single": + return "float"; + case "Boolean": + return "boolean"; + case "Byte[]": + case "SByte[]": + return "bytes"; + case "DateTime": + { + if(kind == null) + { + return new Dictionary + { + {"type", "long"}, {"logicalType", "timestamp-millis"} + }; + } + switch (kind) + { + case LogicalTypeKind.Date: + return new Dictionary { { "type", "int" }, { "logicalType", "date" } }; + case LogicalTypeKind.TimestampMillisecond: + return new Dictionary + { + {"type", "long"}, {"logicalType", "timestamp-millis"} + }; + case LogicalTypeKind.TimestampMicrosecond: + return new Dictionary + { + {"type", "long"}, {"logicalType", "timestamp-micros"} + }; + default: + throw new Exception($"[DateTime] Unknown LogicalTypeKind:{kind}. Try TimeSpan instead!"); + } + } + case "TimeSpan": + { + if (kind == null) + { + return new Dictionary + { + {"type", "long"}, {"logicalType", "timestamp-millis"} + }; + } + switch (kind) + { + case LogicalTypeKind.TimeMillisecond: + return new Dictionary { { "type", "int" }, { "logicalType", "time-millis" } }; + case LogicalTypeKind.TimeMicrosecond: + return new Dictionary { { "type", "long" }, { "logicalType", "time-micros" } }; + default: + throw new Exception($"[TimeSpan] Unknown LogicalTypeKind:{kind}. Try DateTime instead!"); + } + } + + case "Decimal": + case "BigInteger": + case "AvroDecimal": + return new Dictionary + { + {"type", "bytes"}, {"logicalType", "decimal"}, {"precision", 4}, {"scale", 2} + }; + case "Guid": + return new Dictionary + { + {"type", "string"}, {"logicalType", "uuid"} + }; + + default: + throw new ArgumentException($"{type} not supported"); + } + } + } +} \ No newline at end of file diff --git a/SchemaGenerator/avro-schema.png b/SchemaGenerator/avro-schema.png new file mode 100644 index 0000000..cfc9220 Binary files /dev/null and b/SchemaGenerator/avro-schema.png differ diff --git a/build.cmd b/build.cmd new file mode 100755 index 0000000..b08cc59 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..c0c0e61 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_MULTILEVEL_LOOKUP = 0 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..d4a7e51 --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_MULTILEVEL_LOOKUP=0 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 0000000..31e43dc --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs new file mode 100644 index 0000000..0435878 --- /dev/null +++ b/build/Build.CI.GitHubActions.cs @@ -0,0 +1,106 @@ +// Copyright 2021 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Collections.Generic; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.CI.GitHubActions.Configuration; +using Nuke.Common.Execution; +using Nuke.Common.Utilities; + +[CustomGitHubActions("Build", + GitHubActionsImage.WindowsLatest, + GitHubActionsImage.UbuntuLatest, + AutoGenerate = true, + FetchDepth = 0, + OnPushBranches = new[] { "main", "dev", "alpha", "beta" }, + OnPullRequestBranches = new[] { "main", "dev", "alpha", "beta" }, + CacheKeyFiles = new[] { "global.json", "SchemaGenerator/**/*.csproj" }, + InvokedTargets = new[] { nameof(Compile) }, + OnPushExcludePaths = new[] { "docs/**/*", "package.json", "README.md" }, + PublishArtifacts = false, + EnableGitHubToken = true) +] + +[CustomGitHubActions("Tests", + GitHubActionsImage.WindowsLatest, + GitHubActionsImage.UbuntuLatest, + AutoGenerate = true, + FetchDepth = 0, + OnPushBranches = new[] { "main", "dev" }, + OnPullRequestBranches = new[] { "main", "dev" }, + CacheKeyFiles = new[] { "global.json", "SchemaGenerator/**/*.csproj" }, + InvokedTargets = new[] { nameof(Test) }, + EnableGitHubToken = true) +] + +[CustomGitHubActions("Release", + GitHubActionsImage.WindowsLatest, + AutoGenerate = true, + FetchDepth = 0, + OnPushTags = new[] { "*" }, + CacheKeyFiles = new[] { "global.json", "SchemaGenerator/**/*.csproj" }, + InvokedTargets = new[] { nameof(Release) }, + PublishArtifacts = true, + EnableGitHubToken = true, + ImportSecrets = new[] { "NUGET_API_KEY" })] +partial class Build +{ +} +class CustomGitHubActionsAttribute : GitHubActionsAttribute +{ + public CustomGitHubActionsAttribute(string name, GitHubActionsImage image, params GitHubActionsImage[] images) : base(name, image, images) + { + } + + protected override GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCollection relevantTargets) + { + var job = base.GetJobs(image, relevantTargets); + + try + { + var newSteps = new List(job.Steps); + foreach (var version in new[] { "7.0.*", "8.0.*" }) + { + newSteps.Insert(1, new GitHubActionsSetupDotNetStep + { + Version = version + }); + } + + job.Steps = newSteps.ToArray(); + } + catch (Exception ex) + { + + } + return job; + } +} + +class GitHubActionsSetupDotNetStep : GitHubActionsStep +{ + public string Version { get; init; } + + public override void Write(CustomFileWriter writer) + { + try + { + writer.WriteLine("- uses: actions/setup-dotnet@v1"); + + using (writer.Indent()) + { + writer.WriteLine("with:"); + using (writer.Indent()) + { + writer.WriteLine($"dotnet-version: {Version}"); + } + } + } + catch (Exception ex) + { + + } + } +} \ No newline at end of file diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..698d85c --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,266 @@ +using System; +using System.IO; +using System.Linq; +using Nuke.Common; +using Nuke.Common.CI; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.Git; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using static Nuke.Common.ChangeLog.ChangelogTasks; +using static Nuke.Common.Tools.Git.GitTasks; +using Nuke.Common.ChangeLog; +using System.Collections.Generic; +using Octokit; +using JetBrains.Annotations; + +[DotNetVerbosityMapping] +[ShutdownDotNetAfterServerBuild] +partial class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main() => Execute(x => x.Test); + + [CI] readonly GitHubActions GitHubActions; + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Solution] readonly Solution Solution; + [GitRepository] readonly GitRepository GitRepository; + [Required] [GitVersion(Framework = "net6.0")] readonly GitVersion GitVersion; + + [Parameter] string NugetApiUrl = "https://api.nuget.org/v3/index.json"; + + [Parameter] [Secret] string NuGetApiKey; + + AbsolutePath OutputTests => RootDirectory / "TestResults"; + + AbsolutePath OutputPerfTests => RootDirectory / "PerfResults"; + AbsolutePath DocSiteDirectory => RootDirectory / "docs/_site"; + public string ChangelogFile => RootDirectory / "CHANGELOG.md"; + public AbsolutePath DocFxDir => RootDirectory / "docs"; + public string DocFxDirJson => DocFxDir / "docfx.json"; + AbsolutePath OutputNuget => Output / "nuget"; + AbsolutePath Output => RootDirectory / "output"; + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + AbsolutePath TestSourceDirectory => RootDirectory / "AvroSchemaGenerator.Tests"; + GitHubClient GitHubClient; + public ChangeLog Changelog => ReadChangelog(ChangelogFile); + + public ReleaseNotes LatestVersion => Changelog.ReleaseNotes.OrderByDescending(s => s.Version).FirstOrDefault() ?? throw new ArgumentException("Bad Changelog File. Version Should Exist"); + public string ReleaseVersion => LatestVersion.Version?.ToString() ?? throw new ArgumentException("Bad Changelog File. Define at least one version"); + string TagVersion => GitVersion.MajorMinorPatch; + + bool IsTaggedBuild => !string.IsNullOrWhiteSpace(TagVersion); + + string VersionSuffix; + protected override void OnBuildInitialized() + { + VersionSuffix = !IsTaggedBuild + ? $"dev-{DateTime.UtcNow:yyyyMMdd-HHmm}" + : ""; + + if (IsLocalBuild) + { + VersionSuffix = $"dev-{DateTime.UtcNow:yyyyMMdd-HHmm}"; + } + + Information("BUILD SETUP"); + Information($"Configuration:\t{Configuration}"); + Information($"Version suffix:\t{VersionSuffix}"); + Information($"Tagged build:\t{IsTaggedBuild}"); + } + Target Clean => _ => _ + .Before(Restore) + .Executes(() => + { + RootDirectory + .GlobDirectories("**/bin", "**/obj", Output, OutputTests, OutputPerfTests, OutputNuget, DocSiteDirectory) + .ForEach(AbsolutePathExtensions.DeleteDirectory); + AbsolutePathExtensions.CreateOrCleanDirectory(ArtifactsDirectory); + }); + IEnumerable ChangelogSectionNotes => ExtractChangelogSectionNotes(ChangelogFile); + + Target RunChangelog => _ => _ + .Unlisted() + //.OnlyWhenDynamic(() => GitVersion.BranchName == "main") + //.OnlyWhenStatic(() => InvokedTargets.Contains(nameof(RunChangelog))) + .Executes(() => + { + FinalizeChangelog(ChangelogFile, TagVersion, GitRepository); + Git($"add {ChangelogFile}"); + //Git($"commit -m \"Finalize {Path.GetFileName(ChangelogFile)} for {GitVersion.SemVer}.\""); + //Git($"tag -f {GitVersion.SemVer}"); + //git push --atomic origin + //https://gist.github.com/danielestevez/2044589 + //https://www.atlassian.com/git/tutorials/merging-vs-rebasing + Information("Please review CHANGELOG.md and press any key to continue ..."); + }); + + Target Restore => _ => _ + .Executes(() => + { + try + { + DotNetRestore(s => s + .SetProjectFile(Solution)); + } + catch (Exception ex) + { + Information(ex.ToString()); + } + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => + { + var projectName = "AvroSchemaGenerator.Tests"; + var project = Solution.GetProject(projectName).NotNull("project != null"); + Information($"Running tests from {projectName}"); + foreach (var fw in project.GetTargetFrameworks()) + { + Information($"Running tests from {projectName} framework '{fw}'"); + DotNetTest(c => c + .SetProjectFile(project) + .SetConfiguration(Configuration.ToString()) + .SetFramework(fw) + .SetVerbosity(verbosity: DotNetVerbosity.Normal) + .EnableNoBuild()); + } + }); + + Target Pack => _ => _ + .DependsOn(Compile) + //.DependsOn(RunChangelog)// requires authrntication in github action + .Executes(() => + { + var branchName = GitRepository.Branch; + var releaseNotes = GetNuGetReleaseNotes(ChangelogFile, GitRepository); + + var project = Solution.GetProject("AvroSchemaGenerator"); + DotNetPack(s => s + .SetProject(project) + .SetConfiguration(Configuration) + //.EnableNoBuild() + .SetVersion(TagVersion) + .EnableNoRestore() + .SetAssemblyVersion(TagVersion) + .SetFileVersion(TagVersion) + .SetInformationalVersion(TagVersion) + //.SetVersionSuffix(VersionSuffix) + .SetPackageReleaseNotes(releaseNotes) + .SetDescription("Generate Avro Schema with support for RECURSIVE SCHEMA") + .SetPackageTags("Avro", "Schema Generator") + .AddAuthors("Ebere Abanonu (@mestical)") + .SetPackageProjectUrl("https://github.com/eaba/AvroSchemaGenerator") + .SetOutputDirectory(OutputNuget)); + + }); + + [UsedImplicitly] + Target Release => _ => _ + //.DependsOn(RunChangelog) + .DependsOn(Pack) + .Requires(() => NugetApiUrl) + .Requires(() => !NuGetApiKey.IsNullOrEmpty()) + .Requires(() => Configuration.Equals(Configuration.Release)) + .Triggers(GitHubRelease) + .Executes(() => + { + OutputNuget.GlobFiles("*.nupkg", "*.symbols.nupkg") + .NotNull() + .ForEach(x => + { + Assert.NotNullOrEmpty(x); + DotNetNuGetPush(s => s + .SetTargetPath(x) + .SetSource(NugetApiUrl) + .SetApiKey(NuGetApiKey) + ); + }); + }); + + Target AuthenticatedGitHubClient => _ => _ + .Unlisted() + .OnlyWhenDynamic(() => !string.IsNullOrWhiteSpace(GitHubActions.Token)) + .Executes(() => + { + GitHubClient = new GitHubClient(new ProductHeaderValue("nuke-build")) + { + Credentials = new Credentials(GitHubActions.Token, AuthenticationType.Bearer) + }; + }); + Target GitHubRelease => _ => _ + .Unlisted() + .Description("Creates a GitHub release (or amends existing) and uploads the artifact") + .OnlyWhenDynamic(() => !string.IsNullOrWhiteSpace(GitHubActions.Token)) + .DependsOn(AuthenticatedGitHubClient) + .Executes(async () => + { + var version = TagVersion; + var releaseNotes = GetNuGetReleaseNotes(ChangelogFile); + Release release; + + + var identifier = GitRepository.Identifier.Split("/"); + var (gitHubOwner, repoName) = (identifier[0], identifier[1]); + try + { + release = await GitHubClient.Repository.Release.Get(gitHubOwner, repoName, version); + } + catch (NotFoundException) + { + var newRelease = new NewRelease(version) + { + Body = releaseNotes, + Name = version, + Draft = false, + Prerelease = GitRepository.IsOnReleaseBranch() + }; + release = await GitHubClient.Repository.Release.Create(gitHubOwner, repoName, newRelease); + } + + foreach (var existingAsset in release.Assets) + { + await GitHubClient.Repository.Release.DeleteAsset(gitHubOwner, repoName, existingAsset.Id); + } + + Information($"GitHub Release {version}"); + var packages = OutputNuget.GlobFiles("*.nupkg", "*.symbols.nupkg").NotNull(); + foreach (var artifact in packages) + { + var releaseAssetUpload = new ReleaseAssetUpload(artifact.Name, "application/zip", File.OpenRead(artifact), null); + var releaseAsset = await GitHubClient.Repository.Release.UploadAsset(release, releaseAssetUpload); + Information($" {releaseAsset.BrowserDownloadUrl}"); + } + }); + string ParseReleaseNote() + { + return XmlTasks.XmlPeek(RootDirectory / "Directory.Build.props", "//Project/PropertyGroup/PackageReleaseNotes").FirstOrDefault(); + } + static void Information(string info) + { + Serilog.Log.Information(info); + } +} diff --git a/build/Configuration.cs b/build/Configuration.cs new file mode 100644 index 0000000..9b22a3b --- /dev/null +++ b/build/Configuration.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 0000000..55e84bb --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + + CS0649;CS0169 + .. + .. + 1 + + + + + + + + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings new file mode 100644 index 0000000..7bc2848 --- /dev/null +++ b/build/_build.csproj.DotSettings @@ -0,0 +1,27 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True diff --git a/global.json b/global.json new file mode 100644 index 0000000..c19a2e0 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestMinor" + } +}