Skip to content

Commit

Permalink
Contract dynamic swagger endpoint (#527)
Browse files Browse the repository at this point in the history
  • Loading branch information
rowandh authored and fassadlr committed Jul 22, 2021
1 parent fc1d805 commit a414f48
Show file tree
Hide file tree
Showing 15 changed files with 1,024 additions and 25 deletions.
14 changes: 13 additions & 1 deletion src/Stratis.Bitcoin.Features.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
Expand All @@ -28,6 +29,7 @@ public Startup(IWebHostEnvironment env, IFullNode fullNode)
}

private IFullNode fullNode;
private SwaggerUIOptions uiOptions;

public IConfigurationRoot Configuration { get; }

Expand Down Expand Up @@ -109,8 +111,15 @@ public void ConfigureServices(IServiceCollection services)
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

// Register the Swagger generator. This will use the options we injected just above.
services.AddSwaggerGen();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("contracts", new OpenApiInfo { Title = "Contract API", Version = "1" });
});
services.AddSwaggerGenNewtonsoftSupport(); // Use Newtonsoft JSON serializer with swagger. Needs to be placed after AddSwaggerGen()

// Hack to be able to access and modify the options object
services.AddSingleton(_ => this.uiOptions);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down Expand Up @@ -141,6 +150,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
// Hack to be able to access and modify the options object configured here
this.uiOptions = c;
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OpenApi.Models;
using Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor;
using Stratis.SmartContracts;
using Stratis.SmartContracts.CLR.Compilation;
using Stratis.SmartContracts.CLR.Loader;
using Xunit;

namespace Stratis.Bitcoin.Features.SmartContracts.Tests
{
public class ContractSchemaFactoryTests
{
private const string Code = @"
using Stratis.SmartContracts;
[Deploy]
public class PrimitiveParams : SmartContract
{
public PrimitiveParams(ISmartContractState state): base(state) {}
public void AcceptsBool(bool b) {}
public void AcceptsByte(byte bb) {}
public void AcceptsByteArray(byte[] ba) {}
public void AcceptsChar(char c) {}
public void AcceptsString(string s) {}
public void AcceptsUint(uint ui) {}
public void AcceptsUlong(ulong ul) {}
public void AcceptsInt(int i) {}
public void AcceptsLong(long l) {}
public void AcceptsAddress(Address a) {}
public bool SomeProperty {get; set;}
public void AcceptsAllParams(bool b, byte bb, byte[] ba, char c, string s, uint ui, ulong ul, int i, long l, Address a) {}
}
public class DontDeploy : SmartContract
{
public DontDeploy(ISmartContractState state): base(state) {}
public void SomeMethod(string i) {}
}
";
[Fact]
public void Map_Parameter_Type_Success()
{
var compilationResult = ContractCompiler.Compile(Code);

var assembly = Assembly.Load(compilationResult.Compilation);

var mapper = new ContractSchemaFactory();

MethodInfo methodInfo = assembly.ExportedTypes.First(t => t.Name == "PrimitiveParams").GetMethod("AcceptsAllParams");
var schema = mapper.Map(methodInfo);
var properties = schema.Properties;

Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(bool)]().Type, properties["b"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(byte)]().Type, properties["bb"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(byte[])]().Type, properties["ba"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(char)]().Type, properties["c"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(string)]().Type, properties["s"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(uint)]().Type, properties["ui"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(ulong)]().Type, properties["ul"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(int)]().Type, properties["i"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(long)]().Type, properties["l"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(string)]().Type, properties["a"].Type);
}

[Fact]
public void Map_Type_Success()
{
var compilationResult = ContractCompiler.Compile(Code);

var assembly = Assembly.Load(compilationResult.Compilation);

var mapper = new ContractSchemaFactory();

// Maps the methods in a type to schemas.
IDictionary<string, OpenApiSchema> mapped = mapper.Map(new ContractAssembly(assembly).GetPublicMethods());

Assert.Equal("AcceptsBool", mapped["AcceptsBool"].Title);
Assert.Equal("AcceptsByte", mapped["AcceptsByte"].Title);
Assert.Equal("AcceptsByteArray", mapped["AcceptsByteArray"].Title);
Assert.Equal("AcceptsChar", mapped["AcceptsChar"].Title);
Assert.Equal("AcceptsString", mapped["AcceptsString"].Title);
Assert.Equal("AcceptsUint", mapped["AcceptsUint"].Title);
Assert.Equal("AcceptsUlong", mapped["AcceptsUlong"].Title);
Assert.Equal("AcceptsInt", mapped["AcceptsInt"].Title);
Assert.Equal("AcceptsLong", mapped["AcceptsLong"].Title);
Assert.Equal("AcceptsAddress", mapped["AcceptsAddress"].Title);

Assert.Equal(11, mapped.Count);
}

[Fact]
public void Only_Map_Deployed_Type_Success()
{
var compilationResult = ContractCompiler.Compile(Code);

var assembly = Assembly.Load(compilationResult.Compilation);

var mapper = new ContractSchemaFactory();

IDictionary<string, OpenApiSchema> mapped = mapper.Map(new ContractAssembly(assembly));

Assert.Equal(11, mapped.Count);
Assert.False(mapped.ContainsKey("SomeMethod"));
}

[Fact]
public void Only_Map_Deployed_Type_Single_Contract_Success()
{
string code = @"
using Stratis.SmartContracts;
public class PrimitiveParams : SmartContract
{
public PrimitiveParams(ISmartContractState state): base(state) {}
public void SomeMethod(string i) {}
}
";
var compilationResult = ContractCompiler.Compile(code);

var assembly = Assembly.Load(compilationResult.Compilation);

var contractAssembly = new ContractAssembly(assembly);

var mapper = new ContractSchemaFactory();

IDictionary<string, OpenApiSchema> mapped = mapper.Map(contractAssembly);

Assert.Equal(1, mapped.Count);
Assert.True(mapped.ContainsKey("SomeMethod"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor;
using Stratis.SmartContracts.CLR.Compilation;
using Stratis.SmartContracts.CLR.Serialization;
using Xunit;

namespace Stratis.Bitcoin.Features.SmartContracts.Tests
{
public class ParameterInfoMapperExtensionsTests
{
[Fact]
public void Map_Method_Params_Success()
{
var code = @"
using Stratis.SmartContracts;
public class Test
{
public void AcceptsAllParams(bool b, byte bb, byte[] ba, char c, string s, uint ui, ulong ul, int i, long l, Address a) {}
}
";
var compiled = ContractCompiler.Compile(code).Compilation;
var assembly = Assembly.Load(compiled);
var method = assembly.ExportedTypes.First().GetMethod("AcceptsAllParams");

// The jObject as we expect it to come from swagger.
var jObject = JObject.FromObject(new
{
b = "true",
bb = "DD",
ba = "AABB",
c = 'a',
s = "Test",
ui = 12,
ul = 123123128823,
i = 257,
l = 1238457438573495346,
a = "address"
});

var mapped = method.GetParameters().Map(jObject);

// Check the order and type of each param is correct.
Assert.Equal(10, mapped.Length);
Assert.Equal($"{(int)MethodParameterDataType.Bool}#true", mapped[0]);
Assert.Equal($"{(int)MethodParameterDataType.Byte}#DD", mapped[1]);
Assert.Equal($"{(int)MethodParameterDataType.ByteArray}#AABB", mapped[2]);
Assert.Equal($"{(int)MethodParameterDataType.Char}#a", mapped[3]);
Assert.Equal($"{(int)MethodParameterDataType.String}#Test", mapped[4]);
Assert.Equal($"{(int)MethodParameterDataType.UInt}#12", mapped[5]);
Assert.Equal($"{(int)MethodParameterDataType.ULong}#123123128823", mapped[6]);
Assert.Equal($"{(int)MethodParameterDataType.Int}#257", mapped[7]);
Assert.Equal($"{(int)MethodParameterDataType.Long}#1238457438573495346", mapped[8]);
Assert.Equal($"{(int)MethodParameterDataType.Address}#address", mapped[9]);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Stratis.SmartContracts.CLR.Loader;

namespace Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor
{
public static class ContractAssemblyExtensions
{
/// <summary>
/// Gets the public methods defined by the contract, ignoring property getters/setters.
/// </summary>
/// <returns></returns>
public static IEnumerable<MethodInfo> GetPublicMethods(this IContractAssembly contractAssembly)
{
Type deployedType = contractAssembly.DeployedType;

if (deployedType == null)
return new List<MethodInfo>();

return deployedType
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) // Get only the methods declared on the contract type
.Where(m => !m.IsSpecialName); // Ignore property setters/getters
}

public static IEnumerable<PropertyInfo> GetPublicGetterProperties(this IContractAssembly contractAssembly)
{
Type deployedType = contractAssembly.DeployedType;

if (deployedType == null)
return new List<PropertyInfo>();

return deployedType
.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetGetMethod() != null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OpenApi.Models;
using Stratis.SmartContracts;
using Stratis.SmartContracts.CLR.Loader;

namespace Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor
{
/// <summary>
/// Factory for generating swagger schema for smart contract primitives.
/// </summary>
public class ContractSchemaFactory
{
public static readonly Dictionary<Type, Func<OpenApiSchema>> PrimitiveTypeMap = new Dictionary<Type, Func<OpenApiSchema>>
{
{ typeof(short), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(ushort), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(int), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(uint), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(long), () => new OpenApiSchema { Type = "integer", Format = "int64" } },
{ typeof(ulong), () => new OpenApiSchema { Type = "integer", Format = "int64" } },
{ typeof(float), () => new OpenApiSchema { Type = "number", Format = "float" } },
{ typeof(double), () => new OpenApiSchema { Type = "number", Format = "double" } },
{ typeof(decimal), () => new OpenApiSchema { Type = "number", Format = "double" } },
{ typeof(byte), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(sbyte), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(byte[]), () => new OpenApiSchema { Type = "string", Format = "byte" } },
{ typeof(sbyte[]), () => new OpenApiSchema { Type = "string", Format = "byte" } },
{ typeof(char), () => new OpenApiSchema { Type = "string", Format = "char" } },
{ typeof(string), () => new OpenApiSchema { Type = "string" } },
{ typeof(bool), () => new OpenApiSchema { Type = "boolean" } },
{ typeof(Address), () => new OpenApiSchema { Type = "string" } }
};

/// <summary>
/// Maps a contract assembly to its schemas.
/// </summary>
/// <param name="assembly"></param>
/// <returns></returns>
public IDictionary<string, OpenApiSchema> Map(IContractAssembly assembly)
{
return this.Map(assembly.GetPublicMethods());
}

/// <summary>
/// Maps a type to its schemas.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public IDictionary<string, OpenApiSchema> Map(IEnumerable<MethodInfo> methods)
{
return methods.Select(this.Map).ToDictionary(k => k.Title, v => v);
}

/// <summary>
/// Maps a single method to a schema.
/// </summary>
/// <param name="method"></param>
/// <returns></returns>
public OpenApiSchema Map(MethodInfo method)
{
var schema = new OpenApiSchema();
schema.Properties = new Dictionary<string, OpenApiSchema>();
schema.Title = method.Name;

foreach (var parameter in method.GetParameters())
{
// Default to string.
OpenApiSchema paramSchema = PrimitiveTypeMap.ContainsKey(parameter.ParameterType)
? PrimitiveTypeMap[parameter.ParameterType]()
: PrimitiveTypeMap[typeof(string)]();

schema.Properties.Add(parameter.Name, paramSchema);
}

return schema;
}
}
}

0 comments on commit a414f48

Please sign in to comment.