Compare commits

...

86 Commits

Author SHA1 Message Date
3da97d4fff Upgraded project to DotNet 8.0 and upgraded nuget packages to the latest version. Bumped package version to 1.8.9 2025-06-17 09:53:06 -07:00
d0d64ef570 Modified the RestClientGenerator to prevent infinite dependency tracing. This method is a little cleaner too. Bumped package version to 1.8.8 2025-06-02 11:31:15 -07:00
f116be9908 Added readme to nuget package. 2024-09-11 17:52:08 -07:00
2c2b498223 Added mimeType option to the WithStream extension. Bumped package version to 1.8.7 2024-08-29 10:18:36 -07:00
bc82aeb8c2 Added new WithStream extension for responses. Bumped package version to 1.8.6 2024-08-29 10:12:44 -07:00
670605ce91 Bumped package version to 1.8.5. Added enum route argument converting support. Cleaned up code and added unit test. 2024-07-10 06:37:02 -07:00
a698e71e4b Fixed a minor issue where the request parameter was the same name as the request object in javascript route functions. Bumped package version to 1.8.4 2024-03-06 08:14:40 -08:00
51b8ba073c Bumped package version to 1.8.3. Added code to escape the names of fields, properties when generating the constructor. 2024-02-23 16:55:40 -08:00
b8e8e1dd86 Bumped package version to 1.8.2. Added the ability to use json names in the Javascript Client generator if a JsonProperty exists on a field or a property. 2024-02-23 16:23:54 -08:00
cc83f99612 Bumped package version to 1.8.1. Javascript client now includes a urlHandler and requestHandler that can be used to modify the request before it's sent. 2024-02-19 08:42:58 -08:00
8747b5fb3e Modifying client generator to allow modifying requests before they are sent. 2024-02-19 08:05:48 -08:00
50861d5381 Bumped package version to 1.8.0. Added support for automatic gzip decompression in HttpListenerRequestExtensions. Added a new compress parameter for json requests in client generated code that auto compresses the json if set. Fixed a few bugs and cleaned up code. 2024-01-13 10:53:44 -08:00
5f83b30cb2 Added ability to specify a RouteResponse and RouteRequest as a dynamic. Improved documentation and fixed some null checks that were missing with this change. Bumped package version to 1.7.6 2023-12-14 11:33:42 -08:00
38ef135b8a Fixed a stackoverflow bug when generating a Rest Client and the object has it's type as a sub type. Bumped package versino to 1.7.5 2023-12-14 11:14:27 -08:00
9633e211a1 Fixed an accidental bug introduced to the client generation. Bumped package version to 1.7.4 2023-09-24 19:49:25 -07:00
b9260dbdb1 Fixed a bug where a double empty constructor could be generated for a C# client if there is no fields/properties. Bumped package version to 1.7.3 2023-09-24 19:34:26 -07:00
4c64f1c134 Modified Javascript Client generator to throw the response instead of a message so that behavior of the code using the Client can changed based on the response returned. Bumped package version to 1.7.2 2023-07-28 15:23:29 -07:00
2f71c18b65 Fixed an issue where if a route didn't end with a /, the generated code would still contain an ending slash. Bumped package version to 1.7.1 2023-07-11 09:52:15 -07:00
69a1d9c3a8 Improved RouteMatcher and added more unit tests to cover some edge cases. Cleaned up code. Bumped package version to 1.7.0 2023-07-09 11:14:33 -07:00
8c44d56ab4 Added utf16 response extensions to help support some legacy code. Changed HttpListener to protected to help with overriding the default behavior of the RouteListener if needed. Bumped package version to 1.6.9 2023-07-05 08:04:56 -07:00
4b05b1b6b9 Fixed an absolutely stupid bug. Bumped package version to 1.6.8 2023-06-29 10:00:59 -07:00
7934f807ef Improved file serving to support startPaths. Improved code and simplified a few things. Bumped package version to 1.6.7 2023-06-29 09:50:12 -07:00
36872164c5 Improved SinglePage algorithm. Bumped package version to 1.6.6 2023-06-29 09:29:21 -07:00
dc1abd516b Added missing code to the ServeSinglePage. Bumped package version to 1.6.5 2023-06-29 08:37:45 -07:00
4617f861fc Added support for cases where the parent directory is included in the request path. Added unit tests to check this case. Bumped package version to 1.6.4 2023-06-29 08:21:54 -07:00
46aab308fa Merge pull request 'ServeFileExtension' (#2) from ServeFileExtension into master
Reviewed-on: #2
2023-06-29 01:39:07 +00:00
c2f60ff19f Bumped package version to 1.6.3. Added unit tests for serving files. Fixed issues and improved the security of the serve file extensions and the ability to compress files. 2023-06-28 18:37:46 -07:00
d96c44e542 Working on code and tests for a serve file extension to make developing a small web server easier. 2023-06-28 17:41:31 -07:00
8467251a17 Working on ServeMultiPage and ServeSinglePage to help make it easier to serve a website using this library. 2023-06-28 16:35:34 -07:00
ac2f63229c Fixed a minor bug with the GetValue Javascript generated function. Bumped package version to 1.6.2 2023-06-16 10:28:31 -07:00
9367ba0ff5 Bumped package version to 1.6.1 and added helper GetValue function to Javascript and improved the documentation. 2023-06-16 10:17:21 -07:00
52a6db9e1a Added ReadAsForm to the RequestExtensions to help with reading form data. Bumped package verison to 1.6.0 2023-06-09 14:53:41 -07:00
efd744974d Bumped package version to 1.5.9, exposed a parameter in the init method to allow a custom message handler to be used with the client for C#. 2023-06-01 06:29:39 -07:00
22ff3c1312 Fixed a bug with boolean default values. Bumped package version to 1.5.8. 2023-05-29 07:08:30 -07:00
a630f0e334 Fixed a csharp client generator bug. The csharp client generator now outputs a constructor for types that allows setting property/fields. Bumped package version to 1.5.7 2023-05-29 06:47:52 -07:00
755a2399e4 Bumped package version to 1.5.6 added a helper function to get the name from a enum value. 2023-05-22 08:59:56 -07:00
5e814e81c6 Bumped package version to 1.5.5, added code to the CSharp client generator to generate a empty constructor and a constructor that can take an existing instance of a dto and automatically copy all the values. 2023-05-19 07:25:55 -07:00
5048640a53 Bumped package version to 1.5.4, changed @method to use @function in client generator for javascript. Added blob support for requests. 2023-04-20 16:31:41 -07:00
16c1bb878c Added GetNamesValues function to javascript client generator output. Bumped package version to 1.5.3. 2023-04-20 08:50:28 -07:00
54e1fbefec Removed enum extends since it's not super helpful. Bumped package version to 1.5.2. 2023-04-14 06:44:30 -07:00
bbd38496da Bumped package version to 1.5.1, added GetNames/GetValues helper functions to Javascript Enum Type class. 2023-04-14 06:38:01 -07:00
fe5fc73d14 Bumped package version to 1.5.0, changed Program.cs a little of the example. Improved the Javascript Client generator by exporting all types and changing enum to behave in a more useful way. 2023-04-13 15:00:24 -07:00
e2c5aba868 Added Javascript client generator. Fixed a few bugs. Improved client generator code. 2023-04-12 18:03:55 -07:00
6a7f6ce096 Modified ClientGenerator to support responses as a parameter input. Improved streams support. Bumped package version to 1.4.8 2023-03-31 09:19:22 -07:00
b9e1f2ca7d Modified GetMimeType to return binary type if there is no extension. 2023-03-31 08:44:45 -07:00
cf477522c0 Bumped package version to 1.4.7. Added support for Stream requests and MemoryStream responses. 2023-03-31 08:21:32 -07:00
fe99ba9b9d Added RouteTypeName attribute that can be used to rename a type when generating client code. 2023-03-31 07:02:44 -07:00
4bc388d86b Bumped package version to 1.4.5 and modified ClientGenerator to include System.Net assembly as a DotNet type. 2023-03-30 10:36:51 -07:00
71941f5dd0 Changed RouteListener to attempt to listen on all interfaces, if denied it falls back to local. Bumped package version to 1.4.4 2023-03-28 06:20:20 -07:00
0c8467f942 Fixed an issue where built int Enums would be converted to int type for fields. Bumped package version to 1.4.3. 2023-03-24 08:21:12 -07:00
f2db3bddbf Changed logo to new design. 2023-03-24 07:38:53 -07:00
ff7c356655 Improved route function generator to handle status codes better and check to see if json content was null or empty and return default. Bumped package version to 1.4.2 2023-03-24 07:33:35 -07:00
1345e326a8 Added an attribute that can be used to included extra types in the client generation. Bumped package version to 1.4.1 2023-03-24 07:15:42 -07:00
e8735d8764 Added support for sub types in the C# client code generator. Bumped package version to 1.4.0 2023-03-24 06:52:00 -07:00
fafdb48d51 Bumped package version to 1.3.9 added support for Enum code generation and support for inherited types. 2023-03-23 08:23:08 -07:00
7fd42ae81b Added Logo. 2023-03-15 10:29:07 -07:00
97fecdbe6f Changed all response data extensions to no longer use chunking since we can specify the content length. Bumped package version to 1.3.8 2023-03-03 16:12:13 -08:00
69e71bff0b Cleaned up documentation. Response extension now sets the content length for a few extensions. Added WithNoBody response extension. Bumped package version to 1.3.7 2023-03-03 15:49:39 -08:00
c0f029ce08 Bumped package version to 1.3.6. Added RouteFileCache.Cached function that doesn't return content. 2023-03-03 13:02:11 -08:00
f4fe34e461 Added WithPreCompressedFile extension. Bumped package version to 1.3.5 2023-03-01 17:14:52 -08:00
bd4f5f63b6 Renamed fileName back to filePath 2023-03-01 16:33:41 -08:00
2b892dfd66 Added a new RouteFileCache and more response extensions. Bumped package version to 1.3.4. 2023-03-01 16:19:20 -08:00
662bd03ddc Changed Listener.Block to not use as much CPU usage, at least not visually in the task manager. 2023-03-01 12:22:23 -08:00
f882a74c6d Added compressed response extensions and added more to the example program. Bumped package version to 1.3.3 2023-02-24 12:05:57 -08:00
22633ec94f Added missing json mime type. 2023-02-10 12:12:37 -08:00
f8b726634e Changed mime type for javascript. Bumped package version to 1.3.2. 2023-02-10 12:11:44 -08:00
55d75a7bb0 Improved WithFile extension to allow custom MimeTypes and added an auto mime type detection. Bumped package to 1.3.1 2023-02-09 12:53:03 -08:00
0ee5e98768 Added a missing unit test and fixed another bug with the RouteMatcher. Bumped package version to 1.3.0 2023-02-08 15:23:43 -08:00
85889973c8 Fixed some bugs within the RouteMatcher to better support wild card and catch all matching. Added unit tests. Bumped package version to 1.2.9 2023-02-08 15:14:17 -08:00
12fe2da2a3 Fixed issue where client generator was actually removing routes from the original list. Improved documentation. Bumped package version to 1.2.8 2023-02-08 14:44:00 -08:00
7ffa2d43ef Improved the csharp client generator to check if the base url is null or whitespace and to remove trailing / characters. Fixed some bugs with different client names. Generator also now sets up cookie support. 2023-02-07 13:02:56 -08:00
6ae73aaa7d Changed response extensions to use utf-8 encoding instead of utf-16. Bumped package version to 1.2.6 2023-02-06 12:46:02 -08:00
a1f2f4a91c Fixed bug in client generation. Publishing nuget. 2023-02-06 12:15:39 -08:00
5a1efa90c7 Reverting. Actually this is better. 2023-02-06 11:38:36 -08:00
612a658b77 Changed static csharp generator to now rely on the client name so that it can be easily renamed. 2023-02-06 11:37:33 -08:00
e4c3ee0673 Added static code generation option to the CSharp Client generator. Bumped package version to 1.2.4 2023-02-06 11:21:56 -08:00
2076a1d02b Broke up RestClientGenerator so that custom generators for other languages can be created or the C# one could be modified for custom use cases. 2023-02-06 10:46:38 -08:00
ed1d10ba9d Added Json flag to RouteRequest and RouteResponse to control whether or not the content is Json and needs to be handled. Updated example code. Renamed ClientCodeGenerator to RestClientGenerator. Bumped package version to 1.2.2. 2023-02-06 09:01:06 -08:00
60768b1b3e Added a hidden route attribute that can be used to exclude a given route from code generation. Bumped package version to 1.2.1 2023-02-05 13:27:10 -08:00
5c2b2ea4ef Bumped package version to 1.2.0. Added missing route method conversions to code generator. 2023-02-05 13:02:18 -08:00
887cd687e0 Merge pull request 'Merging Code Generation Feature' (#1) from CodeGen into master
Reviewed-on: #1
2023-02-05 20:29:29 +00:00
42c4682c89 Bumped package version to 1.1.9. Added documentation and cleaned up code. Going to merge into Master. 2023-02-05 12:28:37 -08:00
1475159f1c Added RouteName and Request/Response handling to the code generator. More work is needed. 2023-02-05 10:59:29 -08:00
74f8921f7a Improved the code generator. More to come. 2023-02-04 10:37:30 -08:00
6bb01464e7 Working on C# client code generation. 2023-02-04 08:26:09 -08:00
19ccdb9026 Working on a client code generation feature to help speed up using an api built with this library. 2023-02-03 13:33:28 -08:00
34 changed files with 7310 additions and 105 deletions

1419
Logo/Logo.ai Normal file

File diff suppressed because one or more lines are too long

BIN
Logo/Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

6
Logo/Logo.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.51 215.12">
<path d="m0,117.58v-20.04c5.58,0,9.9-1.38,12.97-4.13,3.06-2.75,4.6-6.65,4.6-11.68v-42.26c0-12.54,3.5-22.25,10.49-29.14C35.04,3.45,44.91,0,57.65,0v20.04c-6.06,0-10.75,1.48-14.05,4.44-3.31,2.96-4.96,7.16-4.96,12.61v44.74c0,11.37-3.36,20.17-10.07,26.4-6.72,6.23-16.24,9.35-28.57,9.35Zm57.65,97.54c-12.74,0-22.61-3.45-29.6-10.33-6.99-6.89-10.49-16.57-10.49-29.03v-42.36c0-5.03-1.53-8.92-4.6-11.68-3.07-2.75-7.39-4.13-12.97-4.13v-20.04c12.33,0,21.85,3.22,28.57,9.66,6.72,6.44,10.07,15.55,10.07,27.33v43.4c0,5.44,1.65,9.66,4.96,12.66,3.31,3,7.99,4.49,14.05,4.49v20.04Z"/>
<path d="m80.38,174.51V24.18h21.08v150.33h-21.08Zm9.2-63.54v-20.04h51.25c6.13,0,11.06-2.13,14.77-6.41,3.72-4.27,5.58-9.92,5.58-16.94s-1.86-12.67-5.58-16.94c-3.72-4.27-8.65-6.41-14.77-6.41h-51.25v-20.04h50.42c8.75,0,16.39,1.81,22.94,5.42,6.54,3.62,11.62,8.66,15.24,15.14,3.62,6.48,5.42,14.09,5.42,22.83s-1.83,16.27-5.48,22.78c-3.65,6.51-8.73,11.57-15.24,15.19s-14.14,5.42-22.89,5.42h-50.42Zm75.32,63.54l-34.41-68.19,20.97-4.75,38.44,72.95h-25Z"/>
<path d="m261.51,117.58c-12.26,0-21.77-3.12-28.52-9.35-6.75-6.23-10.13-15.03-10.13-26.4v-44.74c0-5.44-1.65-9.64-4.96-12.61-3.31-2.96-7.99-4.44-14.05-4.44V0c12.74,0,22.61,3.45,29.6,10.33,6.99,6.89,10.49,16.6,10.49,29.14v42.26c0,5.03,1.53,8.92,4.6,11.68,3.06,2.76,7.39,4.13,12.97,4.13v20.04Zm-57.65,97.54v-20.04c6.06,0,10.75-1.5,14.05-4.49,3.31-3,4.96-7.22,4.96-12.66v-43.4c0-11.78,3.37-20.89,10.13-27.33,6.75-6.44,16.26-9.66,28.52-9.66v20.04c-5.58,0-9.9,1.38-12.97,4.13-3.07,2.76-4.6,6.65-4.6,11.68v42.36c0,12.47-3.5,22.15-10.49,29.03-6.99,6.89-16.86,10.33-29.6,10.33Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 20 KiB

501
Rest.Net.Example/Client.cs Normal file
View File

@ -0,0 +1,501 @@
//Generated using MontoyaTech.Rest.Net - 2/18/2024
public class Client
{
public string BaseUrl;
public System.Net.CookieContainer CookieContainer;
public System.Net.Http.HttpMessageHandler MessageHandler;
public System.Net.Http.HttpClient HttpClient;
public System.Action<System.Net.Http.HttpRequestMessage> RequestHandler;
public TestApi Test;
public AuthApi Auth;
public StreamApi Stream;
public FormApi Form;
public Client(string baseUrl, System.Net.Http.HttpMessageHandler handler = null, System.Action<System.Net.Http.HttpRequestMessage> requestHandler = null)
{
if (string.IsNullOrWhiteSpace(baseUrl))
throw new System.ArgumentException("baseUrl must not be null or whitespace.");
if (baseUrl.EndsWith('/'))
baseUrl = baseUrl.Substring(0, baseUrl.Length - 1);
this.BaseUrl = baseUrl;
this.CookieContainer = new System.Net.CookieContainer();
if (handler == null)
{
handler = new System.Net.Http.HttpClientHandler()
{
AllowAutoRedirect = true,
UseCookies = true,
CookieContainer = this.CookieContainer,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
}
this.MessageHandler = handler;
this.RequestHandler = requestHandler;
this.HttpClient = new System.Net.Http.HttpClient(handler);
this.HttpClient.DefaultRequestHeaders.Add("Accept", "*/*");
this.HttpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
this.HttpClient.DefaultRequestHeaders.Add("Accept-Encoding", "identity");
this.Test = new TestApi(this);
this.Auth = new AuthApi(this);
this.Stream = new StreamApi(this);
this.Form = new FormApi(this);
}
public class TestApi
{
public Client Client;
public TestApi(Client client)
{
this.Client = client;
}
public string Status()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/status");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public string Add(double a, double b)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{this.Client.BaseUrl}/add/{a}/{b}");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public string Compress()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/compress");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public string CompressFile()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/file/compress");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class AuthApi
{
public Client Client;
public AuthApi(Client client)
{
this.Client = client;
}
public bool UserExists(string name)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/auth/{name}");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return bool.Parse(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public void Signup(UserDto request, bool compress = false)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{this.Client.BaseUrl}/auth/signup");
this.Client.RequestHandler?.Invoke(message);
if (compress)
{
using (var uncompressedStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(request))))
{
using (var compressedStream = new System.IO.MemoryStream())
{
using (var gzipStream = new System.IO.Compression.GZipStream(compressedStream, System.IO.Compression.CompressionMode.Compress, true))
uncompressedStream.CopyTo(gzipStream);
message.Content = new System.Net.Http.ByteArrayContent(compressedStream.ToArray());
message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Application.Json);
message.Content.Headers.ContentEncoding.Add("gzip");
}
}
}
else
{
message.Content = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request));
message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Application.Json);
}
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
public UserDto Get()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/auth/");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserDto>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public dynamic Dynamic()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/auth/dynamic");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public UserRole GetRole()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/auth/role");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserRole>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class StreamApi
{
public Client Client;
public StreamApi(Client client)
{
this.Client = client;
}
public void Upload(System.IO.MemoryStream request, bool compress = false)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{this.Client.BaseUrl}/upload");
this.Client.RequestHandler?.Invoke(message);
request.Seek(0, System.IO.SeekOrigin.Begin);
message.Content = new System.Net.Http.StreamContent(request);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
public System.IO.MemoryStream Download()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{this.Client.BaseUrl}/download");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var stream = new System.IO.MemoryStream();
response.Content.CopyToAsync(stream).GetAwaiter().GetResult();
return stream;
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class FormApi
{
public Client Client;
public FormApi(Client client)
{
this.Client = client;
}
public System.Collections.Generic.Dictionary<string, string> FormTest()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{this.Client.BaseUrl}/form");
this.Client.RequestHandler?.Invoke(message);
var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<System.Collections.Generic.Dictionary<string, string>>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class IncludedType
{
public int Test;
public IncludedType() { }
public IncludedType(IncludedType instance)
{
this.Test = instance.Test;
}
public IncludedType(int Test = 0)
{
this.Test = Test;
}
}
public class UserDto : BaseUser
{
public System.PlatformID MachineType;
public string Name;
public System.Collections.Generic.List<string> List;
public System.String[] Array;
public ulong Property { get; set; }
public UserDto() { }
public UserDto(UserDto instance)
{
this.MachineType = instance.MachineType;
this.Name = instance.Name;
this.List = instance.List;
this.Array = instance.Array;
this.Property = instance.Property;
}
public UserDto(System.PlatformID MachineType = 0, string Name = null, System.Collections.Generic.List<string> List = null, System.String[] Array = null, ulong Property = 0)
{
this.MachineType = MachineType;
this.Name = Name;
this.List = List;
this.Array = Array;
this.Property = Property;
}
}
public class BaseUser
{
public string Id;
public char FirstInitial;
public System.Collections.Generic.List<Permission> Permissions;
public UserRole Role { get; set; }
public BaseUser() { }
public BaseUser(BaseUser instance)
{
this.Id = instance.Id;
this.FirstInitial = instance.FirstInitial;
this.Permissions = instance.Permissions;
this.Role = instance.Role;
}
public BaseUser(string Id = null, char FirstInitial = '\0', System.Collections.Generic.List<Permission> Permissions = null, UserRole Role = 0)
{
this.Id = Id;
this.FirstInitial = FirstInitial;
this.Permissions = Permissions;
this.Role = Role;
}
public class Permission
{
public string Name;
public Types Type;
public Permission() { }
public Permission(Permission instance)
{
this.Name = instance.Name;
this.Type = instance.Type;
}
public Permission(string Name = null, Types Type = 0)
{
this.Name = Name;
this.Type = Type;
}
public enum Types : int
{
Read = 0,
Write = 1,
}
}
}
public enum UserRole : byte
{
Unknown = 0,
Admin = 2,
User = 1,
}
}

View File

@ -4,44 +4,117 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using MontoyaTech.Rest.Net;
using System.Net.Mime;
using System.Collections;
using Newtonsoft.Json.Linq;
using System.Web;
using System.Net.Http;
using Newtonsoft.Json;
namespace MontoyaTech.Rest.Net.Example
{
public class Program
public class BaseUser
{
public class User
[JsonProperty("id")]
public string Id;
[JsonProperty("firstInitial")]
public char FirstInitial;
[JsonProperty("role")]
public UserRole Role { get; set; }
[JsonProperty("permissions")]
public List<Permission> Permissions;
public class Permission
{
public string Name;
public Types Type;
public enum Types { Read, Write }
}
}
[RouteTypeName("UserDto")]
public class User : BaseUser
{
public PlatformID MachineType;
public string Name = null;
public List<string> List = null;
public string[] Array = null;
public ulong Property { get; set; }
public User() { }
public User(string name)
{
this.Name = name;
}
}
public static explicit operator User(string input)
public enum UserRole : byte
{
return new User(input.ToString());
Unknown = 0,
Admin = 2,
User = 1
}
public class IncludedType
{
public int Test;
}
public class Program
{
public static RouteFileCache FileCache = new RouteFileCache(100 * 1024 * 1024);
public static void Main(string[] args)
{
var listener = new RouteListener(8080,
File.WriteAllText("test.txt", "hello from a file");
var listener = new RouteListener(8088,
new Route(HttpRequestMethod.Get, "/status", Status),
new Route<double, double>(HttpRequestMethod.Post, "/add/{a}/{b}", Add),
new Route<User>(HttpRequestMethod.Post, "/signup/{username}", Signup),
new Route(HttpRequestMethod.Get, "/json", Json)
new Route(HttpRequestMethod.Get, "/compress", Compress),
new Route(HttpRequestMethod.Get, "/file/compress", CompressFile),
new Route<string>(HttpRequestMethod.Get, "/auth/{username}", Exists),
new Route(HttpRequestMethod.Post, "/auth/signup", Signup),
new Route(HttpRequestMethod.Get, "/auth/", Json),
new Route(HttpRequestMethod.Get, "/auth/dynamic", Dynamic),
new Route(HttpRequestMethod.Get, "/auth/role", GetRole),
new Route(HttpRequestMethod.Post, "/upload", Upload),
new Route(HttpRequestMethod.Get, "/download", Download),
new Route(HttpRequestMethod.Post, "/form", FormTest)
);
string code = listener.GenerateCSharpClient();
File.WriteAllText("Client.cs", listener.GenerateCSharpClient());
File.WriteAllText("Client.js", listener.GenerateJavascriptClient(useJsonNames: true));
File.WriteAllText("StaticClient.cs", listener.GenerateCSharpClient("StaticClient", staticCode: true));
File.WriteAllText("StaticClient.js", listener.GenerateJavascriptClient("StaticClient", staticCode: true, useJsonNames: true));
Console.WriteLine("Generated Client.cs, Client.js, StaticClient.cs, StaticClient.js");
listener.RequestPreProcessEvent += (HttpListenerContext context) => {
Console.WriteLine("Request start: " + context.Request.RawUrl);
Console.WriteLine($"[{context.Request.HttpMethod}] Request start: " + context.Request.RawUrl);
return true;
};
listener.RequestPostProcessEvent += (HttpListenerContext context) =>
{
Console.WriteLine("Request end: " + context.Request.RawUrl);
Console.WriteLine($"[{context.Request.HttpMethod}] Request end: " + context.Request.RawUrl);
};
listener.Start();
@ -51,29 +124,140 @@ namespace MontoyaTech.Rest.Net.Example
foreach (var route in listener.Routes)
Console.WriteLine($"- [{route.Method}] {route.Syntax}");
Console.WriteLine($"Rest api server running at http://localhost:{listener.Port}");
Console.WriteLine($"Rest api server running at {listener.BaseUrl}");
StaticClient.Init(listener.BaseUrl, requestHandler: (message) =>
{
var builder = new UriBuilder(message.RequestUri);
var query = HttpUtility.ParseQueryString(builder.Query);
query.Add("authToken", "test");
builder.Query = query.ToString();
message.RequestUri = builder.Uri;
message.Headers.Add("Auth", "Test");
});
using (var stream = new MemoryStream())
{
var bytes = Encoding.UTF8.GetBytes("hello world!");
stream.Write(bytes, 0, bytes.Length);
StaticClient.Stream.Upload(stream);
}
using (var stream = StaticClient.Stream.Download())
{
var str = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine("Download output:" + str);
}
listener.Block();
}
[RouteGroup("Test")]
[RouteResponse(typeof(string))]
[RouteInclude(typeof(IncludedType))]
public static HttpListenerResponse Status(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithText("Everything is operational. 👍");
}
[RouteGroup("Test")]
[RouteResponse(typeof(string))]
public static HttpListenerResponse Add(HttpListenerContext context, double a, double b)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithText((a + b).ToString());
}
public static HttpListenerResponse Signup(HttpListenerContext context, User user)
[RouteGroup("Test")]
[RouteResponse(typeof(string))]
public static HttpListenerResponse Compress(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithText("User:" + user.Name);
return context.Response.WithStatus(HttpStatusCode.OK).WithCompressedText("hello world");
}
[RouteGroup("Test")]
[RouteResponse(typeof(string))]
public static HttpListenerResponse CompressFile(HttpListenerContext context)
{
var content = FileCache.Cache("test.txt");
return context.Response.WithStatus(HttpStatusCode.OK).WithCompressedFile("test.txt", content);
}
[RouteGroup("Test")]
[RouteRequest(typeof(User))]
public static HttpListenerResponse SignupRequest(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK);
}
[RouteGroup("Auth")]
[RouteName("UserExists")]
[RouteResponse(typeof(bool), Json = false)]
public static HttpListenerResponse Exists(HttpListenerContext context, string name)
{
Console.WriteLine("Auth.Exists called, name:" + name);
return context.Response.WithStatus(HttpStatusCode.OK).WithText("true");
}
[RouteGroup("Auth")]
[RouteRequest(typeof(User))]
public static HttpListenerResponse Signup(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK);
}
[RouteGroup("Auth")]
[RouteResponse(typeof(UserRole))]
public static HttpListenerResponse GetRole(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithJson(UserRole.Admin);
}
[RouteGroup("Auth")]
[RouteName("Get")]
[RouteResponse(typeof(User))]
public static HttpListenerResponse Json(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithJson(new User("Rest.Net"));
}
[RouteGroup("Auth")]
[RouteResponse(Dynamic = true)]
public static HttpListenerResponse Dynamic(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithJson(777);
}
[RouteGroup("Stream")]
[RouteRequest(typeof(MemoryStream))]
public static HttpListenerResponse Upload(HttpListenerContext context)
{
var content = context.Request.ReadAsString();
Console.WriteLine("Uploaded:" + content);
return context.Response.WithStatus(HttpStatusCode.OK);
}
[RouteGroup("Stream")]
[RouteResponse(typeof(MemoryStream), Parameter = false)]
public static HttpListenerResponse Download(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithText("Hello world");
}
[RouteGroup("Form")]
[RouteResponse(typeof(Dictionary<string, string>))]
public static HttpListenerResponse FormTest(HttpListenerContext context)
{
return context.Response.WithStatus(HttpStatusCode.OK).WithJson(context.Request.ReadAsForm());
}
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<AssemblyName>MontoyaTech.Rest.Net.Example</AssemblyName>

View File

@ -0,0 +1,457 @@
//Generated using MontoyaTech.Rest.Net - 2/18/2024
public class StaticClient
{
public static string BaseUrl;
public static System.Net.CookieContainer CookieContainer;
public static System.Net.Http.HttpMessageHandler MessageHandler;
public static System.Net.Http.HttpClient HttpClient;
public static System.Action<System.Net.Http.HttpRequestMessage> RequestHandler;
public static void Init(string baseUrl, System.Net.Http.HttpMessageHandler handler = null, System.Action<System.Net.Http.HttpRequestMessage> requestHandler = null)
{
if (string.IsNullOrWhiteSpace(baseUrl))
throw new System.ArgumentException("baseUrl must not be null or whitespace.");
if (baseUrl.EndsWith('/'))
baseUrl = baseUrl.Substring(0, baseUrl.Length - 1);
StaticClient.BaseUrl = baseUrl;
StaticClient.CookieContainer = new System.Net.CookieContainer();
if (handler == null)
{
handler = new System.Net.Http.HttpClientHandler()
{
AllowAutoRedirect = true,
UseCookies = true,
CookieContainer = StaticClient.CookieContainer,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
}
StaticClient.MessageHandler = handler;
StaticClient.RequestHandler = requestHandler;
StaticClient.HttpClient = new System.Net.Http.HttpClient(handler);
StaticClient.HttpClient.DefaultRequestHeaders.Add("Accept", "*/*");
StaticClient.HttpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
StaticClient.HttpClient.DefaultRequestHeaders.Add("Accept-Encoding", "identity");
}
public class Test
{
public static string Status()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/status");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public static string Add(double a, double b)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{StaticClient.BaseUrl}/add/{a}/{b}");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public static string Compress()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/compress");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public static string CompressFile()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/file/compress");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<string>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class Auth
{
public static bool UserExists(string name)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/auth/{name}");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return bool.Parse(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public static void Signup(UserDto request, bool compress = false)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{StaticClient.BaseUrl}/auth/signup");
StaticClient.RequestHandler?.Invoke(message);
if (compress)
{
using (var uncompressedStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(request))))
{
using (var compressedStream = new System.IO.MemoryStream())
{
using (var gzipStream = new System.IO.Compression.GZipStream(compressedStream, System.IO.Compression.CompressionMode.Compress, true))
uncompressedStream.CopyTo(gzipStream);
message.Content = new System.Net.Http.ByteArrayContent(compressedStream.ToArray());
message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Application.Json);
message.Content.Headers.ContentEncoding.Add("gzip");
}
}
}
else
{
message.Content = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request));
message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Application.Json);
}
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
public static UserDto Get()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/auth/");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserDto>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public static dynamic Dynamic()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/auth/dynamic");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
public static UserRole GetRole()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/auth/role");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserRole>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class Stream
{
public static void Upload(System.IO.MemoryStream request, bool compress = false)
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{StaticClient.BaseUrl}/upload");
StaticClient.RequestHandler?.Invoke(message);
request.Seek(0, System.IO.SeekOrigin.Begin);
message.Content = new System.Net.Http.StreamContent(request);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
public static System.IO.MemoryStream Download()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, $"{StaticClient.BaseUrl}/download");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var stream = new System.IO.MemoryStream();
response.Content.CopyToAsync(stream).GetAwaiter().GetResult();
return stream;
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class Form
{
public static System.Collections.Generic.Dictionary<string, string> FormTest()
{
var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, $"{StaticClient.BaseUrl}/form");
StaticClient.RequestHandler?.Invoke(message);
var response = StaticClient.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (string.IsNullOrEmpty(content))
return default;
return Newtonsoft.Json.JsonConvert.DeserializeObject<System.Collections.Generic.Dictionary<string, string>>(content);
}
else
{
throw new System.Exception("Unexpected Http Response StatusCode:" + response.StatusCode);
}
}
}
public class IncludedType
{
public int Test;
public IncludedType() { }
public IncludedType(IncludedType instance)
{
this.Test = instance.Test;
}
public IncludedType(int Test = 0)
{
this.Test = Test;
}
}
public class UserDto : BaseUser
{
public System.PlatformID MachineType;
public string Name;
public System.Collections.Generic.List<string> List;
public System.String[] Array;
public ulong Property { get; set; }
public UserDto() { }
public UserDto(UserDto instance)
{
this.MachineType = instance.MachineType;
this.Name = instance.Name;
this.List = instance.List;
this.Array = instance.Array;
this.Property = instance.Property;
}
public UserDto(System.PlatformID MachineType = 0, string Name = null, System.Collections.Generic.List<string> List = null, System.String[] Array = null, ulong Property = 0)
{
this.MachineType = MachineType;
this.Name = Name;
this.List = List;
this.Array = Array;
this.Property = Property;
}
}
public class BaseUser
{
public string Id;
public char FirstInitial;
public System.Collections.Generic.List<Permission> Permissions;
public UserRole Role { get; set; }
public BaseUser() { }
public BaseUser(BaseUser instance)
{
this.Id = instance.Id;
this.FirstInitial = instance.FirstInitial;
this.Permissions = instance.Permissions;
this.Role = instance.Role;
}
public BaseUser(string Id = null, char FirstInitial = '\0', System.Collections.Generic.List<Permission> Permissions = null, UserRole Role = 0)
{
this.Id = Id;
this.FirstInitial = FirstInitial;
this.Permissions = Permissions;
this.Role = Role;
}
public class Permission
{
public string Name;
public Types Type;
public Permission() { }
public Permission(Permission instance)
{
this.Name = instance.Name;
this.Type = instance.Type;
}
public Permission(string Name = null, Types Type = 0)
{
this.Name = Name;
this.Type = Type;
}
public enum Types : int
{
Read = 0,
Write = 1,
}
}
}
public enum UserRole : byte
{
Unknown = 0,
Admin = 2,
User = 1,
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPackable>false</IsPackable>
@ -9,14 +9,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="FluentAssertions" Version="8.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using FluentAssertions;
using MontoyaTech.Rest.Net;
using System.Net;
namespace MontoyaTech.Rest.Net.Tests
{
public class RestClientGeneratorTests
{
[Fact]
public void RestClientGenerator_HttpStatusCode_Should_BeSystemType()
{
var generator = new RestClientGenerator();
generator.IsTypeDotNet(typeof(HttpStatusCode)).Should().BeTrue();
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using MontoyaTech.Rest.Net;
using Xunit;
namespace Rest.Net.Tests
{
public class RouteArgumentConverterTests
{
public enum TestEnum : long
{
A = 1,
B = 2,
C = 3
}
[Fact]
public static void RouteArgumentConverter_Should_Convert_To_Enum()
{
var converted = RouteArgumentConverter.Convert<TestEnum>("1");
converted.GetType().IsEquivalentTo(typeof(TestEnum)).Should().BeTrue();
converted.Should().Be(TestEnum.A);
}
[Fact]
public static void RouteArgumentConverter_Should_Convert_OutOfRange_To_Enum()
{
var converted = RouteArgumentConverter.Convert<TestEnum>("4");
converted.GetType().IsEquivalentTo(typeof(TestEnum)).Should().BeTrue();
((int)converted).Should().Be(4);
}
}
}

View File

@ -10,24 +10,72 @@ namespace MontoyaTech.Rest.Net.Tests
{
public class RouteMatcherTests
{
[Fact]
public void SyntaxWithPathShouldNotMatchRoot()
{
RouteMatcher.Matches("http://localhost/", "/test", out _).Should().BeFalse();
}
[Fact]
public void SyntaxWithRootShouldMatch()
{
RouteMatcher.Matches("http://localhost/", "/", out _).Should().BeTrue();
}
[Fact]
public void SyntaxWithRootNoSlashShouldMatch()
{
RouteMatcher.Matches("http://localhost", "/", out _).Should().BeTrue();
}
[Fact]
public void SyntaxWithRootCatchAllShouldMatch()
{
RouteMatcher.Matches("http://localhost/test1/test2", "/**", out _).Should().BeTrue();
}
[Fact]
public void SyntaxNonSlashShouldNotMatch()
{
RouteMatcher.Matches("http://localhost/test1/", "/test1", out _).Should().BeFalse();
}
[Fact]
public void SyntaxSlashShouldMatch()
{
RouteMatcher.Matches("http://localhost/test1/", "/test1/", out _).Should().BeTrue();
}
[Fact]
public void SyntaxSlashExtraRouteShouldNotMatch()
{
RouteMatcher.Matches("http://localhost/test1/test2", "/test1/", out _).Should().BeFalse();
}
[Fact]
public void SyntaxSlashCatchAllEmptyShouldMatch()
{
RouteMatcher.Matches("http://localhost/test1/", "/test1/**", out _).Should().BeTrue();
}
[Fact]
public void SyntaxSlashCatchAllEmptyNoSlashShouldNotMatch()
{
RouteMatcher.Matches("http://localhost/test1", "/test1/**", out _).Should().BeFalse();
}
[Fact]
public void SyntaxWithRootWildcardShouldMatch()
{
RouteMatcher.Matches("http://localhost/test1", "/*", out _).Should().BeTrue();
}
[Fact]
public void SyntaxWildCardEmptyShouldMatch()
{
RouteMatcher.Matches("http://localhost/test1/", "/test1/*", out _).Should().BeTrue();
}
[Fact]
public void SyntaxWithRootWildcardShouldNotMatch()
{
@ -99,6 +147,7 @@ namespace MontoyaTech.Rest.Net.Tests
public void SyntaxWithOrShouldMatch()
{
RouteMatcher.Matches("http://localhost/a/b", "/a/b|c", out _).Should().BeTrue();
RouteMatcher.Matches("http://localhost/a/c", "/a/b|c", out _).Should().BeTrue();
}

View File

@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using MontoyaTech.Rest.Net;
using Xunit;
namespace MontoyaTech.Rest.Net.Tests
{
public class ServeFileTests
{
public string BaseDirectory = null;
public string TestDirectory = null;
public string TestFile = null;
public string IndexFile = null;
public ServeFileTests()
{
this.BaseDirectory = Path.Combine(Environment.CurrentDirectory, "test");
if (!Directory.Exists(this.BaseDirectory))
Directory.CreateDirectory(this.BaseDirectory);
this.TestDirectory = Path.Combine(this.BaseDirectory, "test2");
if (!Directory.Exists(this.TestDirectory))
Directory.CreateDirectory(this.TestDirectory);
this.TestFile = Path.Combine(this.BaseDirectory, "test.html");
if (!File.Exists(this.TestFile))
File.WriteAllText(this.TestFile, "hello world");
this.IndexFile = Path.Combine(this.BaseDirectory, "index.html");
if (!File.Exists(this.IndexFile))
File.WriteAllText(this.IndexFile, "hello world");
}
[Fact]
public void ServeMultiple_File_ShouldWork()
{
HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, null, "/test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
[Fact]
public void ServeMultiple_Directory_ShouldWork()
{
HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, null, "/test2", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeTrue();
resolvedPath.Should().BeEquivalentTo(this.TestDirectory);
}
[Fact]
public void ServeMultiple_NavigatingUp_Should_NotWork()
{
HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, null, "../test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeFalse();
}
[Fact]
public void ServeMultiple_Correct_NavigatingUp_Should_Work()
{
HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, null, "a/b/../../test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
[Fact]
public void ServeMultiple_NavigatingUp_Multiple_Should_NotWork()
{
HttpListenerResponseExtensions.ResolveMultiPagePath(this.BaseDirectory, null, "test/../../test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeFalse();
}
[Fact]
public void ServeSingle_Empty_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.IndexFile);
}
[Fact]
public void ServeSingle_File_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
[Fact]
public void ServeSingle_Directory_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/test2", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeTrue();
resolvedPath.Should().BeEquivalentTo(this.TestDirectory);
}
[Fact]
public void ServeSingle_File_Route_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/a/b/test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
[Fact]
public void ServeSingle_Directory_Route_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/a/b/test2", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeTrue();
resolvedPath.Should().BeEquivalentTo(this.TestDirectory);
}
[Fact]
public void ServeSingle_File_Route_Invalid_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/a/b/c", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.IndexFile);
}
[Fact]
public void ServeSingle_File_WithoutExtension_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/test", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
[Fact]
public void ServeSingle_File_Route_WithoutExtension_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, null, "/a/b/c/test", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
[Fact]
public void ServeSingle_File_StartPath_Should_Work()
{
HttpListenerResponseExtensions.ResolveSinglePagePath(this.BaseDirectory, "/test", "test/test.html", "index.html", out string resolvedPath, out bool isDirectory).Should().BeTrue();
isDirectory.Should().BeFalse();
resolvedPath.Should().BeEquivalentTo(this.TestFile);
}
}
}

430
Rest.Net/CodeWriter.cs Normal file
View File

@ -0,0 +1,430 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of a writer that helps with generating code.
/// </summary>
public class CodeWriter
{
/// <summary>
/// The internal string builder.
/// </summary>
private StringBuilder Builder = new StringBuilder();
/// <summary>
/// The current number of characters written..
/// </summary>
public int Length
{
get
{
return Builder.Length;
}
}
/// <summary>
/// The current number of indents.
/// </summary>
private int Indents = 0;
/// <summary>
/// Whether or not the writer is pending an indent that needs
/// to be handled.
/// </summary>
private bool PendingIndent = false;
/// <summary>
/// Creates a new default CodeWriter.
/// </summary>
public CodeWriter() { }
/// <summary>
/// Creates a new default text writer and copies the indent data from the passed
/// text writer.
/// </summary>
/// <param name="writer"></param>
public CodeWriter(CodeWriter writer)
{
this.Indents = writer.Indents;
//If we have indents, then we must set pending indent.
if (this.Indents > 0)
this.PendingIndent = true;
}
/// <summary>
/// Writes text to the writer.
/// </summary>
/// <param name="text"></param>
public CodeWriter Write(string text)
{
if (this.PendingIndent)
{
for (var i = 0; i < Indents; i++)
this.Builder.Append(' ');
this.PendingIndent = false;
}
this.Builder.Append(text);
return this;
}
/// <summary>
/// Writes text to the writer surrounded by quotes.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public CodeWriter WriteString(string text)
{
if (this.PendingIndent)
{
for (var i = 0; i < Indents; i++)
this.Builder.Append(' ');
this.PendingIndent = false;
}
this.Builder.Append('"').Append(text).Append('"');
return this;
}
/// <summary>
/// Writes a character to the writer.
/// </summary>
/// <param name="char"></param>
public CodeWriter Write(char @char)
{
if (this.PendingIndent)
{
for (var i = 0; i < Indents; i++)
this.Builder.Append(' ');
this.PendingIndent = false;
}
this.Builder.Append(@char);
return this;
}
/// <summary>
/// Writes text to the writer and a newline.
/// </summary>
/// <param name="text"></param>
public CodeWriter WriteLine(string text)
{
if (this.PendingIndent)
{
for (var i = 0; i < Indents; i++)
this.Builder.Append(' ');
this.PendingIndent = false;
}
this.Builder.Append(text);
this.Builder.Append('\n');
this.PendingIndent = true;
return this;
}
/// <summary>
/// Writes a character and new line to the writer.
/// </summary>
/// <param name="char"></param>
public CodeWriter WriteLine(char @char)
{
if (this.PendingIndent)
{
for (var i = 0; i < Indents; i++)
this.Builder.Append(' ');
this.PendingIndent = false;
}
this.Builder.Append(@char);
this.Builder.Append('\n');
this.PendingIndent = true;
return this;
}
/// <summary>
/// Writes text to the writer if the condition is met.
/// </summary>
/// <param name="condition"></param>
/// <param name="text"></param>
/// <returns></returns>
public CodeWriter WriteAssert(bool condition, string text)
{
if (condition)
return this.Write(text);
return this;
}
/// <summary>
/// Writes a character to the writer if the condition is met.
/// </summary>
/// <param name="condition"></param>
/// <param name="char"></param>
/// <returns></returns>
public CodeWriter WriteAssert(bool condition, char @char)
{
if (condition)
return this.Write(@char);
return this;
}
/// <summary>
/// Writes text to the writer and a newline if the condition is met.
/// </summary>
/// <param name="condition"></param>
/// <param name="text"></param>
/// <returns></returns>
public CodeWriter WriteLineAssert(bool condition, string text)
{
if (condition)
return this.WriteLine(text);
return this;
}
/// <summary>
/// Writes a character and new line to the writer if the condition is met.
/// </summary>
/// <param name="condition"></param>
/// <param name="char"></param>
/// <returns></returns>
public CodeWriter WriteLineAssert(bool condition, char @char)
{
if (condition)
return this.WriteLine(@char);
return this;
}
/// <summary>
/// Writes the contents of another text writer into this one.
/// </summary>
/// <param name="writer"></param>
public CodeWriter Write(CodeWriter writer)
{
this.Builder.Append(writer.Builder.ToString());
return this;
}
/// <summary>
/// Inserts a new line to the writer.
/// </summary>
public CodeWriter NewLine()
{
this.Builder.Append('\n');
this.PendingIndent = true;
return this;
}
/// <summary>
/// Inserts a new line to the writer if the condition is met.
/// </summary>
public CodeWriter NewLineAssert(bool condition)
{
if (condition)
{
this.Builder.Append('\n');
this.PendingIndent = true;
}
return this;
}
/// <summary>
/// Writes a space to the writer unless there is already one.
/// </summary>
/// <returns></returns>
public CodeWriter WriteSpacer()
{
if (this.Builder.Length >= 1 && this.Builder[this.Builder.Length - 1] != ' ')
this.Builder.Append(' ');
return this;
}
/// <summary>
/// Writes a separator to the writer unless one wouldn't make sense.
/// </summary>
/// <returns></returns>
public CodeWriter WriteSeparator()
{
if (this.Builder.Length > 0)
{
var offset = this.Builder.Length - 1;
var prev = this.Builder[offset];
while (offset >= 0 && (prev == ' ' || prev == '\t' || prev == '\n' || prev == '\r'))
{
offset--;
prev = this.Builder[offset];
}
if (prev != '(' && prev != '{' && prev != '[' && prev != ',')
this.Builder.Append(", ");
}
return this;
}
/// <summary>
/// Writes a separator to the writer and text if a condition is met unless a separator here isn't needed, then just text is written.
/// </summary>
/// <param name="condition"></param>
/// <param name="text"></param>
/// <returns></returns>
public CodeWriter WriteSeparatorAssert(bool condition, string text)
{
if (condition)
{
if (this.Builder.Length > 0)
{
var offset = this.Builder.Length - 1;
var prev = this.Builder[offset];
while (offset >= 0 && (prev == ' ' || prev == '\t' || prev == '\n' || prev == '\r'))
{
offset--;
prev = this.Builder[offset];
}
if (prev != '(' && prev != '{' && prev != '[' && prev != ',')
this.Builder.Append(", ");
}
if (text != null)
this.Builder.Append(text);
}
return this;
}
/// <summary>
/// Writes a blank line as a spacer to the writer
/// unless there is already one.
/// </summary>
public CodeWriter WriteBreak()
{
if (this.Builder.Length >= 2)
{
var prevChar = this.Builder[this.Builder.Length - 2];
if (prevChar != '\n' && prevChar != '{' && prevChar != '(' && prevChar != '[')
{
this.Builder.Append('\n');
this.PendingIndent = true;
}
}
else
{
this.Builder.Append('\n');
this.PendingIndent = true;
}
return this;
}
/// <summary>
/// Indents the writer one time.
/// </summary>
public CodeWriter Indent()
{
this.Indents += 4;
return this;
}
/// <summary>
/// Indents the writer one time if the condition is met.
/// </summary>
public CodeWriter IndentAssert(bool condition)
{
if (condition)
this.Indents += 4;
return this;
}
/// <summary>
/// Removes one indent from the writer.
/// </summary>
public CodeWriter Outdent()
{
if (this.Indents > 0)
this.Indents -= 4;
return this;
}
/// <summary>
/// Removes one indent from the writer if the condition is met.
/// </summary>
public CodeWriter OutdentAssert(bool condition)
{
if (condition && this.Indents > 0)
this.Indents -= 4;
return this;
}
/// <summary>
/// Resets all indents from the writer.
/// </summary>
public CodeWriter ResetIndents()
{
this.Indents = 0;
return this;
}
/// <summary>
/// Removes the last character from the writer.
/// </summary>
/// <returns></returns>
public CodeWriter Remove()
{
if (this.Builder.Length > 0)
this.Builder.Remove(this.Builder.Length - 1, 1);
return this;
}
/// <summary>
/// Returns the last character written to the writer if there was one, otherwise it returns character 0.
/// </summary>
/// <returns></returns>
public char Peek()
{
if (this.Builder.Length > 0)
return this.Builder[this.Builder.Length - 1];
else
return (char)0;
}
/// <summary>
/// Gets all the written data from the writer.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return this.Builder.ToString();
}
}
}

View File

@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks;
using System.IO;
using Newtonsoft.Json;
using System.IO.Compression;
namespace MontoyaTech.Rest.Net
{
@ -22,11 +23,22 @@ namespace MontoyaTech.Rest.Net
public static string ReadAsString(this HttpListenerRequest request)
{
try
{
//If the request has been compressed, automatically handle it.
if (request.Headers["Content-Encoding"] == "gzip")
{
using (var inputStream = request.InputStream)
using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, true))
using (var stream = new StreamReader(gzipStream))
return stream.ReadToEnd();
}
else
{
using (var input = request.InputStream)
using (var stream = new StreamReader(input))
return stream.ReadToEnd();
}
}
catch
{
return "";
@ -43,10 +55,21 @@ namespace MontoyaTech.Rest.Net
{
try
{
using (var input = request.InputStream)
using (var stream = new StreamReader(input))
//If the request has been compressed, automatically handle it.
if (request.Headers["Content-Encoding"] == "gzip")
{
using (var inputStream = request.InputStream)
using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, true))
using (var stream = new StreamReader(gzipStream))
return JsonConvert.DeserializeObject<T>(stream.ReadToEnd());
}
else
{
using (var inputStream = request.InputStream)
using (var stream = new StreamReader(inputStream))
return JsonConvert.DeserializeObject<T>(stream.ReadToEnd());
}
}
catch
{
return default(T);
@ -61,6 +84,24 @@ namespace MontoyaTech.Rest.Net
public static byte[] ReadAsBytes(this HttpListenerRequest request)
{
try
{
//If the request has been compressed, automatically handle it.
if (request.Headers["Content-Encoding"] == "gzip")
{
using (var inputStream = request.InputStream)
{
using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, true))
{
using (var memoryStream = new MemoryStream())
{
gzipStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}
}
else
{
using (var input = request.InputStream)
{
@ -71,6 +112,7 @@ namespace MontoyaTech.Rest.Net
}
}
}
}
catch
{
return null;
@ -86,9 +128,23 @@ namespace MontoyaTech.Rest.Net
public static bool ReadToStream(this HttpListenerRequest request, Stream stream)
{
try
{
//If the request has been compressed, automatically handle it.
if (request.Headers["Content-Encoding"] == "gzip")
{
using (var inputStream = request.InputStream)
{
using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, true))
{
gzipStream.CopyTo(stream);
}
}
}
else
{
using (var input = request.InputStream)
input.CopyTo(stream);
}
return true;
}
@ -97,5 +153,24 @@ namespace MontoyaTech.Rest.Net
return false;
}
}
/// <summary>
/// Reads the content of a HttpListenerRequest as http form data and returns it as a dictionary. Any duplicate keys are ignored.
/// </summary>
/// <param name="request"></param>
/// <returns>A dictionary of keyvalue pairs from the form data.</returns>
public static Dictionary<string, string> ReadAsForm(this HttpListenerRequest request)
{
try
{
using (var input = request.InputStream)
using (var stream = new StreamReader(input))
return stream.ReadToEnd().Trim().Split('&').Where(value => value.Contains('=')).ToLookup(value => value.Split('=')[0].Trim(), value => value.Split('=')[1].Trim()).ToDictionary(group => group.Key, group => group.FirstOrDefault());
}
catch
{
return new Dictionary<string, string>();
}
}
}
}

View File

@ -6,6 +6,8 @@ using System.Text;
using System.Threading.Tasks;
using System.IO;
using Newtonsoft.Json;
using System.IO.Compression;
using System.IO.Pipes;
namespace MontoyaTech.Rest.Net
{
@ -14,6 +16,18 @@ namespace MontoyaTech.Rest.Net
/// </summary>
public static class HttpListenerResponseExtensions
{
/// <summary>
/// Sets the response to have no body.
/// </summary>
/// <param name="response"></param>
/// <returns></returns>
public static HttpListenerResponse WithNoBody(this HttpListenerResponse response)
{
response.ContentLength64 = 0;
return response;
}
/// <summary>
/// Sets the response content type to text and writes the given text to it.
/// </summary>
@ -21,12 +35,69 @@ namespace MontoyaTech.Rest.Net
/// <param name="text"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithText(this HttpListenerResponse response, string text)
{
response.ContentType = "text/plain; charset=utf-8";
var bytes = Encoding.UTF8.GetBytes(text);
response.ContentLength64 = bytes.Length;
response.OutputStream.Write(bytes, 0, bytes.Length);
response.OutputStream.Dispose();
return response;
}
/// <summary>
/// Sets the response content type to text encoded as utf16 and writes the given text to it.
/// </summary>
/// <param name="response"></param>
/// <param name="text"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithText16(this HttpListenerResponse response, string text)
{
response.ContentType = "text/plain; charset=utf-16";
var bytes = Encoding.Unicode.GetBytes(text);
response.ContentLength64 = bytes.Length;
response.OutputStream.Write(bytes, 0, bytes.Length);
response.OutputStream.Dispose();
return response;
}
/// <summary>
/// Sets the response content type to text and writes the given text compressed to it.
/// </summary>
/// <param name="response"></param>
/// <param name="text"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithCompressedText(this HttpListenerResponse response, string text)
{
response.ContentType = "text/plain; charset=utf-8";
response.Headers.Add("Content-Encoding", "gzip");
using (var memoryStream = new MemoryStream())
{
var bytes = Encoding.UTF8.GetBytes(text);
using (var compressedStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
compressedStream.Write(bytes, 0, bytes.Length);
response.ContentLength64 = memoryStream.Length;
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(response.OutputStream);
response.OutputStream.Dispose();
}
return response;
}
@ -37,12 +108,69 @@ namespace MontoyaTech.Rest.Net
/// <param name="obj"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithJson(this HttpListenerResponse response, object obj)
{
response.ContentType = "application/json; charset=utf-8";
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(obj));
response.ContentLength64 = bytes.Length;
response.OutputStream.Write(bytes, 0, bytes.Length);
response.OutputStream.Dispose();
return response;
}
/// <summary>
/// Sets the response content type to json encoded as utf16 and serializes the object as json and writes it.
/// </summary>
/// <param name="response"></param>
/// <param name="obj"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithJson16(this HttpListenerResponse response, object obj)
{
response.ContentType = "application/json; charset=utf-16";
var bytes = Encoding.Unicode.GetBytes(JsonConvert.SerializeObject(obj));
response.ContentLength64 = bytes.Length;
response.OutputStream.Write(bytes, 0, bytes.Length);
response.OutputStream.Dispose();
return response;
}
/// <summary>
/// Sets the response content type to json and writes the given json compressed to it.
/// </summary>
/// <param name="response"></param>
/// <param name="obj"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithCompressedJson(this HttpListenerResponse response, object obj)
{
response.ContentType = "application/json; charset=utf-8";
response.Headers.Add("Content-Encoding", "gzip");
using (var memoryStream = new MemoryStream())
{
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(obj));
using (var compressedStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
compressedStream.Write(bytes, 0, bytes.Length);
response.ContentLength64 = memoryStream.Length;
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(response.OutputStream);
response.OutputStream.Dispose();
}
return response;
}
@ -50,17 +178,160 @@ namespace MontoyaTech.Rest.Net
/// Sets the response content type to a file and writes the given file content to the response.
/// </summary>
/// <param name="response"></param>
/// <param name="filePath"></param>
/// <param name="filePath">The path of the file to send.</param>
/// <param name="mimeType">The mime type of the file to send, if null, it will be auto detected if possible.</param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithFile(this HttpListenerResponse response, string filePath)
public static HttpListenerResponse WithFile(this HttpListenerResponse response, string filePath, string mimeType = null)
{
response.ContentType = "application/octet-stream";
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("filePath must not be null or empty");
if (string.IsNullOrWhiteSpace(mimeType))
mimeType = Path.GetExtension(filePath).GetMimeType();
response.ContentType = mimeType;
response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(filePath)}""");
response.SendChunked = true;
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (var responseStream = response.OutputStream)
fileStream.CopyTo(responseStream);
{
response.ContentLength64 = fileStream.Length;
fileStream.CopyTo(response.OutputStream);
response.OutputStream.Dispose();
}
return response;
}
/// <summary>
/// Sets the response content type to a file and writes the file content with name to the response.
/// </summary>
/// <param name="response"></param>
/// <param name="filePath"></param>
/// <param name="content"></param>
/// <param name="mimeType"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static HttpListenerResponse WithFile(this HttpListenerResponse response, string filePath, byte[] content, string mimeType = null)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("filePath must not be null or empty");
if (string.IsNullOrWhiteSpace(mimeType))
mimeType = Path.GetExtension(filePath).GetMimeType();
response.ContentType = mimeType;
response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(filePath)}""");
response.ContentLength64 = content.Length;
response.OutputStream.Write(content, 0, content.Length);
response.OutputStream.Dispose();
return response;
}
/// <summary>
/// Sets the response content type to a file and compresses the given file content to the response.
/// </summary>
/// <param name="response"></param>
/// <param name="filePath">The path of the file to send.</param>
/// <param name="mimeType">The mime type of the file to send, if null, it will be auto detected if possible.</param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithCompressedFile(this HttpListenerResponse response, string filePath, string mimeType = null)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("filePath must not be null or empty");
if (string.IsNullOrWhiteSpace(mimeType))
mimeType = Path.GetExtension(filePath).GetMimeType();
response.ContentType = mimeType;
response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(filePath)}""");
response.Headers.Add("Content-Encoding", "gzip");
using (var memoryStream = new MemoryStream())
{
using (var compressedStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
fileStream.CopyTo(compressedStream);
response.ContentLength64 = memoryStream.Length;
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(response.OutputStream);
response.OutputStream.Dispose();
}
return response;
}
/// <summary>
/// Sets the response content type to a file and writes the file content with name to the response.
/// </summary>
/// <param name="response"></param>
/// <param name="filePath"></param>
/// <param name="content"></param>
/// <param name="mimeType"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static HttpListenerResponse WithCompressedFile(this HttpListenerResponse response, string filePath, byte[] content, string mimeType = null)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("filePath must not be null or empty");
if (string.IsNullOrWhiteSpace(mimeType))
mimeType = Path.GetExtension(filePath).GetMimeType();
response.ContentType = mimeType;
response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(filePath)}""");
response.Headers.Add("Content-Encoding", "gzip");
using (var memoryStream = new MemoryStream())
{
using (var compressedStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
compressedStream.Write(content, 0, content.Length);
response.ContentLength64 = memoryStream.Length;
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(response.OutputStream);
response.OutputStream.Dispose();
}
return response;
}
/// <summary>
/// Sets the response content type to a precompressed file and writes the file contents to the response.
/// </summary>
/// <param name="response"></param>
/// <param name="filePath"></param>
/// <param name="content"></param>
/// <param name="mimeType"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static HttpListenerResponse WithPreCompressedFile(this HttpListenerResponse response, string filePath, byte[] content, string mimeType = null)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("filePath must not be null or empty");
if (string.IsNullOrWhiteSpace(mimeType))
mimeType = Path.GetExtension(filePath).GetMimeType();
response.ContentType = mimeType;
response.Headers.Add("Content-Deposition", $@"attachment; filename=""{Path.GetFileName(filePath)}""");
response.Headers.Add("Content-Encoding", "gzip");
response.ContentLength64 = content.Length;
response.OutputStream.Write(content, 0, content.Length);
response.OutputStream.Dispose();
return response;
}
@ -72,15 +343,68 @@ namespace MontoyaTech.Rest.Net
/// <param name="html"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithHtml(this HttpListenerResponse response, string html)
{
response.ContentType = "text/html; charset=utf-8";
var bytes = Encoding.UTF8.GetBytes(html);
response.ContentLength64 = bytes.Length;
response.OutputStream.Write(bytes, 0, bytes.Length);
return response;
}
/// <summary>
/// Sets the response content type to html encoded in utf 16 and writes the given html to it.
/// </summary>
/// <param name="response"></param>
/// <param name="html"></param>
/// <returns>This response.</returns>
public static HttpListenerResponse WithHtml16(this HttpListenerResponse response, string html)
{
response.ContentType = "text/html; charset=utf-16";
var bytes = Encoding.Unicode.GetBytes(html);
response.ContentLength64 = bytes.Length;
response.OutputStream.Write(bytes, 0, bytes.Length);
return response;
}
/// <summary>
/// Sets the response to include the given stream and sets the length and content type if possible.
/// </summary>
/// <param name="response"></param>
/// <param name="stream"></param>
/// <param name="mimeType">If set, sets the content type to this value. If null, and no content type is set, sets it to octet-stream.</param>
/// <returns>This response.</returns>
/// <exception cref="ArgumentNullException"></exception>
public static HttpListenerResponse WithStream(this HttpListenerResponse response, Stream stream, string mimeType = null)
{
if (stream == null)
throw new ArgumentNullException($"{nameof(stream)} cannot be null.");
if (!string.IsNullOrWhiteSpace(mimeType))
response.ContentType = mimeType;
else if (string.IsNullOrWhiteSpace(response.ContentType))
response.ContentType = "application/octet-stream";
try
{
response.ContentLength64 = stream.Length;
}
catch { }
stream.CopyTo(response.OutputStream);
response.OutputStream.Dispose();
return response;
}
/// <summary>
/// Sets the status code for a given response.
/// </summary>
@ -205,7 +529,7 @@ namespace MontoyaTech.Rest.Net
}
/// <summary>
/// Sets a redirect for a given response.
/// Sets the status code for a given response to redirect with the url to redirect to.
/// </summary>
/// <param name="response"></param>
/// <param name="url"></param>
@ -266,5 +590,290 @@ namespace MontoyaTech.Rest.Net
{
return response.WithStatus(HttpStatusCode.BadRequest).WithText($"{fieldName} in request is out of range.");
}
/// <summary>
/// Sets the response to serve a file in the context of a multi page application.
/// </summary>
/// <param name="response">The response to modify</param>
/// <param name="basePath">The base path where to serve files from</param>
/// <param name="request">The request to serve</param>
/// <param name="indexFile">The name of the index file, default is index.html</param>
/// <param name="compress">Whether or not to compress files served. Default is false.</param>
/// <param name="compressExtensions">A collection of file extensions that should be compressed, example: .jpg, default is null. If and compress is true, all files will be compressed.</param>
/// <returns>The modified response</returns>
public static HttpListenerResponse ServeMultiPage(this HttpListenerResponse response, string basePath, string startPath, HttpListenerRequest request, string indexFile = "index.html", bool compress = false, HashSet<string> compressExtensions = null)
{
if (ResolveMultiPagePath(basePath, startPath, request.Url.LocalPath, indexFile, out string resolvedPath, out bool isDirectory))
{
if (isDirectory)
{
return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
}
else
{
if (request.HttpMethod.Equals("head", StringComparison.CurrentCultureIgnoreCase))
{
return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
}
else
{
if (compress && (compressExtensions == null || compressExtensions.Contains(Path.GetExtension(resolvedPath))))
return response.WithStatus(HttpStatusCode.OK).WithCompressedFile(resolvedPath);
else
return response.WithStatus(HttpStatusCode.OK).WithFile(resolvedPath);
}
}
}
else
{
return response.WithStatus(HttpStatusCode.NotFound);
}
}
internal static bool ResolveMultiPagePath(string basePath, string startPath, string requestPath, string indexFile, out string resolvedPath, out bool isDirectory)
{
resolvedPath = null;
isDirectory = false;
//If the requestPath is pointing to nothing change that to the index file.
if (string.IsNullOrWhiteSpace(requestPath) || requestPath == "/" || requestPath == ".")
requestPath = indexFile;
//Break the startPath into it's components
var startComponents = startPath?.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
//Break the request path into it's components so we can enfore staying in the base path.
var requestComponents = requestPath.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
//If we have start components, remove request components that match.
if (startComponents != null)
{
for (int i = 0; i < startComponents.Count; i++)
{
if (requestComponents.Count > 0 && requestComponents[0].Equals(startComponents[i], StringComparison.CurrentCultureIgnoreCase))
requestComponents.RemoveAt(0);
}
}
//Quirk, if the components is now empty, point to the indexFile
if (requestComponents.Count == 0)
requestComponents.Add(indexFile);
//Process the request components and handle directory changes
for (int i = 0; i < requestComponents.Count; i++)
{
if (requestComponents[i].Trim() == "..")
{
requestComponents.RemoveAt(i--);
if (i >= 0)
requestComponents.RemoveAt(i--);
else
return false; //Trying to jump outside of basePath
}
else if (requestComponents[i].Trim() == "...")
{
requestComponents.RemoveAt(i--);
if (i >= 0)
requestComponents.RemoveAt(i--);
else
return false; //Trying to jump outside of basePath
if (i >= 0)
requestComponents.RemoveAt(i--);
else
return false; //Trying to jump outside of basePath
}
else if (requestComponents[i].Trim() == ".")
{
requestComponents.RemoveAt(i--);
}
}
if (requestComponents.Count == 0)
return false;
var absolutePath = Path.Combine(basePath, requestComponents.Separate(Path.DirectorySeparatorChar));
if (File.Exists(absolutePath))
{
resolvedPath = absolutePath;
return true;
}
else if (Directory.Exists(absolutePath))
{
resolvedPath = absolutePath;
isDirectory = true;
return true;
}
else
{
return false;
}
}
/// <summary>
/// Sets the response to serve a file in the context of a single page application.
/// </summary>
/// <param name="response">The response to modify</param>
/// <param name="basePath">The base path where to serve files from</param>
/// <param name="startPath">The starting path that should be removed from requests, if null or empty, requests won't be affected</param>
/// <param name="request">The request to serve</param>
/// <param name="indexFile">The name of the index file, default is index.html</param>
/// <param name="compress">Whether or not to compress files served. Default is false.</param>
/// <param name="compressExtensions">A collection of file extensions that should be compressed, example: .jpg, default is null. If and compress is true, all files will be compressed.</param>
/// <returns>The modified response</returns>
public static HttpListenerResponse ServeSinglePage(this HttpListenerResponse response, string basePath, string startPath, HttpListenerRequest request, string indexFile = "index.html", bool compress = false, HashSet<string> compressExtensions = null)
{
if (ResolveSinglePagePath(basePath, startPath, request.Url.LocalPath, indexFile, out string resolvedPath, out bool isDirectory))
{
if (isDirectory)
{
return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
}
else
{
if (request.HttpMethod.Equals("head", StringComparison.CurrentCultureIgnoreCase))
{
return response.WithNoBody().WithStatus(HttpStatusCode.NoContent);
}
else
{
if (compress && (compressExtensions == null || compressExtensions.Contains(Path.GetExtension(resolvedPath))))
return response.WithStatus(HttpStatusCode.OK).WithCompressedFile(resolvedPath);
else
return response.WithStatus(HttpStatusCode.OK).WithFile(resolvedPath);
}
}
}
else
{
return response.WithStatus(HttpStatusCode.NotFound);
}
}
internal static bool ResolveSinglePagePath(string basePath, string startPath, string requestPath, string indexFile, out string resolvedPath, out bool isDirectory)
{
resolvedPath = null;
isDirectory = false;
//If the requestPath is pointing to nothing change that to the index file.
if (string.IsNullOrWhiteSpace(requestPath) || requestPath == "/" || requestPath == ".")
requestPath = indexFile;
//Break the startPath into it's components
var startComponents = startPath?.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
//Break the request path into it's components so we can enfore staying in the base path.
var requestComponents = requestPath.Split(new char[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries).ToList();
//If we have start components, remove request components that match.
if (startComponents != null)
{
for (int i = 0; i < startComponents.Count; i++)
{
if (requestComponents.Count > 0 && requestComponents[0].Equals(startComponents[i], StringComparison.CurrentCultureIgnoreCase))
requestComponents.RemoveAt(0);
}
}
//Process the request components and handle directory changes
for (int i = 0; i < requestComponents.Count; i++)
{
if (requestComponents[i].Trim() == "..")
{
requestComponents.RemoveAt(i--);
if (i >= 0)
requestComponents.RemoveAt(i--);
else
return false; //Trying to jump outside of basePath
}
else if (requestComponents[i].Trim() == "...")
{
requestComponents.RemoveAt(i--);
if (i >= 0)
requestComponents.RemoveAt(i--);
else
return false; //Trying to jump outside of basePath
if (i >= 0)
requestComponents.RemoveAt(i--);
else
return false; //Trying to jump outside of basePath
}
else if (requestComponents[i].Trim() == ".")
{
requestComponents.RemoveAt(i--);
}
}
//Check the components and remove any that are invalid.
while (requestComponents.Count > 0)
{
string path = Path.Combine(basePath, requestComponents[0]);
if (File.Exists(path) || Directory.Exists(path))
{
break;
}
//If we have no more components and we are missing an extension and with .html a file exists, then use it.
else if (requestComponents.Count == 1 && !requestComponents[0].Contains('.') && File.Exists(path + ".html"))
{
requestComponents[0] = requestComponents[0] + ".html";
break;
}
else
{
requestComponents.RemoveAt(0);
}
}
//Quirk, if the components is now empty, point to the indexFile
if (requestComponents.Count == 0)
requestComponents.Add(indexFile);
//Combine the path into an absolute path
var absolutePath = Path.Combine(basePath, requestComponents.Separate(Path.DirectorySeparatorChar));
//If a file exists, return true
if (File.Exists(absolutePath))
{
resolvedPath = absolutePath;
return true;
}
//If a file exists with adding .html then use that
else if (File.Exists(absolutePath + ".html"))
{
resolvedPath = absolutePath + ".html";
return true;
}
//If a directory exists then use that
else if (Directory.Exists(absolutePath))
{
resolvedPath = absolutePath;
isDirectory = true;
return true;
}
//Otherwise redirect to index.html
else
{
resolvedPath = Path.Combine(basePath, indexFile);
return true;
}
}
}
}

View File

@ -0,0 +1,592 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
internal static class MimeTypeExtensions
{
public static string GetMimeType(this string extension)
{
if (string.IsNullOrWhiteSpace(extension))
return "application/octet-stream";
if (extension.StartsWith("."))
extension = extension.Substring(1);
switch (extension.ToLower())
{
case "323": return "text/h323";
case "3g2": return "video/3gpp2";
case "3gp": return "video/3gpp";
case "3gp2": return "video/3gpp2";
case "3gpp": return "video/3gpp";
case "7z": return "application/x-7z-compressed";
case "aa": return "audio/audible";
case "aac": return "audio/aac";
case "aaf": return "application/octet-stream";
case "aax": return "audio/vnd.audible.aax";
case "ac3": return "audio/ac3";
case "aca": return "application/octet-stream";
case "accda": return "application/msaccess.addin";
case "accdb": return "application/msaccess";
case "accdc": return "application/msaccess.cab";
case "accde": return "application/msaccess";
case "accdr": return "application/msaccess.runtime";
case "accdt": return "application/msaccess";
case "accdw": return "application/msaccess.webapplication";
case "accft": return "application/msaccess.ftemplate";
case "acx": return "application/internet-property-stream";
case "addin": return "text/xml";
case "ade": return "application/msaccess";
case "adobebridge": return "application/x-bridge-url";
case "adp": return "application/msaccess";
case "adt": return "audio/vnd.dlna.adts";
case "adts": return "audio/aac";
case "afm": return "application/octet-stream";
case "ai": return "application/postscript";
case "aif": return "audio/x-aiff";
case "aifc": return "audio/aiff";
case "aiff": return "audio/aiff";
case "air": return "application/vnd.adobe.air-application-installer-package+zip";
case "amc": return "application/x-mpeg";
case "application": return "application/x-ms-application";
case "art": return "image/x-jg";
case "asa": return "application/xml";
case "asax": return "application/xml";
case "ascx": return "application/xml";
case "asd": return "application/octet-stream";
case "asf": return "video/x-ms-asf";
case "ashx": return "application/xml";
case "asi": return "application/octet-stream";
case "asm": return "text/plain";
case "asmx": return "application/xml";
case "aspx": return "application/xml";
case "asr": return "video/x-ms-asf";
case "asx": return "video/x-ms-asf";
case "atom": return "application/atom+xml";
case "au": return "audio/basic";
case "avi": return "video/x-msvideo";
case "axs": return "application/olescript";
case "bas": return "text/plain";
case "bcpio": return "application/x-bcpio";
case "bin": return "application/octet-stream";
case "bmp": return "image/bmp";
case "c": return "text/plain";
case "cab": return "application/octet-stream";
case "caf": return "audio/x-caf";
case "calx": return "application/vnd.ms-office.calx";
case "cat": return "application/vnd.ms-pki.seccat";
case "cc": return "text/plain";
case "cd": return "text/plain";
case "cdda": return "audio/aiff";
case "cdf": return "application/x-cdf";
case "cer": return "application/x-x509-ca-cert";
case "chm": return "application/octet-stream";
case "class": return "application/x-java-applet";
case "clp": return "application/x-msclip";
case "cmx": return "image/x-cmx";
case "cnf": return "text/plain";
case "cod": return "image/cis-cod";
case "config": return "application/xml";
case "contact": return "text/x-ms-contact";
case "coverage": return "application/xml";
case "cpio": return "application/x-cpio";
case "cpp": return "text/plain";
case "crd": return "application/x-mscardfile";
case "crl": return "application/pkix-crl";
case "crt": return "application/x-x509-ca-cert";
case "cs": return "text/plain";
case "csdproj": return "text/plain";
case "csh": return "application/x-csh";
case "csproj": return "text/plain";
case "css": return "text/css";
case "csv": return "text/csv";
case "cur": return "application/octet-stream";
case "cxx": return "text/plain";
case "dat": return "application/octet-stream";
case "datasource": return "application/xml";
case "dbproj": return "text/plain";
case "dcr": return "application/x-director";
case "def": return "text/plain";
case "deploy": return "application/octet-stream";
case "der": return "application/x-x509-ca-cert";
case "dgml": return "application/xml";
case "dib": return "image/bmp";
case "dif": return "video/x-dv";
case "dir": return "application/x-director";
case "disco": return "text/xml";
case "dll": return "application/x-msdownload";
case "dll.config": return "text/xml";
case "dlm": return "text/dlm";
case "doc": return "application/msword";
case "docm": return "application/vnd.ms-word.document.macroenabled.12";
case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "dot": return "application/msword";
case "dotm": return "application/vnd.ms-word.template.macroenabled.12";
case "dotx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.template";
case "dsp": return "application/octet-stream";
case "dsw": return "text/plain";
case "dtd": return "text/xml";
case "dtsconfig": return "text/xml";
case "dv": return "video/x-dv";
case "dvi": return "application/x-dvi";
case "dwf": return "drawing/x-dwf";
case "dwp": return "application/octet-stream";
case "dxr": return "application/x-director";
case "eml": return "message/rfc822";
case "emz": return "application/octet-stream";
case "eot": return "application/octet-stream";
case "eps": return "application/postscript";
case "etl": return "application/etl";
case "etx": return "text/x-setext";
case "evy": return "application/envoy";
case "exe": return "application/octet-stream";
case "exe.config": return "text/xml";
case "fdf": return "application/vnd.fdf";
case "fif": return "application/fractals";
case "filters": return "application/xml";
case "fla": return "application/octet-stream";
case "flr": return "x-world/x-vrml";
case "flv": return "video/x-flv";
case "fas": return "text/xml";
case "signxml": return "text/xml";
case "a2xml": return "text/xml";
case "fsscript": return "application/fsharp-script";
case "fsx": return "application/fsharp-script";
case "generictest": return "application/xml";
case "gif": return "image/gif";
case "group": return "text/x-ms-group";
case "gsm": return "audio/x-gsm";
case "gtar": return "application/x-gtar";
case "gz": return "application/x-gzip";
case "h": return "text/plain";
case "hdf": return "application/x-hdf";
case "hdml": return "text/x-hdml";
case "hhc": return "application/x-oleobject";
case "hhk": return "application/octet-stream";
case "hhp": return "application/octet-stream";
case "hlp": return "application/winhlp";
case "hpp": return "text/plain";
case "hqx": return "application/mac-binhex40";
case "hta": return "application/hta";
case "htc": return "text/x-component";
case "htm": return "text/html";
case "html": return "text/html";
case "htt": return "text/webviewhtml";
case "hxa": return "application/xml";
case "hxc": return "application/xml";
case "hxd": return "application/octet-stream";
case "hxe": return "application/xml";
case "hxf": return "application/xml";
case "hxh": return "application/octet-stream";
case "hxi": return "application/octet-stream";
case "hxk": return "application/xml";
case "hxq": return "application/octet-stream";
case "hxr": return "application/octet-stream";
case "hxs": return "application/octet-stream";
case "hxt": return "text/html";
case "hxv": return "application/xml";
case "hxw": return "application/octet-stream";
case "hxx": return "text/plain";
case "i": return "text/plain";
case "ico": return "image/x-icon";
case "ics": return "application/octet-stream";
case "idl": return "text/plain";
case "ief": return "image/ief";
case "iii": return "application/x-iphone";
case "inc": return "text/plain";
case "inf": return "application/octet-stream";
case "inl": return "text/plain";
case "ins": return "application/x-internet-signup";
case "ipa": return "application/x-itunes-ipa";
case "ipg": return "application/x-itunes-ipg";
case "ipproj": return "text/plain";
case "ipsw": return "application/x-itunes-ipsw";
case "iqy": return "text/x-ms-iqy";
case "isp": return "application/x-internet-signup";
case "ite": return "application/x-itunes-ite";
case "itlp": return "application/x-itunes-itlp";
case "itms": return "application/x-itunes-itms";
case "itpc": return "application/x-itunes-itpc";
case "ivf": return "video/x-ivf";
case "jar": return "application/java-archive";
case "java": return "application/octet-stream";
case "jck": return "application/liquidmotion";
case "jcz": return "application/liquidmotion";
case "jfif": return "image/pjpeg";
case "jnlp": return "application/x-java-jnlp-file";
case "jpb": return "application/octet-stream";
case "jpe": return "image/jpeg";
case "jpeg": return "image/jpeg";
case "jpg": return "image/jpeg";
case "js": return "text/javascript";
case "json": return "application/json";
case "jsx": return "text/jscript";
case "jsxbin": return "text/plain";
case "latex": return "application/x-latex";
case "library-ms": return "application/windows-library+xml";
case "lit": return "application/x-ms-reader";
case "loadtest": return "application/xml";
case "lpk": return "application/octet-stream";
case "lsf": return "video/x-la-asf";
case "lst": return "text/plain";
case "lsx": return "video/x-la-asf";
case "lzh": return "application/octet-stream";
case "m13": return "application/x-msmediaview";
case "m14": return "application/x-msmediaview";
case "m1v": return "video/mpeg";
case "m2t": return "video/vnd.dlna.mpeg-tts";
case "m2ts": return "video/vnd.dlna.mpeg-tts";
case "m2v": return "video/mpeg";
case "m3u": return "audio/x-mpegurl";
case "m3u8": return "audio/x-mpegurl";
case "m4a": return "audio/m4a";
case "m4b": return "audio/m4b";
case "m4p": return "audio/m4p";
case "m4r": return "audio/x-m4r";
case "m4v": return "video/x-m4v";
case "mac": return "image/x-macpaint";
case "mak": return "text/plain";
case "man": return "application/x-troff-man";
case "manifest": return "application/x-ms-manifest";
case "map": return "text/plain";
case "master": return "application/xml";
case "mda": return "application/msaccess";
case "mdb": return "application/x-msaccess";
case "mde": return "application/msaccess";
case "mdp": return "application/octet-stream";
case "me": return "application/x-troff-me";
case "mfp": return "application/x-shockwave-flash";
case "mht": return "message/rfc822";
case "mhtml": return "message/rfc822";
case "mid": return "audio/mid";
case "midi": return "audio/mid";
case "mix": return "application/octet-stream";
case "mk": return "text/plain";
case "mmf": return "application/x-smaf";
case "mno": return "text/xml";
case "mny": return "application/x-msmoney";
case "mod": return "video/mpeg";
case "mov": return "video/quicktime";
case "movie": return "video/x-sgi-movie";
case "mp2": return "video/mpeg";
case "mp2v": return "video/mpeg";
case "mp3": return "audio/mpeg";
case "mp4": return "video/mp4";
case "mp4v": return "video/mp4";
case "mpa": return "video/mpeg";
case "mpe": return "video/mpeg";
case "mpeg": return "video/mpeg";
case "mpf": return "application/vnd.ms-mediapackage";
case "mpg": return "video/mpeg";
case "mpp": return "application/vnd.ms-project";
case "mpv2": return "video/mpeg";
case "mqv": return "video/quicktime";
case "ms": return "application/x-troff-ms";
case "msi": return "application/octet-stream";
case "mso": return "application/octet-stream";
case "mts": return "video/vnd.dlna.mpeg-tts";
case "mtx": return "application/xml";
case "mvb": return "application/x-msmediaview";
case "mvc": return "application/x-miva-compiled";
case "mxp": return "application/x-mmxp";
case "nc": return "application/x-netcdf";
case "nsc": return "video/x-ms-asf";
case "nws": return "message/rfc822";
case "ocx": return "application/octet-stream";
case "oda": return "application/oda";
case "odc": return "text/x-ms-odc";
case "odh": return "text/plain";
case "odl": return "text/plain";
case "odp": return "application/vnd.oasis.opendocument.presentation";
case "ods": return "application/oleobject";
case "odt": return "application/vnd.oasis.opendocument.text";
case "one": return "application/onenote";
case "onea": return "application/onenote";
case "onepkg": return "application/onenote";
case "onetmp": return "application/onenote";
case "onetoc": return "application/onenote";
case "onetoc2": return "application/onenote";
case "orderedtest": return "application/xml";
case "osdx": return "application/opensearchdescription+xml";
case "p10": return "application/pkcs10";
case "p12": return "application/x-pkcs12";
case "p7b": return "application/x-pkcs7-certificates";
case "p7c": return "application/pkcs7-mime";
case "p7m": return "application/pkcs7-mime";
case "p7r": return "application/x-pkcs7-certreqresp";
case "p7s": return "application/pkcs7-signature";
case "pbm": return "image/x-portable-bitmap";
case "pcast": return "application/x-podcast";
case "pct": return "image/pict";
case "pcx": return "application/octet-stream";
case "pcz": return "application/octet-stream";
case "pdf": return "application/pdf";
case "pfb": return "application/octet-stream";
case "pfm": return "application/octet-stream";
case "pfx": return "application/x-pkcs12";
case "pgm": return "image/x-portable-graymap";
case "pic": return "image/pict";
case "pict": return "image/pict";
case "pkgdef": return "text/plain";
case "pkgundef": return "text/plain";
case "pko": return "application/vnd.ms-pki.pko";
case "pls": return "audio/scpls";
case "pma": return "application/x-perfmon";
case "pmc": return "application/x-perfmon";
case "pml": return "application/x-perfmon";
case "pmr": return "application/x-perfmon";
case "pmw": return "application/x-perfmon";
case "png": return "image/png";
case "pnm": return "image/x-portable-anymap";
case "pnt": return "image/x-macpaint";
case "pntg": return "image/x-macpaint";
case "pnz": return "image/png";
case "pot": return "application/vnd.ms-powerpoint";
case "potm": return "application/vnd.ms-powerpoint.template.macroenabled.12";
case "potx": return "application/vnd.openxmlformats-officedocument.presentationml.template";
case "ppa": return "application/vnd.ms-powerpoint";
case "ppam": return "application/vnd.ms-powerpoint.addin.macroenabled.12";
case "ppm": return "image/x-portable-pixmap";
case "pps": return "application/vnd.ms-powerpoint";
case "ppsm": return "application/vnd.ms-powerpoint.slideshow.macroenabled.12";
case "ppsx": return "application/vnd.openxmlformats-officedocument.presentationml.slideshow";
case "ppt": return "application/vnd.ms-powerpoint";
case "pptm": return "application/vnd.ms-powerpoint.presentation.macroenabled.12";
case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
case "prf": return "application/pics-rules";
case "prm": return "application/octet-stream";
case "prx": return "application/octet-stream";
case "ps": return "application/postscript";
case "psc1": return "application/powershell";
case "psd": return "application/octet-stream";
case "psess": return "application/xml";
case "psm": return "application/octet-stream";
case "psp": return "application/octet-stream";
case "pub": return "application/x-mspublisher";
case "pwz": return "application/vnd.ms-powerpoint";
case "qht": return "text/x-html-insertion";
case "qhtm": return "text/x-html-insertion";
case "qt": return "video/quicktime";
case "qti": return "image/x-quicktime";
case "qtif": return "image/x-quicktime";
case "qtl": return "application/x-quicktimeplayer";
case "qxd": return "application/octet-stream";
case "ra": return "audio/x-pn-realaudio";
case "ram": return "audio/x-pn-realaudio";
case "rar": return "application/octet-stream";
case "ras": return "image/x-cmu-raster";
case "rat": return "application/rat-file";
case "rc": return "text/plain";
case "rc2": return "text/plain";
case "rct": return "text/plain";
case "rdlc": return "application/xml";
case "resx": return "application/xml";
case "rf": return "image/vnd.rn-realflash";
case "rgb": return "image/x-rgb";
case "rgs": return "text/plain";
case "rm": return "application/vnd.rn-realmedia";
case "rmi": return "audio/mid";
case "rmp": return "application/vnd.rn-rn_music_package";
case "roff": return "application/x-troff";
case "rpm": return "audio/x-pn-realaudio-plugin";
case "rqy": return "text/x-ms-rqy";
case "rtf": return "application/rtf";
case "rtx": return "text/richtext";
case "ruleset": return "application/xml";
case "s": return "text/plain";
case "safariextz": return "application/x-safari-safariextz";
case "scd": return "application/x-msschedule";
case "sct": return "text/scriptlet";
case "sd2": return "audio/x-sd2";
case "sdp": return "application/sdp";
case "sea": return "application/octet-stream";
case "searchconnector-ms": return "application/windows-search-connector+xml";
case "setpay": return "application/set-payment-initiation";
case "setreg": return "application/set-registration-initiation";
case "settings": return "application/xml";
case "sgimb": return "application/x-sgimb";
case "sgml": return "text/sgml";
case "sh": return "application/x-sh";
case "shar": return "application/x-shar";
case "shtml": return "text/html";
case "sit": return "application/x-stuffit";
case "sitemap": return "application/xml";
case "skin": return "application/xml";
case "sldm": return "application/vnd.ms-powerpoint.slide.macroenabled.12";
case "sldx": return "application/vnd.openxmlformats-officedocument.presentationml.slide";
case "slk": return "application/vnd.ms-excel";
case "sln": return "text/plain";
case "slupkg-ms": return "application/x-ms-license";
case "smd": return "audio/x-smd";
case "smi": return "application/octet-stream";
case "smx": return "audio/x-smd";
case "smz": return "audio/x-smd";
case "snd": return "audio/basic";
case "snippet": return "application/xml";
case "snp": return "application/octet-stream";
case "sol": return "text/plain";
case "sor": return "text/plain";
case "spc": return "application/x-pkcs7-certificates";
case "spl": return "application/futuresplash";
case "src": return "application/x-wais-source";
case "srf": return "text/plain";
case "ssisdeploymentmanifest": return "text/xml";
case "ssm": return "application/streamingmedia";
case "sst": return "application/vnd.ms-pki.certstore";
case "stl": return "application/vnd.ms-pki.stl";
case "sv4cpio": return "application/x-sv4cpio";
case "sv4crc": return "application/x-sv4crc";
case "svc": return "application/xml";
case "svg": return "image/svg+xml";
case "swf": return "application/x-shockwave-flash";
case "t": return "application/x-troff";
case "tar": return "application/x-tar";
case "tcl": return "application/x-tcl";
case "testrunconfig": return "application/xml";
case "testsettings": return "application/xml";
case "tex": return "application/x-tex";
case "texi": return "application/x-texinfo";
case "texinfo": return "application/x-texinfo";
case "tgz": return "application/x-compressed";
case "thmx": return "application/vnd.ms-officetheme";
case "thn": return "application/octet-stream";
case "tif": return "image/tiff";
case "tiff": return "image/tiff";
case "tlh": return "text/plain";
case "tli": return "text/plain";
case "toc": return "application/octet-stream";
case "tr": return "application/x-troff";
case "trm": return "application/x-msterminal";
case "trx": return "application/xml";
case "ts": return "video/vnd.dlna.mpeg-tts";
case "tsv": return "text/tab-separated-values";
case "ttf": return "application/octet-stream";
case "tts": return "video/vnd.dlna.mpeg-tts";
case "txt": return "text/plain";
case "u32": return "application/octet-stream";
case "uls": return "text/iuls";
case "user": return "text/plain";
case "ustar": return "application/x-ustar";
case "vb": return "text/plain";
case "vbdproj": return "text/plain";
case "vbk": return "video/mpeg";
case "vbproj": return "text/plain";
case "vbs": return "text/vbscript";
case "vcf": return "text/x-vcard";
case "vcproj": return "application/xml";
case "vcs": return "text/plain";
case "vcxproj": return "application/xml";
case "vddproj": return "text/plain";
case "vdp": return "text/plain";
case "vdproj": return "text/plain";
case "vdx": return "application/vnd.ms-visio.viewer";
case "vml": return "text/xml";
case "vscontent": return "application/xml";
case "vsct": return "text/xml";
case "vsd": return "application/vnd.visio";
case "vsi": return "application/ms-vsi";
case "vsix": return "application/vsix";
case "vsixlangpack": return "text/xml";
case "vsixmanifest": return "text/xml";
case "vsmdi": return "application/xml";
case "vspscc": return "text/plain";
case "vss": return "application/vnd.visio";
case "vsscc": return "text/plain";
case "vssettings": return "text/xml";
case "vssscc": return "text/plain";
case "vst": return "application/vnd.visio";
case "vstemplate": return "text/xml";
case "vsto": return "application/x-ms-vsto";
case "vsw": return "application/vnd.visio";
case "vsx": return "application/vnd.visio";
case "vtx": return "application/vnd.visio";
case "wav": return "audio/wav";
case "wave": return "audio/wav";
case "wax": return "audio/x-ms-wax";
case "wbk": return "application/msword";
case "wbmp": return "image/vnd.wap.wbmp";
case "wcm": return "application/vnd.ms-works";
case "wdb": return "application/vnd.ms-works";
case "wdp": return "image/vnd.ms-photo";
case "webarchive": return "application/x-safari-webarchive";
case "webtest": return "application/xml";
case "wiq": return "application/xml";
case "wiz": return "application/msword";
case "wks": return "application/vnd.ms-works";
case "wlmp": return "application/wlmoviemaker";
case "wlpginstall": return "application/x-wlpg-detect";
case "wlpginstall3": return "application/x-wlpg3-detect";
case "wm": return "video/x-ms-wm";
case "wma": return "audio/x-ms-wma";
case "wmd": return "application/x-ms-wmd";
case "wmf": return "application/x-msmetafile";
case "wml": return "text/vnd.wap.wml";
case "wmlc": return "application/vnd.wap.wmlc";
case "wmls": return "text/vnd.wap.wmlscript";
case "wmlsc": return "application/vnd.wap.wmlscriptc";
case "wmp": return "video/x-ms-wmp";
case "wmv": return "video/x-ms-wmv";
case "wmx": return "video/x-ms-wmx";
case "wmz": return "application/x-ms-wmz";
case "wpl": return "application/vnd.ms-wpl";
case "wps": return "application/vnd.ms-works";
case "wri": return "application/x-mswrite";
case "wrl": return "x-world/x-vrml";
case "wrz": return "x-world/x-vrml";
case "wsc": return "text/scriptlet";
case "wsdl": return "text/xml";
case "wvx": return "video/x-ms-wvx";
case "webp": return "image/webp";
case "woff": return "application/font-woff";
case "x": return "application/directx";
case "xaf": return "x-world/x-vrml";
case "xaml": return "application/xaml+xml";
case "xap": return "application/x-silverlight-app";
case "xbap": return "application/x-ms-xbap";
case "xbm": return "image/x-xbitmap";
case "xdr": return "text/plain";
case "xht": return "application/xhtml+xml";
case "xhtml": return "application/xhtml+xml";
case "xla": return "application/vnd.ms-excel";
case "xlam": return "application/vnd.ms-excel.addin.macroenabled.12";
case "xlc": return "application/vnd.ms-excel";
case "xld": return "application/vnd.ms-excel";
case "xlk": return "application/vnd.ms-excel";
case "xll": return "application/vnd.ms-excel";
case "xlm": return "application/vnd.ms-excel";
case "xls": return "application/vnd.ms-excel";
case "xlsb": return "application/vnd.ms-excel.sheet.binary.macroenabled.12";
case "xlsm": return "application/vnd.ms-excel.sheet.macroenabled.12";
case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "xlt": return "application/vnd.ms-excel";
case "xltm": return "application/vnd.ms-excel.template.macroenabled.12";
case "xltx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.template";
case "xlw": return "application/vnd.ms-excel";
case "xml": return "text/xml";
case "xmta": return "application/xml";
case "xof": return "x-world/x-vrml";
case "xoml": return "text/plain";
case "xpm": return "image/x-xpixmap";
case "xps": return "application/vnd.ms-xpsdocument";
case "xrm-ms": return "text/xml";
case "xsc": return "application/xml";
case "xsd": return "text/xml";
case "xsf": return "text/xml";
case "xsl": return "text/xml";
case "xslt": return "text/xml";
case "xsn": return "application/octet-stream";
case "xss": return "application/xml";
case "xtp": return "application/octet-stream";
case "xwd": return "image/x-xwindowdump";
case "z": return "application/x-compress";
case "zip": return "application/x-zip-compressed";
default: return "application/octet-stream";
}
}
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
@ -17,9 +17,10 @@
<AssemblyName>MontoyaTech.Rest.Net</AssemblyName>
<RootNamespace>MontoyaTech.Rest.Net</RootNamespace>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Version>1.1.8</Version>
<Version>1.8.9</Version>
<PackageReleaseNotes></PackageReleaseNotes>
<PackageIcon>Logo_Symbol_Black_Outline.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
@ -27,10 +28,18 @@
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="MontoyaTech.Rest.Net.Tests" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,676 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.IO.Pipes;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of a Rest Client Generator that can generate a C# Client.
/// </summary>
public class RestCSharpClientGenerator : RestClientGenerator
{
/// <summary>
/// Whether or not to generate static code, if true the client will be static.
/// </summary>
public bool StaticCode = false;
/// <summary>
/// Generates a CSharp Client from a given set of routes and returns it.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
public override string Generate(List<Route> routes)
{
var includedTypes = this.FindRoutesDependencies(routes);
var routeGroups = this.FindRouteGroups(routes);
var writer = new CodeWriter();
writer.WriteLine($"//Generated using MontoyaTech.Rest.Net - {DateTime.Now.ToShortDateString()}");
writer.WriteBreak().WriteLine($"public class {this.ClientName}").WriteLine("{").Indent();
//Create the base url field
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static string BaseUrl;");
else
writer.WriteBreak().WriteLine("public string BaseUrl;");
//Create the cookie container field
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static System.Net.CookieContainer CookieContainer;");
else
writer.WriteBreak().WriteLine("public System.Net.CookieContainer CookieContainer;");
//Create the client handler field
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static System.Net.Http.HttpMessageHandler MessageHandler;");
else
writer.WriteBreak().WriteLine("public System.Net.Http.HttpMessageHandler MessageHandler;");
//Create the http client field
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static System.Net.Http.HttpClient HttpClient;");
else
writer.WriteBreak().WriteLine("public System.Net.Http.HttpClient HttpClient;");
//Create the request handler field
if (this.StaticCode)
writer.WriteBreak().WriteLine("public static System.Action<System.Net.Http.HttpRequestMessage> RequestHandler;");
else
writer.WriteBreak().WriteLine("public System.Action<System.Net.Http.HttpRequestMessage> RequestHandler;");
//Create fields foreach route group so they can be accessed.
if (!this.StaticCode)
foreach (var group in routeGroups)
writer.WriteBreak().WriteLine($"public {group.Key}Api {group.Key};");
//Create the client constructor or init method
if (this.StaticCode)
{
writer.WriteBreak().WriteLine("public static void Init(string baseUrl, System.Net.Http.HttpMessageHandler messageHandler = null, System.Action<System.Net.Http.HttpRequestMessage> requestHandler = null)").WriteLine("{").Indent();
//Make sure the base url isn't null or whitespace
writer.WriteBreak().WriteLine("if (string.IsNullOrWhiteSpace(baseUrl))");
writer.Indent().WriteLine(@"throw new System.ArgumentException(""baseUrl must not be null or whitespace."");").Outdent();
//If the baseUrl ends with a /, remove it.
writer.WriteBreak().WriteLine("if (baseUrl.EndsWith('/'))");
writer.Indent().WriteLine("baseUrl = baseUrl.Substring(0, baseUrl.Length - 1);").Outdent();
//Init the base url
writer.WriteBreak().WriteLine($"{this.ClientName}.BaseUrl = baseUrl;");
//Init the cookie container
writer.WriteBreak().WriteLine($"{this.ClientName}.CookieContainer = new System.Net.CookieContainer();");
//Init the client handler
writer.WriteBreak().WriteLine("if (messageHandler == null)");
writer.WriteLine("{").Indent();
writer.WriteLine($"messageHandler = new System.Net.Http.HttpClientHandler()");
writer.WriteLine("{").Indent();
writer.WriteLine("AllowAutoRedirect = true,");
writer.WriteLine("UseCookies = true,");
writer.WriteLine($"CookieContainer = {this.ClientName}.CookieContainer,");
writer.WriteLine("AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate");
writer.Outdent().WriteLine("};");
writer.Outdent().WriteLine("}");
//Store the message handler
writer.WriteBreak().WriteLine($"{this.ClientName}.MessageHandler = messageHandler;");
//Store the request handler
writer.WriteBreak().WriteLine($"{this.ClientName}.RequestHandler = requestHandler;");
//Init the http client
writer.WriteBreak().WriteLine($"{this.ClientName}.HttpClient = new System.Net.Http.HttpClient(handler);");
writer.WriteBreak().WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");");
writer.WriteBreak().WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");");
writer.WriteBreak().WriteLine(@$"{this.ClientName}.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");");
writer.Outdent().WriteLine("}");
}
else
{
writer.WriteBreak().WriteLine($"public {this.ClientName}(string baseUrl, System.Net.Http.HttpMessageHandler messageHandler = null, System.Action<System.Net.Http.HttpRequestMessage> requestHandler = null)").WriteLine("{").Indent();
//Make sure the base url isn't null or whitespace
writer.WriteBreak().WriteLine("if (string.IsNullOrWhiteSpace(baseUrl))");
writer.Indent().WriteLine(@"throw new System.ArgumentException(""baseUrl must not be null or whitespace."");").Outdent();
//If the baseUrl ends with a /, remove it.
writer.WriteBreak().WriteLine("if (baseUrl.EndsWith('/'))");
writer.Indent().WriteLine("baseUrl = baseUrl.Substring(0, baseUrl.Length - 1);").Outdent();
//Init the base url
writer.WriteBreak().WriteLine("this.BaseUrl = baseUrl;");
//Init the cookie container
writer.WriteBreak().WriteLine("this.CookieContainer = new System.Net.CookieContainer();");
//Init the client handler
writer.WriteBreak().WriteLine("if (messageHandler == null)");
writer.WriteLine("{").Indent();
writer.WriteLine("messageHandler = new System.Net.Http.HttpClientHandler()");
writer.WriteLine("{").Indent();
writer.WriteLine("AllowAutoRedirect = true,");
writer.WriteLine("UseCookies = true,");
writer.WriteLine("CookieContainer = this.CookieContainer,");
writer.WriteLine("AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate");
writer.Outdent().WriteLine("};");
writer.Outdent().WriteLine("}");
//Store the message handler
writer.WriteBreak().WriteLine("this.MessageHandler = messageHandler;");
//Store the request handler
writer.WriteBreak().WriteLine("this.RequestHandler = requestHandler;");
//Init the http client
writer.WriteBreak().WriteLine("this.HttpClient = new System.Net.Http.HttpClient(handler);");
writer.WriteBreak().WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept"", ""*/*"");");
writer.WriteBreak().WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Connection"", ""keep-alive"");");
writer.WriteBreak().WriteLine(@"this.HttpClient.DefaultRequestHeaders.Add(""Accept-Encoding"", ""identity"");");
//Init all the route group fields
foreach (var group in routeGroups)
writer.WriteBreak().WriteLine($"this.{group.Key} = new {group.Key}Api(this);");
writer.Outdent().WriteLine("}");
}
this.GenerateCSharpRouteGroups(routeGroups, writer);
this.GenerateCSharpIncludedTypes(includedTypes, writer);
writer.Outdent().WriteLine("}");
return writer.ToString();
}
/// <summary>
/// Generates C# for a set of included types.
/// </summary>
/// <param name="types"></param>
/// <param name="writer"></param>
protected internal virtual void GenerateCSharpIncludedTypes(List<Type> types, CodeWriter writer)
{
foreach (var type in types)
{
bool subType = false;
//See if this type belongs to another type in the list.
if (type.DeclaringType != null)
for (int i = 0; i < types.Count && !subType; i++)
if (type.DeclaringType == types[i])
subType = true;
//If not, generate the C# for this type.
if (!subType)
this.GenerateCSharpIncludedType(type, types, writer);
}
}
/// <summary>
/// Generates C# for a given included type.
/// </summary>
/// <param name="type"></param>
/// <param name="writer"></param>
protected internal virtual void GenerateCSharpIncludedType(Type type, List<Type> types, CodeWriter writer)
{
writer.WriteBreak();
var newName = type.GetCustomAttribute<RouteTypeName>();
writer.Write($"public {(type.IsEnum ? "enum" : "class")} {(newName != null ? newName.Name : type.Name)}");
if (type.IsEnum)
writer.Write(" : ").Write(this.GetTypeFullyResolvedName(Enum.GetUnderlyingType(type)));
else if (!(IsTypeDotNet(type.BaseType) && type.BaseType.Name == "Object"))
writer.Write(" : ").Write(this.GetTypeFullyResolvedName(type.BaseType));
writer.NewLine().WriteLine("{").Indent();
FieldInfo[] fields;
if (type.IsEnum)
fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
else
fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
if (fields != null)
foreach (var field in fields)
if (field.IsPublic && !field.IsSpecialName)
this.GenerateCSharpIncludedField(field, writer);
var properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
if (properties != null)
foreach (var property in properties)
if (!property.IsSpecialName && property.GetSetMethod() != null && property.GetGetMethod() != null)
this.GenerateCSharpIncludedProperty(property, writer);
if (!type.IsEnum)
{
//Generate an empty constructor
writer.WriteBreak();
writer.WriteLine($"public {(newName != null ? newName.Name : type.Name)}() {{ }}");
//Generate a constructor to set all the fields/properties from an existing instance
writer.WriteBreak();
writer.WriteLine($"public {(newName != null ? newName.Name : type.Name)}({(newName != null ? newName.Name : type.Name)} instance)");
writer.WriteLine('{').Indent();
foreach (var field in fields)
writer.WriteLine($"this.{field.Name} = instance.{field.Name};");
foreach (var property in properties)
writer.WriteLine($"this.{property.Name} = instance.{property.Name};");
writer.Outdent().WriteLine('}');
//Generate a constructor to set all the fields/properties with optional default values
if (fields.Length > 0 || properties.Length > 0)
{
writer.WriteBreak();
writer.Write($"public {(newName != null ? newName.Name : type.Name)}(");
foreach (var field in fields)
writer.WriteSeparator().Write($"{this.GetTypeFullyResolvedName(field.FieldType)} {field.Name} = {this.GetTypeDefaultValue(field.FieldType)}");
foreach (var property in properties)
writer.WriteSeparator().Write($"{this.GetTypeFullyResolvedName(property.PropertyType)} {property.Name} = {this.GetTypeDefaultValue(property.PropertyType)}");
writer.WriteLine(")");
writer.WriteLine('{').Indent();
foreach (var field in fields)
writer.WriteLine($"this.{field.Name} = {field.Name};");
foreach (var property in properties)
writer.WriteLine($"this.{property.Name} = {property.Name};");
writer.Outdent().WriteLine('}');
}
}
//Generate C# for any types that belong to this one.
for (int i = 0; i < types.Count; i++)
if (types[i].DeclaringType == type)
GenerateCSharpIncludedType(types[i], types, writer);
writer.Outdent().WriteLine("}");
}
/// <summary>
/// Generates C# for a field inside an included type.
/// </summary>
/// <param name="field"></param>
/// <param name="writer"></param>
protected internal virtual void GenerateCSharpIncludedField(FieldInfo field, CodeWriter writer)
{
writer.WriteBreak();
if (field.DeclaringType != null && field.DeclaringType.IsEnum)
writer.WriteLine($"{field.Name} = {field.GetRawConstantValue()},");
else
writer.WriteLine($"public {this.GetTypeFullyResolvedName(field.FieldType)} {field.Name};");
}
/// <summary>
/// Generates C# for a property inside an included type.
/// </summary>
/// <param name="property"></param>
/// <param name="writer"></param>
protected internal virtual void GenerateCSharpIncludedProperty(PropertyInfo property, CodeWriter writer)
{
writer.WriteBreak();
writer.WriteLine($"public {this.GetTypeFullyResolvedName(property.PropertyType)} {property.Name} {{ get; set; }}");
}
/// <summary>
/// Generates C# for a set of route groups.
/// </summary>
/// <param name="groups"></param>
/// <param name="writer"></param>
protected internal virtual void GenerateCSharpRouteGroups(Dictionary<string, List<Route>> groups, CodeWriter writer)
{
foreach (var group in groups)
this.GenerateCSharpRouteGroup(group.Key, group.Value, writer);
}
/// <summary>
/// Generates C# for a given route group.
/// </summary>
/// <param name="name"></param>
/// <param name="routes"></param>
/// <param name="writer"></param>
protected internal virtual void GenerateCSharpRouteGroup(string name, List<Route> routes, CodeWriter writer)
{
writer.WriteBreak();
//Output the class header
if (this.StaticCode)
writer.WriteLine($"public class {name}").WriteLine("{").Indent();
else
writer.WriteLine($"public class {name}Api").WriteLine("{").Indent();
//Output the client instance
if (!this.StaticCode)
writer.WriteBreak().WriteLine($"public {this.ClientName} Client;");
//Output the constuctor if not static.
if (!this.StaticCode)
{
writer.WriteBreak().WriteLine($"public {name}Api({this.ClientName} client)").WriteLine("{").Indent();
writer.WriteLine("this.Client = client;");
writer.Outdent().WriteLine("}");
}
//Output all the route functions.
foreach (var route in routes)
this.GenerateCSharpRouteFunction(route, writer);
writer.Outdent().WriteLine("}");
}
/// <summary>
/// Generates a C# function for a given route.
/// </summary>
/// <param name="route"></param>
/// <param name="writer"></param>
/// <exception cref="NotSupportedException"></exception>
protected internal virtual void GenerateCSharpRouteFunction(Route route, CodeWriter writer)
{
writer.WriteBreak();
var methodInfo = route.GetTarget().GetMethodInfo();
var routeName = methodInfo.GetCustomAttribute<RouteName>();
var routeRequest = methodInfo.GetCustomAttribute<RouteRequest>();
var routeResponse = methodInfo.GetCustomAttribute<RouteResponse>();
//Generate the route function header
if (this.StaticCode)
writer.Write($"public static {(routeResponse == null ? "void" : (routeResponse.Dynamic ? "dynamic" : this.GetTypeFullyResolvedName(routeResponse.ResponseType)))} {(routeName == null ? methodInfo.Name : routeName.Name)}(");
else
writer.Write($"public {(routeResponse == null ? "void" : (routeResponse.Dynamic ? "dynamic" : this.GetTypeFullyResolvedName(routeResponse.ResponseType)))} {(routeName == null ? methodInfo.Name : routeName.Name)}(");
//Generate the functions parameters
var parameters = methodInfo.GetParameters();
if (parameters != null)
{
for (int i = 1; i < parameters.Length; i++)
{
writer.WriteSeparator();
writer.Write(this.GetTypeFullyResolvedName(parameters[i].ParameterType)).Write(" ").Write(parameters[i].Name);
}
}
if (routeRequest != null)
{
writer.WriteSeparator();
if (routeRequest.Dynamic)
writer.Write("dynamic");
else
writer.Write(this.GetTypeFullyResolvedName(routeRequest.RequestType));
writer.Write(" request");
}
if (routeResponse != null && routeResponse.Parameter)
{
writer.WriteSeparator();
if (routeResponse.Dynamic)
writer.Write("dynamic");
else
writer.Write(this.GetTypeFullyResolvedName(routeResponse.ResponseType));
writer.Write(" input");
}
//If JSON, add a compress parameter to control compressing the content.
if (routeRequest != null && routeRequest.Json)
{
writer.WriteSeparator();
writer.Write("bool compress = false");
}
writer.WriteLine(")").WriteLine("{").Indent();
//Generate the message code
writer.WriteBreak().Write($"var message = new System.Net.Http.HttpRequestMessage(");
switch (route.Method.ToLower())
{
case "post":
writer.Write("System.Net.Http.HttpMethod.Post");
break;
case "get":
writer.Write("System.Net.Http.HttpMethod.Get");
break;
case "delete":
writer.Write("System.Net.Http.HttpMethod.Delete");
break;
case "put":
writer.Write("System.Net.Http.HttpMethod.Put");
break;
case "options":
writer.Write("System.Net.Http.HttpMethod.Options");
break;
case "patch":
writer.Write("System.Net.Http.HttpMethod.Patch");
break;
case "head":
writer.Write("System.Net.Http.HttpMethod.Head");
break;
case "trace":
writer.Write("System.Net.Http.HttpMethod.Trace");
break;
default:
throw new NotSupportedException("Unsupport route method:" + route.Method);
}
if (this.StaticCode)
writer.WriteSeparator().Write('$').Write('"').Write($"{{{this.ClientName}.BaseUrl}}");
else
writer.WriteSeparator().Write('$').Write('"').Write("{this.Client.BaseUrl}");
//Reconstruct the route syntax into a request url.
var components = route.Syntax.Split('/');
int argumentIndex = 0;
foreach (var component in components)
{
if (writer.Peek() != '/')
writer.Write('/');
if (!string.IsNullOrWhiteSpace(component))
{
if (component.StartsWith("{"))
{
writer.Write("{").Write(parameters[argumentIndex++ + 1].Name).Write("}");
}
else if (component == "*")
{
writer.Write("*");
}
else if (component == "**")
{
break;
}
else
{
writer.Write(component);
}
}
}
writer.Write('"').WriteLine(");");
//Invoke the request handler if needed.
if (this.StaticCode)
writer.WriteBreak().WriteLine($"{this.ClientName}.RequestHandler?.Invoke(message);");
else
writer.WriteBreak().WriteLine($"this.Client.RequestHandler?.Invoke(message);");
//Add the request content if any.
if (routeRequest != null)
{
if (routeRequest.RequestType.IsAssignableTo(typeof(Stream)))
{
writer.WriteBreak().WriteLine("request.Seek(0, System.IO.SeekOrigin.Begin);");
writer.WriteBreak().WriteLine("message.Content = new System.Net.Http.StreamContent(request);");
}
else if (routeRequest.Json)
{
writer.WriteBreak().WriteLine("if (compress)").WriteLine("{").Indent();
writer.WriteLine("using (var uncompressedStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(request))))");
writer.WriteLine("{").Indent();
writer.WriteLine("using (var compressedStream = new System.IO.MemoryStream())");
writer.WriteLine("{").Indent();
writer.WriteLine("using (var gzipStream = new System.IO.Compression.GZipStream(compressedStream, System.IO.Compression.CompressionMode.Compress, true))");
writer.Indent().WriteLine("uncompressedStream.CopyTo(gzipStream);").Outdent();
writer.WriteBreak();
writer.WriteLine("message.Content = new System.Net.Http.ByteArrayContent(compressedStream.ToArray());");
writer.WriteLine("message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Application.Json);");
writer.WriteLine("message.Content.Headers.ContentEncoding.Add(\"gzip\");");
writer.Outdent().WriteLine("}");
writer.Outdent().WriteLine("}");
writer.Outdent().WriteLine("}");
writer.WriteLine("else").WriteLine("{").Indent();
writer.WriteBreak().WriteLine("message.Content = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request));");
writer.WriteBreak().WriteLine("message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Application.Json);");
writer.Outdent().WriteLine("}");
}
else
{
writer.WriteBreak().WriteLine("message.Content = new System.Net.Http.StringContent(request.ToString());");
}
}
//Generate the response code
if (this.StaticCode)
writer.WriteBreak().WriteLine($"var response = {this.ClientName}.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);");
else
writer.WriteBreak().WriteLine("var response = this.Client.HttpClient.Send(message, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);");
//Handle the response
if (routeResponse != null)
{
writer.WriteBreak().WriteLine("if (response.IsSuccessStatusCode)").WriteLine("{").Indent();
if (routeResponse.ResponseType != null && routeResponse.ResponseType.IsAssignableTo(typeof(Stream)))
{
if (routeResponse.Parameter)
{
writer.WriteBreak().WriteLine("response.Content.CopyToAsync(input).GetAwaiter().GetResult();");
writer.WriteBreak().WriteLine("return input;");
}
else
{
writer.WriteBreak().WriteLine($"var stream = new {this.GetTypeFullyResolvedName(routeResponse.ResponseType)}();");
writer.WriteBreak().WriteLine("response.Content.CopyToAsync(stream).GetAwaiter().GetResult();");
writer.WriteBreak().WriteLine("return stream;");
}
}
else
{
writer.WriteBreak().WriteLine("var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();");
writer.WriteBreak().WriteLine("if (string.IsNullOrEmpty(content))").Indent().WriteLine("return default;").Outdent();
if (routeResponse.Json)
{
writer.WriteBreak().WriteLine($"return Newtonsoft.Json.JsonConvert.DeserializeObject<{(routeResponse.Dynamic ? "dynamic" : this.GetTypeFullyResolvedName(routeResponse.ResponseType))}>(content);");
}
else
{
switch (Type.GetTypeCode(routeResponse.ResponseType))
{
case TypeCode.Boolean:
writer.WriteBreak().WriteLine("return bool.Parse(content);");
break;
case TypeCode.Byte:
writer.WriteBreak().WriteLine("return byte.Parse(content);");
break;
case TypeCode.Char:
writer.WriteBreak().WriteLine("return content[0];");
break;
case TypeCode.DateTime:
writer.WriteBreak().WriteLine("return System.DateTime.Parse(content);");
break;
case TypeCode.Decimal:
writer.WriteBreak().WriteLine("return decimal.Parse(content);");
break;
case TypeCode.Double:
writer.WriteBreak().WriteLine("return double.Parse(content);");
break;
case TypeCode.Int16:
writer.WriteBreak().WriteLine("return short.Parse(content);");
break;
case TypeCode.Int32:
writer.WriteBreak().WriteLine("return int.Parse(content);");
break;
case TypeCode.Int64:
writer.WriteBreak().WriteLine("return long.Parse(content);");
break;
case TypeCode.SByte:
writer.WriteBreak().WriteLine("return sbyte.Parse(content);");
break;
case TypeCode.Single:
writer.WriteBreak().WriteLine("return float.Parse(content);");
break;
case TypeCode.String:
writer.WriteBreak().WriteLine("return content;");
break;
case TypeCode.UInt16:
writer.WriteBreak().WriteLine("return ushort.Parse(content);");
break;
case TypeCode.UInt32:
writer.WriteBreak().WriteLine("return uint.Parse(content);");
break;
case TypeCode.UInt64:
writer.WriteBreak().WriteLine("return ulong.Parse(content);");
break;
case TypeCode.Object:
throw new NotSupportedException("RouteResponse has JSON=false but ResponseType is an object.");
}
}
}
writer.Outdent().WriteLine("}").WriteLine("else").WriteLine("{").Indent();
writer.WriteLine(@"throw new System.Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);");
writer.Outdent().WriteLine("}");
}
else
{
writer.WriteBreak().WriteLine("if (!response.IsSuccessStatusCode)").Indent();
writer.WriteLine(@"throw new System.Exception(""Unexpected Http Response StatusCode:"" + response.StatusCode);").Outdent();
}
//Close off the route function.
writer.Outdent().WriteLine("}");
}
}
}

View File

@ -0,0 +1,414 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.Design;
using System.Data;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// A class that can take a set of routes and generate code
/// for a client that can be used to interact with them.
/// </summary>
public class RestClientGenerator
{
/// <summary>
/// The name of the client to generate.
/// </summary>
public string ClientName = "Client";
/// <summary>
/// Generates a Rest Client from a RouteListener and returns the code.
/// </summary>
/// <returns></returns>
public string Generate(RouteListener listener)
{
return this.Generate(listener.Routes);
}
/// <summary>
/// Generates a Rest Client from a set of routes and returns the code.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
public virtual string Generate(List<Route> routes)
{
throw new NotImplementedException();
}
/// <summary>
/// Returns whether or not a given type belongs to DotNet.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
protected internal virtual bool IsTypeDotNet(Type type)
{
var assemblyName = type.Assembly.GetName().Name;
return assemblyName == "System.Private.CoreLib" || assemblyName == "System.Net.Primitives";
}
/// <summary>
/// Finds all the dependencies for a given type and returns them.
/// </summary>
/// <param name="type"></param>
/// <param name="dependencies"></param>
/// <returns></returns>
protected internal virtual HashSet<Type> FindTypeDependencies(Type type, HashSet<Type> dependencies = null)
{
if (dependencies == null)
dependencies = new HashSet<Type>();
else if (dependencies.Contains(type))
return dependencies;
else if (!this.IsTypeDotNet(type))
dependencies.Add(type);
var arguments = type.GetGenericArguments();
if (arguments != null)
foreach (var argument in arguments)
if (argument != type)
this.FindTypeDependencies(argument, dependencies);
if (this.IsTypeDotNet(type))
return dependencies;
dependencies.Add(type);
if (type.IsEnum)
return dependencies;
this.FindTypeDependencies(type.BaseType, dependencies);
var fields = type.GetFields();
if (fields != null)
foreach (var field in fields)
if (field.IsPublic && !field.IsSpecialName && field.FieldType != type)
this.FindTypeDependencies(field.FieldType, dependencies);
var properties = type.GetProperties();
if (properties != null)
foreach (var property in properties)
if (!property.IsSpecialName && property.GetSetMethod() != null && property.GetGetMethod() != null && property.PropertyType != type)
this.FindTypeDependencies(property.PropertyType, dependencies);
return dependencies;
}
/// <summary>
/// Finds all the types that a given route depends on to function.
/// </summary>
/// <param name="route"></param>
/// <returns></returns>
protected internal virtual List<Type> FindRouteDependencies(Route route)
{
var dependencies = new HashSet<Type>();
var methodInfo = route.GetTarget().GetMethodInfo();
//If this route is hidden, return nothing.
if (methodInfo.GetCustomAttribute<RouteHidden>() != null)
return dependencies.ToList();
if (methodInfo != null)
{
var parameters = methodInfo.GetParameters();
if (parameters != null)
{
for (int i = 1; i < parameters.Length; i++)
{
var types = this.FindTypeDependencies(parameters[i].ParameterType);
foreach (var type in types)
if (!dependencies.Contains(type))
dependencies.Add(type);
}
}
var routeRequest = methodInfo.GetCustomAttribute<RouteRequest>();
if (routeRequest != null && routeRequest.RequestType != null)
{
var types = this.FindTypeDependencies(routeRequest.RequestType);
foreach (var type in types)
if (!dependencies.Contains(type))
dependencies.Add(type);
}
var routeResponse = methodInfo.GetCustomAttribute<RouteResponse>();
if (routeResponse != null && routeResponse.ResponseType != null)
{
var types = this.FindTypeDependencies(routeResponse.ResponseType);
foreach (var type in types)
if (!dependencies.Contains(type))
dependencies.Add(type);
}
var routeIncludes = methodInfo.GetCustomAttributes<RouteInclude>();
if (routeIncludes != null)
{
foreach (var include in routeIncludes)
{
var types = this.FindTypeDependencies(include.Type);
foreach (var type in types)
if (!dependencies.Contains(type))
dependencies.Add(type);
}
}
}
return dependencies.ToList();
}
/// <summary>
/// Finds all the types that a given set of routes depend on to function.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
protected internal virtual List<Type> FindRoutesDependencies(List<Route> routes)
{
var dependencies = new HashSet<Type>();
foreach (var route in routes)
{
var types = this.FindRouteDependencies(route);
if (types != null)
for (int i = 0; i < types.Count; i++)
if (!dependencies.Contains(types[i]))
dependencies.Add(types[i]);
}
return dependencies.ToList();
}
/// <summary>
/// Returns the fully resolve name for a given type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
protected internal virtual string GetTypeFullyResolvedName(Type type)
{
if (this.IsTypeDotNet(type))
{
var typeCode = Type.GetTypeCode(type);
if (typeCode != TypeCode.Object && !type.IsEnum)
{
switch (typeCode)
{
case TypeCode.Boolean:
return "bool";
case TypeCode.Byte:
return "byte";
case TypeCode.Char:
return "char";
case TypeCode.DateTime:
return "System.DateTime";
case TypeCode.Decimal:
return "decimal";
case TypeCode.Double:
return "double";
case TypeCode.Int16:
return "short";
case TypeCode.Int32:
return "int";
case TypeCode.Int64:
return "long";
case TypeCode.SByte:
return "sbyte";
case TypeCode.Single:
return "float";
case TypeCode.String:
return "string";
case TypeCode.UInt16:
return "ushort";
case TypeCode.UInt32:
return "uint";
case TypeCode.UInt64:
return "ulong";
}
}
var builder = new StringBuilder();
builder.Append(type.Namespace);
int genericSymbol = type.Name.IndexOf('`');
if (genericSymbol == -1)
builder.Append(".").Append(type.Name);
else
builder.Append(".").Append(type.Name.Substring(0, genericSymbol));
var genericArguments = type.GetGenericArguments();
if (genericArguments != null && genericArguments.Length > 0)
{
builder.Append("<");
for (int i = 0; i < genericArguments.Length; i++)
{
if (i > 0)
builder.Append(", ");
builder.Append(this.GetTypeFullyResolvedName(genericArguments[i]));
}
builder.Append(">");
}
return builder.ToString();
}
else
{
var builder = new StringBuilder();
var newName = type.GetCustomAttribute<RouteTypeName>();
if (newName != null)
{
builder.Append(newName.Name);
}
else
{
int genericSymbol = type.Name.IndexOf('`');
if (genericSymbol == -1)
builder.Append(type.Name);
else
builder.Append(type.Name.Substring(0, genericSymbol));
}
var genericArguments = type.GetGenericArguments();
if (genericArguments != null && genericArguments.Length > 0)
{
builder.Append("<");
for (int i = 0; i < genericArguments.Length; i++)
{
if (i > 0)
builder.Append(", ");
builder.Append(this.GetTypeFullyResolvedName(genericArguments[i]));
}
builder.Append(">");
}
return builder.ToString();
}
}
/// <summary>
/// Returns the default value for a given type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
protected internal virtual string GetTypeDefaultValue(Type type)
{
var typeCode = Type.GetTypeCode(type);
if (type.IsEnum)
return "0";
else if (typeCode == TypeCode.String || typeCode == TypeCode.Object)
return "null";
else if (typeCode == TypeCode.Char)
return "'\\0'";
else if (typeCode == TypeCode.Boolean)
return "false";
else
return Activator.CreateInstance(type).ToString();
}
/// <summary>
/// Finds all the route groupings from a set of routes and returns those groups.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
protected internal virtual Dictionary<string, List<Route>> FindRouteGroups(List<Route> routes)
{
var groups = new Dictionary<string, List<Route>>();
foreach (var route in routes)
{
var methodInfo = route.GetTarget().GetMethodInfo();
//Skip any routes that are hidden.
if (methodInfo.GetCustomAttribute<RouteHidden>() != null)
continue;
var routeGroup = methodInfo.GetCustomAttribute<RouteGroup>();
//If there wasn't a route group on the method, see if there is one on the declaring type.
if (routeGroup == null)
routeGroup = methodInfo.DeclaringType.GetCustomAttribute<RouteGroup>();
//Group this route.
string group = (routeGroup != null ? routeGroup.Name : methodInfo.DeclaringType.Name);
if (groups.ContainsKey(group))
groups[group].Add(route);
else
groups.Add(group, new List<Route>() { route });
}
return groups;
}
/// <summary>
/// Sorts a list of types by the order of which should be declared first. Useful
/// for languages that require classes to be dependency ordered.
/// </summary>
/// <param name="types"></param>
/// <returns></returns>
protected internal virtual List<Type> SortTypesByDependencies(List<Type> types)
{
var dependencies = new Dictionary<Type, HashSet<Type>>();
//Find all the inner dependencies of the types
foreach (var type in types)
if (!dependencies.ContainsKey(type))
dependencies.Add(type, FindTypeDependencies(type).ToHashSet());
//Now begin sorting these dependencies
var sorted = new LinkedList<KeyValuePair<Type, HashSet<Type>>>();
foreach (var dependency in dependencies)
{
var best = sorted.Last;
var curr = sorted.Last;
while (curr != null)
{
if (dependency.Value.Contains(curr.Value.Key))
break;
else if (curr.Value.Value.Contains(dependency.Key))
best = curr.Previous;
curr = curr.Previous;
}
if (best == null)
sorted.AddFirst(dependency);
else
best.List.AddAfter(best, dependency);
}
return sorted.Select(value => value.Key).ToList();
}
}
}

View File

@ -0,0 +1,833 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of a rest client generator that can generate a javascript client.
/// </summary>
public class RestJavascriptClientGenerator : RestClientGenerator
{
/// <summary>
/// Whether or not to generate static code, if true the client will be static.
/// </summary>
public bool StaticCode = false;
/// <summary>
/// Whether or not to use Json Property names instead of the Field/Property names.
/// </summary>
public bool UseJsonNames = false;
/// <summary>
/// Generates a Javascript Client from a given set of routes and returns it.
/// </summary>
/// <param name="routes"></param>
/// <returns></returns>
public override string Generate(List<Route> routes)
{
var includedTypes = this.FindRoutesDependencies(routes);
var routeGroups = this.FindRouteGroups(routes);
var writer = new CodeWriter();
writer.WriteLine($"//Generated using MontoyaTech.Rest.Net - {DateTime.Now.ToShortDateString()}");
writer.WriteBreak().WriteLine($"class {this.ClientName} {{").Indent();
//Create the base url field
writer.WriteBreak().WriteLine("/** @type {string} */");
writer.WriteAssert(this.StaticCode, "static ").WriteLine("BaseUrl = null;");
//Create the url handler field
writer.WriteBreak().WriteLine("/** @type {Function} */");
writer.WriteAssert(this.StaticCode, "static ").WriteLine("UrlHandler = null;");
//Create the request handler field.
writer.WriteBreak().WriteLine("/** @type {Function} */");
writer.WriteAssert(this.StaticCode, "static ").WriteLine("RequestHandler = null;");
//Create fields foreach route group so they can be accessed.
if (!this.StaticCode)
{
foreach (var group in routeGroups)
{
writer.WriteBreak().WriteLine($"/** @type {{{group.Key}Api}} */");
writer.WriteLine($"{group.Key} = null;");
}
}
//Create the client constructor or init method
if (this.StaticCode)
{
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Initializes this api client with a given baseUrl of where to send requests.");
writer.WriteLine("@param {string} baseUrl Base url of the server to make requests against.");
writer.WriteLine("@param {Function} urlHandler An optional function to process request urls before they are sent. This must return the url.");
writer.WriteLine("@param {Function} requestHandler An optional function to process requests before they are sent. This must return the request.");
writer.Outdent().WriteLine("*/");
writer.Write("static Init(baseUrl, urlHandler = null, requestHandler = null) ").WriteLine("{").Indent();
//Make sure the baseUrl isn't null or whitespace
writer.WriteBreak().WriteLine("if (baseUrl == null || baseUrl == undefined || baseUrl.trim() == '') {").Indent();
writer.WriteLine("throw 'baseUrl cannot be null or empty.';");
writer.Outdent().WriteLine("}");
//If the baseUrl ends with a /, remove it.
writer.WriteBreak().WriteLine("if (baseUrl.endsWith('/')) {").Indent();
writer.WriteLine("baseUrl = baseUrl.substring(0, baseUrl.length - 1);");
writer.Outdent().WriteLine("}");
//Store the baseUrl
writer.WriteBreak().WriteLine($"{this.ClientName}.BaseUrl = baseUrl;");
//Store the urlHandler
writer.WriteBreak().WriteLine($"{this.ClientName}.UrlHandler = urlHandler;");
//Store the requestHandler
writer.WriteBreak().WriteLine($"{this.ClientName}.RequestHandler = requestHandler;");
writer.Outdent().WriteLine("}");
}
else
{
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Initializes this api client with a given baseUrl of where to send requests.");
writer.WriteLine("@param {string} baseUrl Base url of the server to make requests against.");
writer.WriteLine("@param {Function} urlHandler An optional function to process request urls before they are sent. This must return the url.");
writer.WriteLine("@param {Function} requestHandler An optional function to process requests before they are sent. This must return the request.");
writer.Outdent().WriteLine("*/");
writer.Write("constructor(baseUrl, urlHandler = null, requestHandler = null) ").WriteLine("{").Indent();
//Make sure the baseUrl isn't null or whitespace
writer.WriteBreak().WriteLine("if (baseUrl == null || baseUrl == undefined || baseUrl.trim() == '') {").Indent();
writer.WriteLine("throw 'baseUrl cannot be null or empty.';");
writer.Outdent().WriteLine("}");
//If the baseUrl ends with a /, remove it.
writer.WriteBreak().WriteLine("if (baseUrl.endsWith('/')) {").Indent();
writer.WriteLine("baseUrl = baseUrl.substring(0, baseUrl.length - 1);");
writer.Outdent().WriteLine("}");
//Store the baseUrl
writer.WriteBreak().WriteLine("this.BaseUrl = baseUrl;");
//Store the urlHandler
writer.WriteBreak().WriteLine($"this.UrlHandler = urlHandler;");
//Store the request handler
writer.WriteBreak().WriteLine("this.RequestHandler = requestHandler;");
//Init all the route group fields
writer.WriteBreak();
foreach (var group in routeGroups)
writer.WriteLine($"this.{group.Key} = new {group.Key}Api(this);");
writer.Outdent().WriteLine("}");
}
writer.Outdent().WriteLine("}");
this.GenerateJavascriptRouteGroups(routeGroups, writer);
includedTypes = this.SortTypesByDependencies(includedTypes);
this.GenerateJavascriptIncludedTypes(includedTypes, writer);
writer.WriteBreak().WriteLine($"window.{this.ClientName} = {this.ClientName};");
//Export the Client and all the Types
writer.WriteBreak().WriteLine("export {").Indent();
writer.WriteLine($"{this.ClientName},");
foreach (var type in includedTypes)
{
var newName = type.GetCustomAttribute<RouteTypeName>();
writer.WriteLine($"{(type.DeclaringType != null ? type.DeclaringType.Name : "")}{(newName != null ? newName.Name : type.Name)},");
}
writer.Outdent().WriteLine("};");
return writer.ToString();
}
protected internal override string GetTypeFullyResolvedName(Type type)
{
var typeCode = Type.GetTypeCode(type);
if (typeof(Array).IsAssignableFrom(type))
{
return $"Array<{this.GetTypeFullyResolvedName(type.GetElementType())}>";
}
else if (typeof(IList).IsAssignableFrom(type))
{
var builder = new StringBuilder();
builder.Append("Array");
var genericArguments = type.GetGenericArguments();
if (genericArguments != null && genericArguments.Length > 0)
{
builder.Append("<");
for (int i = 0; i < genericArguments.Length; i++)
{
if (i > 0)
builder.Append(", ");
builder.Append(this.GetTypeFullyResolvedName(genericArguments[i]));
}
builder.Append(">");
}
return builder.ToString();
}
else if (type.IsAssignableTo(typeof(Stream)))
{
return "Blob";
}
else if (typeCode == TypeCode.String || typeCode == TypeCode.Char)
{
return "string";
}
else if (typeCode == TypeCode.Boolean)
{
return "boolean";
}
else if (typeCode == TypeCode.DBNull)
{
return "object";
}
else if (typeCode == TypeCode.DateTime)
{
return "string";
}
else if (typeCode == TypeCode.Object || type.IsEnum)
{
if (type.DeclaringType != null && !IsTypeDotNet(type.DeclaringType))
return $"{this.GetTypeFullyResolvedName(type.DeclaringType)}{base.GetTypeFullyResolvedName(type)}";
else
return base.GetTypeFullyResolvedName(type);
}
else
{
return "number";
}
}
protected internal override string GetTypeDefaultValue(Type type)
{
var typeCode = Type.GetTypeCode(type);
if (typeCode == TypeCode.Char || typeCode == TypeCode.DateTime)
return "null";
return base.GetTypeDefaultValue(type);
}
protected internal string EscapeName(string name)
{
if (name != null)
{
switch (name)
{
case "arguments":
case "await":
case "break":
case "case":
case "catch":
case "class":
case "const":
case "continue":
case "debugger":
case "default":
case "delete":
case "do":
case "double":
case "else":
case "enum":
case "eval":
case "export":
case "extends":
case "false":
case "finally":
case "for":
case "function":
case "goto":
case "if":
case "import":
case "in":
case "instanceof":
case "interface":
case "let":
case "new":
case "null":
case "return":
case "static":
case "super":
case "switch":
case "this":
case "throw":
case "true":
case "try":
case "typeof":
case "var":
case "void":
case "while":
case "with":
case "yield":
return "_" + name;
}
}
return name;
}
protected internal virtual void GenerateJavascriptIncludedTypes(List<Type> types, CodeWriter writer)
{
foreach (var type in types)
{
bool subType = false;
//See if this type belongs to another type in the list.
if (type.DeclaringType != null)
for (int i = 0; i < types.Count && !subType; i++)
if (type.DeclaringType == types[i])
subType = true;
//If not, generate the code for this type.
if (!subType)
this.GenerateJavascriptIncludedType(type, types, writer);
}
}
protected internal virtual void GenerateJavascriptIncludedType(Type type, List<Type> types, CodeWriter writer)
{
writer.WriteBreak();
var newName = type.GetCustomAttribute<RouteTypeName>();
writer.Write($"class {(type.DeclaringType != null ? type.DeclaringType.Name : "")}{(newName != null ? newName.Name : type.Name)}");
if (!type.IsEnum && !(IsTypeDotNet(type.BaseType) && type.BaseType.Name == "Object"))
writer.Write(" extends ").Write(this.GetTypeFullyResolvedName(type.BaseType));
writer.WriteLine(" {").Indent();
FieldInfo[] fields;
if (type.IsEnum)
fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
else
fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
foreach (var field in fields)
this.GenerateJavascriptIncludedField(field, writer);
var properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public).Where(property => property.GetSetMethod() != null && property.GetGetMethod() != null).ToArray();
foreach (var property in properties)
this.GenerateJavascriptIncludedProperty(property, writer);
//Generate a helper constructor
if (!type.IsEnum)
{
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("@function");
//Docuemnt the fields
foreach (var field in fields)
{
if (this.UseJsonNames)
writer.WriteLine($"@param {{{this.GetTypeFullyResolvedName(field.FieldType)}}} {EscapeName(field.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name)}");
else
writer.WriteLine($"@param {{{this.GetTypeFullyResolvedName(field.FieldType)}}} {EscapeName(field.Name)}");
}
//Document the properties
foreach (var property in properties)
{
if (this.UseJsonNames)
writer.WriteLine($"@param {{{this.GetTypeFullyResolvedName(property.PropertyType)}}} {EscapeName(property.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? property.Name)}");
else
writer.WriteLine($"@param {{{this.GetTypeFullyResolvedName(property.PropertyType)}}} {EscapeName(property.Name)}");
}
writer.Outdent().WriteLine("*/");
writer.Write("constructor(");
//Write the default fields
foreach (var field in fields)
{
if (this.UseJsonNames)
writer.WriteSeparator().Write(EscapeName(field.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name)).Write(" = ").Write(this.GetTypeDefaultValue(field.FieldType));
else
writer.WriteSeparator().Write(EscapeName(field.Name)).Write(" = ").Write(this.GetTypeDefaultValue(field.FieldType));
}
//Write the default properties
foreach (var property in properties)
{
if (this.UseJsonNames)
writer.WriteSeparator().Write(EscapeName(property.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? property.Name)).Write(" = ").Write(this.GetTypeDefaultValue(property.PropertyType));
else
writer.WriteSeparator().Write(EscapeName(property.Name)).Write(" = ").Write(this.GetTypeDefaultValue(property.PropertyType));
}
writer.WriteLine(") {").Indent();
//Init the default fields
foreach (var field in fields)
{
if (this.UseJsonNames)
writer.Write("this.").Write(field.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name).Write(" = ").Write(EscapeName(field.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name)).WriteLine(";");
else
writer.Write("this.").Write(field.Name).Write(" = ").Write(EscapeName(field.Name)).WriteLine(";");
}
//Init the default properties
foreach (var property in properties)
{
if (this.UseJsonNames)
writer.Write("this.").Write(property.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? property.Name).Write(" = ").Write(EscapeName(property.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? property.Name)).WriteLine(";");
else
writer.Write("this.").Write(property.Name).Write(" = ").Write(EscapeName(property.Name)).WriteLine(";");
}
writer.Outdent().WriteLine("}");
}
//If this is an enum, generate a GetNames/GetValues/GetName function.
if (type.IsEnum)
{
var names = Enum.GetNames(type);
var values = Enum.GetValues(type);
//GetName function
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Returns the name of a value in this enum. Returns null if the value is invalid.");
writer.WriteLine("@function");
writer.WriteLine("@param {number} value The value to get the name of in this enum.");
writer.WriteLine("@returns {string} The name for the given value.");
writer.Outdent().WriteLine("*/");
writer.WriteLine("static GetName(value) {").Indent();
writer.WriteLine("switch (value) {").Indent();
for (int i = 0; i < values.Length; i++)
writer.WriteLine($"case {Convert.ToInt32(values.GetValue(i))}: return `{names.GetValue(i)}`;");
writer.Outdent().WriteLine("}");
writer.WriteLine("return null;");
writer.Outdent().WriteLine("}");
//GetValue function
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Returns the value for a name in this enum. Returns null if the name is invalid.");
writer.WriteLine("@function");
writer.WriteLine("@param {string} name The name of the item in this enum to get the value of.");
writer.WriteLine("@returns {number} The value associated with this name in this enum.");
writer.Outdent().WriteLine("*/");
writer.WriteLine("static GetValue(name) {").Indent();
writer.WriteLine("if (!name) {").Indent();
writer.WriteLine("return null;");
writer.Outdent().WriteLine("}");
writer.WriteLine("switch (name.toLowerCase().trim()) {").Indent();
for (int i = 0; i < names.Length; i++)
writer.WriteLine($"case '{names.GetValue(i).ToString().ToLower()}': return {Convert.ToInt32(values.GetValue(i))};");
writer.Outdent().WriteLine("}");
writer.WriteLine("return null;");
writer.Outdent().WriteLine("}");
//GetNames function
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Returns the names of this enums values as an array of strings.");
writer.WriteLine("@function");
writer.WriteLine("@returns {Array<string>}");
writer.Outdent().WriteLine("*/");
writer.WriteLine("static GetNames() {").Indent();
writer.WriteLine("return [").Indent();
foreach (var name in names)
writer.WriteLine($"'{name}',");
writer.Outdent().WriteLine("];");
writer.Outdent().WriteLine("}");
//GetValues function
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Returns the values of this enum as an arrray of numbers.");
writer.WriteLine("@function");
writer.WriteLine("@returns {Array<number>}");
writer.Outdent().WriteLine("*/");
writer.WriteLine("static GetValues() {").Indent();
writer.WriteLine("return [").Indent();
foreach (var value in values)
writer.WriteLine($"{Convert.ToInt32(value)},");
writer.Outdent().WriteLine("];");
writer.Outdent().WriteLine("}");
writer.WriteBreak().WriteLine("/**").Indent();
writer.WriteLine("Returns the names and values of this enum in an array.");
writer.WriteLine("@function");
writer.WriteLine("@returns {Array<object>} Where each element is an object and has a name and value field. Ex: { name: '', value: 0 }");
writer.Outdent().WriteLine("*/");
writer.WriteLine("static GetNamesValues() {").Indent();
writer.WriteLine("return [").Indent();
for (int i = 0; i < names.Length; i++)
writer.WriteLine($"{{ name: `{names[i]}`, value: {Convert.ToInt32(values.GetValue(i))} }},");
writer.Outdent().WriteLine("];");
writer.Outdent().WriteLine("}");
}
//Close off the class
writer.Outdent().WriteLine("}");
writer.WriteBreak().WriteLine($"{this.ClientName}.{(type.DeclaringType != null ? type.DeclaringType.Name : "")}{(newName != null ? newName.Name : type.Name)} = {(type.DeclaringType != null ? type.DeclaringType.Name : "")}{(newName != null ? newName.Name : type.Name)};");
//Generate any types that belong to this.
for (int i = 0; i < types.Count; i++)
if (types[i].DeclaringType == type)
GenerateJavascriptIncludedType(types[i], types, writer);
}
protected internal virtual void GenerateJavascriptIncludedField(FieldInfo field, CodeWriter writer)
{
writer.WriteBreak();
if (field.DeclaringType != null && field.DeclaringType.IsEnum)
{
writer.WriteLine($"/** @type {{{GetTypeFullyResolvedName(field.DeclaringType)}}} */");
if (this.UseJsonNames)
writer.WriteLine($"static {(field.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name)} = {field.GetRawConstantValue()};");
else
writer.WriteLine($"static {field.Name} = {field.GetRawConstantValue()};");
}
else
{
writer.WriteLine($"/** @type {{{GetTypeFullyResolvedName(field.FieldType)}}} */");
if (this.UseJsonNames)
writer.WriteLine($"{(field.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? field.Name)} = {GetTypeDefaultValue(field.FieldType)};");
else
writer.WriteLine($"{field.Name} = {GetTypeDefaultValue(field.FieldType)};");
}
}
protected internal virtual void GenerateJavascriptIncludedProperty(PropertyInfo property, CodeWriter writer)
{
writer.WriteBreak();
writer.WriteLine($"/** @type {{{GetTypeFullyResolvedName(property.PropertyType)}}} */");
if (this.UseJsonNames)
writer.WriteLine($"{(property.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>()?.PropertyName ?? property.Name)} = {GetTypeDefaultValue(property.PropertyType)};");
else
writer.WriteLine($"{property.Name} = {GetTypeDefaultValue(property.PropertyType)};");
}
protected internal virtual void GenerateJavascriptRouteGroups(Dictionary<string, List<Route>> groups, CodeWriter writer)
{
foreach (var group in groups)
this.GenerateJavascriptRouteGroup(group.Key, group.Value, writer);
}
protected internal virtual void GenerateJavascriptRouteGroup(string name, List<Route> routes, CodeWriter writer)
{
writer.WriteBreak();
//Output the class header
if (this.StaticCode)
writer.WriteLine($"class {name} {{").Indent();
else
writer.WriteLine($"class {name}Api {{").Indent();
//Output the client instance
if (!this.StaticCode)
{
writer.WriteBreak().WriteLine($"/** @type {{{this.ClientName}}} */");
writer.WriteLine("Client = null;");
}
//Output the constuctor if not static.
if (!this.StaticCode)
{
writer.WriteBreak();
writer.WriteLine("/**").Indent();
writer.WriteLine("@function");
writer.WriteLine($"@param {{{this.ClientName}}} client");
writer.Outdent().WriteLine("*/");
writer.Write($"constructor(client) ").WriteLine("{").Indent();
writer.WriteLine("this.Client = client;");
writer.Outdent().WriteLine("}");
}
//Output all the route functions.
foreach (var route in routes)
this.GenerateJavascriptRouteFunction(route, writer);
writer.Outdent().WriteLine("}");
//Expose this route group on the client.
if (this.StaticCode)
writer.WriteBreak().WriteLine($"{this.ClientName}.{name} = {name};");
else
writer.WriteBreak().WriteLine($"{this.ClientName}.{name}Api = {name}Api;");
}
protected internal virtual void GenerateJavascriptRouteFunction(Route route, CodeWriter writer)
{
writer.WriteBreak();
var methodInfo = route.GetTarget().GetMethodInfo();
var routeName = methodInfo.GetCustomAttribute<RouteName>();
var routeRequest = methodInfo.GetCustomAttribute<RouteRequest>();
var routeResponse = methodInfo.GetCustomAttribute<RouteResponse>();
var parameters = methodInfo.GetParameters();
//Generate the function jsdoc tags
writer.WriteLine("/**").Indent();
writer.WriteLine("@function");
writer.WriteLine("@async");
writer.WriteLine($"@name {(routeName == null ? methodInfo.Name : routeName.Name)}");
//Generate parameter docs
if (parameters != null)
for (int i = 1; i < parameters.Length; i++)
writer.WriteLine($"@param {{{this.GetTypeFullyResolvedName(parameters[i].ParameterType)}}} {parameters[i].Name}");
//Generate request doc if any
if (routeRequest != null)
writer.WriteLine($"@param {{{(routeRequest.Dynamic ? "Any" : this.GetTypeFullyResolvedName(routeRequest.RequestType))}}} body");
//Generate response doc if any
if (routeResponse != null)
writer.WriteLine($"@returns {{{(routeResponse.Dynamic ? "Any" : this.GetTypeFullyResolvedName(routeResponse.ResponseType))}}}");
writer.WriteLine("@throws {Response} If response status was not ok.");
writer.Outdent().WriteLine("*/");
//Generate the route function header
if (this.StaticCode)
writer.Write($"static async {(routeName == null ? methodInfo.Name : routeName.Name)}(");
else
writer.Write($"async {(routeName == null ? methodInfo.Name : routeName.Name)}(");
//Generate the functions parameters
if (parameters != null)
{
for (int i = 1; i < parameters.Length; i++)
{
writer.WriteSeparator();
writer.Write(parameters[i].Name);
}
}
if (routeRequest != null)
{
writer.WriteSeparator();
writer.Write("body");
}
if (routeResponse != null && routeResponse.Parameter)
{
writer.WriteSeparator();
writer.Write("input");
}
writer.WriteLine(") {").Indent();
//Generate the url
writer.WriteBreak().Write("var url = ");
//Generate the request url
if (this.StaticCode)
writer.Write('`').Write($"${{{this.ClientName}.BaseUrl}}");
else
writer.Write('`').Write("${this.Client.BaseUrl}");
//Reconstruct the route syntax into a request url.
var components = route.Syntax.Split('/');
int argumentIndex = 0;
foreach (var component in components)
{
if (writer.Peek() != '/')
writer.Write('/');
if (!string.IsNullOrWhiteSpace(component))
{
if (component.StartsWith("{"))
{
writer.Write("${").Write(parameters[argumentIndex++ + 1].Name).Write("}");
}
else if (component == "*")
{
writer.Write("*");
}
else if (component == "**")
{
break;
}
else
{
writer.Write(component);
}
}
}
writer.WriteLine("`;");
//Generate the request
writer.WriteBreak().WriteLine("var request = {").Indent();
//Include credentials
writer.WriteLine("credentials: 'include',");
//Generate the method
writer.WriteLine($"method: '{route.Method.ToLower()}',");
//If we have a body, generate that
if (routeRequest != null)
{
if (routeRequest.RequestType.IsAssignableTo(typeof(Stream)))
{
writer.WriteLine("headers: new Headers({ 'Content-Type': 'application/octet-stream' }),");
writer.WriteLine("body: body,");
}
else if (routeRequest.Json)
{
writer.WriteLine("body: JSON.stringify(body),");
}
else
{
writer.WriteLine("body: body.toString(),");
}
}
writer.Outdent().WriteLine("};");
//Generate the response
writer.WriteBreak().Write("var response = await fetch(");
if (this.StaticCode)
writer.Write($"{this.ClientName}.UrlHandler ? {this.ClientName}.UrlHandler(url) : url");
else
writer.Write($"this.Client.UrlHandler ? this.Client.UrlHandler(url) : url");
writer.WriteSeparator();
if (this.StaticCode)
writer.Write($"{this.ClientName}.RequestHandler ? {this.ClientName}.RequestHandler(request) : request");
else
writer.Write("this.Client.RequestHandler ? this.Client.RequestHandler(request) : request");
writer.WriteLine(");");
//Generate code to handle the response
if (routeResponse != null)
{
writer.WriteBreak().WriteLine("if (response.ok) {").Indent();
if (routeResponse.ResponseType != null && routeResponse.ResponseType.IsAssignableTo(typeof(Stream)))
{
if (routeResponse.Parameter)
{
//TODO
}
else
{
//TODO
}
}
else
{
if (routeResponse.Json)
{
writer.WriteBreak().WriteLine("return await response.json();");
}
else
{
switch (Type.GetTypeCode(routeResponse.ResponseType))
{
case TypeCode.Boolean:
writer.WriteBreak().WriteLine("return (await(response.text())).toLowerCase() === 'true';");
break;
case TypeCode.Char:
writer.WriteBreak().WriteLine("return (await(response.text()))[0];");
break;
case TypeCode.DateTime:
writer.WriteBreak().WriteLine("return Date.parse((await(response.text())));");
break;
case TypeCode.String:
writer.WriteBreak().WriteLine("return await response.text();");
break;
case TypeCode.Byte:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.SByte:
case TypeCode.Single:
case TypeCode.UInt16:
case TypeCode.UInt32:
case TypeCode.UInt64:
writer.WriteBreak().WriteLine("return Number((await(response.text())));");
break;
case TypeCode.Object:
throw new NotSupportedException("ResponseType isn't JSON but is an object.");
}
}
}
writer.Outdent().WriteLine("}");
writer.WriteBreak().WriteLine("throw response;");
}
else
{
writer.WriteBreak().WriteLine("if (!response.ok) {").Indent();
writer.WriteLine("throw response;");
writer.Outdent().WriteLine("}");
}
//Close off the route function.
writer.Outdent().WriteLine("}");
}
}
}

View File

@ -28,7 +28,7 @@ namespace MontoyaTech.Rest.Net
private Func<HttpListenerContext, HttpListenerResponse> Target;
/// <summary>
/// Whether or not to close the response after the route is invoked.
/// Whether or not to close the response after the route is invoked, default is true.
/// </summary>
public bool CloseResponse = true;
@ -38,15 +38,21 @@ namespace MontoyaTech.Rest.Net
internal Route() { }
/// <summary>
/// Creates a new route with a given method, syntax, target and optional close response flag.
/// Creates a new route with a given method, syntax, target and optional close response flag which defaults to true.
/// </summary>
/// <param name="name"></param>
/// <param name="method"></param>
/// <param name="syntax"></param>
/// <param name="target"></param>
/// <param name="closeResponse"></param>
public Route(string method, string syntax, Func<HttpListenerContext, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Method cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Syntax cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -56,7 +62,6 @@ namespace MontoyaTech.Rest.Net
/// <summary>
/// Creates a new route with a given method, syntax, target and optional close response flag.
/// </summary>
/// <param name="name"></param>
/// <param name="method"></param>
/// <param name="syntax"></param>
/// <param name="target"></param>
@ -117,6 +122,15 @@ namespace MontoyaTech.Rest.Net
{
this.Target.Invoke(context);
}
/// <summary>
/// Returns the target delegate for this route.
/// </summary>
/// <returns></returns>
public virtual Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1> : Route
@ -125,6 +139,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -138,6 +159,11 @@ namespace MontoyaTech.Rest.Net
{
this.Target.DynamicInvoke(context, RouteArgumentConverter.Convert<T1>(arguments[0]));
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2> : Route
@ -146,6 +172,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -163,6 +196,11 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T2>(arguments[1])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2, T3> : Route
@ -171,6 +209,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, T3, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -189,6 +234,11 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T3>(arguments[2])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2, T3, T4> : Route
@ -197,6 +247,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, T3, T4, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -216,6 +273,11 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T4>(arguments[3])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2, T3, T4, T5> : Route
@ -224,6 +286,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, T3, T4, T5, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -244,6 +313,11 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T5>(arguments[4])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2, T3, T4, T5, T6> : Route
@ -252,6 +326,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, T3, T4, T5, T6, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -273,6 +354,11 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T6>(arguments[5])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2, T3, T4, T5, T6, T7> : Route
@ -281,6 +367,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, T3, T4, T5, T6, T7, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -303,6 +396,11 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T7>(arguments[6])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
public class Route<T1, T2, T3, T4, T5, T6, T7, T8> : Route
@ -311,6 +409,13 @@ namespace MontoyaTech.Rest.Net
public Route(string method, string syntax, Func<HttpListenerContext, T1, T2, T3, T4, T5, T6, T7, T8, HttpListenerResponse> target, bool closeResponse = true)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
else if (string.IsNullOrWhiteSpace(method))
throw new ArgumentException("Cannot be null or empty", nameof(method));
else if (string.IsNullOrWhiteSpace(syntax))
throw new ArgumentException("Cannot be null or empty", nameof(syntax));
this.Method = method;
this.Syntax = syntax;
this.Target = target;
@ -334,5 +439,10 @@ namespace MontoyaTech.Rest.Net
RouteArgumentConverter.Convert<T8>(arguments[7])
);
}
public override Delegate GetTarget()
{
return this.Target;
}
}
}

View File

@ -19,96 +19,130 @@ namespace MontoyaTech.Rest.Net
/// <returns></returns>
public static T Convert<T>(string input)
{
var typeCode = Type.GetTypeCode(typeof(T));
var result = Convert(typeof(T), input);
if (typeCode == TypeCode.String)
if (result == null)
return default(T);
return (T)result;
}
/// <summary>
/// Converts a string to a given type if possible. Otherwise returns null.
/// </summary>
/// <param name="type"></param>
/// <param name="input"></param>
/// <returns></returns>
public static object Convert(Type type, string input)
{
return (dynamic)input;
var typeCode = Type.GetTypeCode(type);
if (type.IsEnum)
{
return Enum.Parse(type, input, true);
}
else if (typeCode == TypeCode.String)
{
return input;
}
else if (typeCode == TypeCode.Object)
{
var castOperator = typeof(T).GetMethod("op_Explicit", new[] { typeof(string) });
var castOperator = type.GetMethod("op_Explicit", new[] { typeof(string) });
if (castOperator == null)
return default(T);
return null;
return (T)castOperator.Invoke(null, new[] { input });
return castOperator.Invoke(null, new[] { input });
}
else if (typeCode == TypeCode.Double)
{
double.TryParse(input, out double result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Single)
{
float.TryParse(input, out float result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Decimal)
{
decimal.TryParse(input, out decimal result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Int64)
{
long.TryParse(input, out long result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Int32)
{
int.TryParse(input, out int result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Int16)
{
short.TryParse(input, out short result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.SByte)
{
sbyte.TryParse(input, out sbyte result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.UInt64)
{
ulong.TryParse(input, out ulong result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.UInt32)
{
uint.TryParse(input, out uint result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.UInt16)
{
ushort.TryParse(input, out ushort result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Byte)
{
byte.TryParse(input, out byte result);
return (dynamic)result;
return result;
}
else if (typeCode == TypeCode.Boolean)
{
if (input == "f" || input == "0" || input == "F")
return (dynamic)false;
return false;
bool.TryParse(input, out bool result);
return ((dynamic)result);
return result;
}
else if (typeCode == TypeCode.DateTime)
{
DateTime.TryParse(input, out DateTime result);
return ((dynamic)result);
return result;
}
else if (typeCode == TypeCode.Char)
{
char.TryParse(input, out char result);
return ((dynamic)result);
return result;
}
return default(T);
return null;
}
}
}

196
Rest.Net/RouteFileCache.cs Normal file
View File

@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections.Concurrent;
using System.Security.Principal;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of a FileCache that can be used with Routes to speed
/// up response times.
/// </summary>
public class RouteFileCache
{
/// <summary>
/// The outline of a CachedFile.
/// </summary>
public class CachedFile
{
public string FilePath;
public byte[] Content;
public int HitCount;
public DateTime Cached = DateTime.Now;
public DateTime LastAccess = DateTime.Now;
public CachedFile(string filePath, byte[] content)
{
this.FilePath = filePath;
this.Content = content;
}
}
/// <summary>
/// The files that are currently cached.
/// </summary>
public Dictionary<string, CachedFile> CachedFiles = new Dictionary<string, CachedFile>();
/// <summary>
/// The current number of bytes cached.
/// </summary>
public int BytesCached { get; internal set; }
/// <summary>
/// The max bytes that can be cached at a time.
/// </summary>
public int MaxBytesCached { get; internal set; }
/// <summary>
/// Creates a new RouteFileCache with the max bytes that can be cached.
/// </summary>
/// <param name="maxBytes"></param>
public RouteFileCache(int maxBytes)
{
this.MaxBytesCached = maxBytes;
}
/// <summary>
/// Returns whether or not a file was cached and the content of the file if it was.
/// </summary>
/// <param name="filePath"></param>
/// <param name="content"></param>
/// <returns></returns>
public bool Cached(string filePath, out byte[] content)
{
lock (this.CachedFiles)
{
content = null;
if (!this.CachedFiles.ContainsKey(filePath))
return false;
var cached = this.CachedFiles[filePath];
cached.HitCount++;
cached.LastAccess = DateTime.Now;
content = cached.Content;
return true;
}
}
/// <summary>
/// Returns whether or not a file was cached.
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public bool Cached(string filePath)
{
lock (this.CachedFiles)
return this.CachedFiles.ContainsKey(filePath);
}
/// <summary>
/// Caches a new file and returns the content, if the file was already cached the content will be returned.
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
/// <exception cref="FileNotFoundException"></exception>
public byte[] Cache(string filePath)
{
lock (this.CachedFiles)
{
if (!this.CachedFiles.ContainsKey(filePath))
{
if (File.Exists(filePath))
{
var content = File.ReadAllBytes(filePath);
this.Cache(filePath, content);
return content;
}
else
{
throw new FileNotFoundException("Failed to find file: " + filePath);
}
}
else
{
var cached = this.CachedFiles[filePath];
cached.HitCount++;
cached.LastAccess = DateTime.Now;
return cached.Content;
}
}
}
/// <summary>
/// Caches a new file with the content, if the file was already cached this will just count as an access and overwrite the content.
/// </summary>
/// <param name="filePath"></param>
/// <param name="content"></param>
public void Cache(string filePath, byte[] content)
{
lock (this.CachedFiles)
{
if (!this.CachedFiles.ContainsKey(filePath))
{
if ((this.BytesCached + content.Length) > this.MaxBytesCached)
this.Free((this.BytesCached + content.Length) - this.MaxBytesCached);
var cached = new CachedFile(filePath, content);
this.BytesCached += content.Length;
this.CachedFiles.Add(filePath, cached);
}
else
{
var cached = this.CachedFiles[filePath];
cached.HitCount++;
cached.LastAccess = DateTime.Now;
cached.Content = content;
}
}
}
/// <summary>
/// Attempts to free up space in the cache by a specific amount of bytes.
/// </summary>
/// <param name="neededBytes"></param>
public void Free(int neededBytes)
{
while (this.BytesCached + neededBytes >= this.MaxBytesCached)
{
CachedFile weakest = null;
foreach (var pair in this.CachedFiles)
{
if (weakest == null)
weakest = pair.Value;
else if (pair.Value.LastAccess < weakest.LastAccess && pair.Value.HitCount < weakest.HitCount)
weakest = pair.Value;
}
if (weakest != null)
{
this.CachedFiles.Remove(weakest.FilePath);
this.BytesCached -= weakest.Content.Length;
}
}
}
}
}

37
Rest.Net/RouteGroup.cs Normal file
View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an attribute that controls what group a route belongs to.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RouteGroup : Attribute
{
/// <summary>
/// The name of the group this Route belongs to.
/// </summary>
public string Name = null;
/// <summary>
/// Creates a new default route group.
/// </summary>
public RouteGroup() { }
/// <summary>
/// Creates a new route group with the name of the group.
/// </summary>
/// <param name="name"></param>
public RouteGroup(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Must not be null or empty", nameof(name));
this.Name = name;
}
}
}

20
Rest.Net/RouteHidden.cs Normal file
View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an Attribute to hide a given route for client code generation.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RouteHidden : Attribute
{
/// <summary>
/// Creates a default RouteHidden attribute.
/// </summary>
public RouteHidden() { }
}
}

34
Rest.Net/RouteInclude.cs Normal file
View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an Attribute that includes a type when generating a client for this route.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RouteInclude : Attribute
{
/// <summary>
/// The type to include.
/// </summary>
public Type Type = null;
/// <summary>
/// Creates a default route include.
/// </summary>
public RouteInclude() { }
/// <summary>
/// Creates a new route include with the type to include.
/// </summary>
/// <param name="type"></param>
public RouteInclude(Type type)
{
this.Type = type;
}
}
}

View File

@ -17,7 +17,7 @@ namespace MontoyaTech.Rest.Net
/// <summary>
/// The internal http listener.
/// </summary>
private HttpListener HttpListener = null;
protected HttpListener HttpListener = null;
/// <summary>
/// The list of routes this RouteListener is listening for.
@ -29,6 +29,17 @@ namespace MontoyaTech.Rest.Net
/// </summary>
public ushort Port = 8081;
/// <summary>
/// Returns the BaseUrl for this RouteListener.
/// </summary>
public string BaseUrl
{
get
{
return $"http://localhost:{this.Port}";
}
}
/// <summary>
/// An event to preprocess routes before the route is executed.
/// </summary>
@ -82,18 +93,29 @@ namespace MontoyaTech.Rest.Net
{
this.HttpListener = new HttpListener();
//Support listening on Windows & Linux.
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
//Attempt to listen on all interfaces first.
this.HttpListener.Prefixes.Add($"http://*:{this.Port}/");
try
{
this.HttpListener.Start();
}
catch (HttpListenerException ex)
{
//If this failed and it's because we are not admin, try again with the local prefixes
if (ex.Message.ToLower().StartsWith("access is denied"))
{
this.HttpListener = new HttpListener();
this.HttpListener.Prefixes.Add($"http://localhost:{this.Port}/");
this.HttpListener.Prefixes.Add($"http://127.0.0.1:{this.Port}/");
this.HttpListener.Start();
}
else
{
this.HttpListener.Prefixes.Add($"http://*:{this.Port}/");
throw;
}
}
this.HttpListener.Start();
this.Listen();
}
@ -204,9 +226,45 @@ namespace MontoyaTech.Rest.Net
/// </summary>
public void Block()
{
while (this.HttpListener != null && Thread.CurrentThread.ThreadState != ThreadState.AbortRequested && Thread.CurrentThread.ThreadState != ThreadState.Aborted)
if (!Thread.Yield())
Thread.Sleep(1000);
try
{
while (this.HttpListener != null && this.HttpListener.IsListening && Thread.CurrentThread.ThreadState != ThreadState.AbortRequested && Thread.CurrentThread.ThreadState != ThreadState.Aborted)
Thread.Sleep(100);
}
catch { }
}
/// <summary>
/// Generates a C# client from this Route Listener and returns the code.
/// </summary>
/// <param name="clientName">The name of the Client. Default is Client.</param>
/// <param name="staticCode">Whether or not to generate a static client.</param>
/// <returns></returns>
public string GenerateCSharpClient(string clientName = "Client", bool staticCode = false)
{
var generator = new RestCSharpClientGenerator();
generator.ClientName = clientName;
generator.StaticCode = staticCode;
return generator.Generate(this);
}
/// <summary>
/// Generates a Javascript client from this Route Listener and returns the code.
/// </summary>
/// <param name="clientName"></param>
/// <param name="staticCode"></param>
/// <returns></returns>
public string GenerateJavascriptClient(string clientName = "Client", bool staticCode = false, bool useJsonNames = false)
{
var generator = new RestJavascriptClientGenerator();
generator.ClientName = clientName;
generator.StaticCode = staticCode;
generator.UseJsonNames = useJsonNames;
return generator.Generate(this);
}
}
}

View File

@ -34,6 +34,10 @@ namespace MontoyaTech.Rest.Net
//Set the arguments to null, initially.
arguments = null;
//If the input url is invalid, default to false.
if (string.IsNullOrWhiteSpace(url))
return false;
//Trim the url and the syntax.
url = url.Trim();
syntax = syntax.Trim();
@ -42,17 +46,27 @@ namespace MontoyaTech.Rest.Net
if (url.StartsWith("http://"))
{
url = url.Substring(7);
//If there is a slash, skip to the first one, or default to empty path.
if (url.Contains('/'))
url = url.Substring(url.IndexOf('/'));
else
url = "/";
}
else if (url.StartsWith("https://"))
{
url = url.Substring(8);
//If there is a slash, skip to the first one, or default to empty path.
if (url.Contains('/'))
url = url.Substring(url.IndexOf('/'));
else
url = "/";
}
//Split the url and the syntax into path segments
var urlSegments = url.Split('/').Where(segment => segment.Length > 0).Select(segment => segment.Trim()).ToArray();
var syntaxSegments = syntax.Split('/').Where(segment => segment.Length > 0).Select(segment => segment.Trim()).ToArray();
var urlSegments = url.Split('/').Select(segment => segment.Trim()).ToArray();
var syntaxSegments = syntax.Split('/').Select(segment => segment.Trim()).ToArray();
//If we have no url segments, and we have no syntax segments, this is a root match which is fine.
if (urlSegments.Length == 0 && syntaxSegments.Length == 0)
@ -72,19 +86,40 @@ namespace MontoyaTech.Rest.Net
int argumentIndex = 0;
//Check each segment against the url.
var max = Math.Min(urlSegments.Length, syntaxSegments.Length);
int max = Math.Max(urlSegments.Length, syntaxSegments.Length);
for (int i = 0; i < max; i++)
{
var syntaxSegment = syntaxSegments[i];
var syntaxSegment = i >= syntaxSegments.Length ? null : syntaxSegments[i];
var urlSegment = urlSegments[i];
var urlSegment = i >= urlSegments.Length ? null : urlSegments[i];
//If the syntax segment is null, this is not a match.
if (syntaxSegment == null)
{
return false;
}
//If the segments syntax is a double wild card then everything after this is a match.
if (syntaxSegment == "**")
else if (syntaxSegment == "**" && urlSegment != null)
{
return true;
}
else
//If we ran out of url segments, and this syntax segment is a wild card, this is not a match.
else if (syntaxSegment == "*" && urlSegment != null)
{
//Allowed
}
//If we have a syntaxSegment but no urlSegment, this can't be a match.
else if (urlSegment == null && syntaxSegment != null)
{
return false;
}
//If both the url segment and syntax segment are empty then this is allowed
else if (urlSegment != null && urlSegment.Length == 0 && syntaxSegment.Length == 0)
{
//Allowed
}
//If we have a url segment see if it matches against the syntax segment.
else if (urlSegment != null)
{
var conditions = new List<string>();
var builder = new StringBuilder();
@ -145,11 +180,6 @@ namespace MontoyaTech.Rest.Net
}
}
if (urlSegments.Length > syntaxSegments.Length)
return false;
else if (syntaxSegments.Length > urlSegments.Length)
return false;
else
return true;
}
}

38
Rest.Net/RouteName.cs Normal file
View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an attribute that allows you to rename an attribute in
/// the output code generation.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RouteName : Attribute
{
/// <summary>
/// The name of this Route.
/// </summary>
public string Name = null;
/// <summary>
/// Creates a new default RouteName.
/// </summary>
public RouteName() { }
/// <summary>
/// Creates a new RouteName with a given name.
/// </summary>
/// <param name="name"></param>
public RouteName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Cannot be null or whitespace", nameof(name));
this.Name = name;
}
}
}

44
Rest.Net/RouteRequest.cs Normal file
View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an attribute that defines a routes request type.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RouteRequest : Attribute
{
/// <summary>
/// The type of the request.
/// </summary>
public Type RequestType = null;
/// <summary>
/// Whether or not the Request is Json Serialized. Default is true.
/// </summary>
public bool Json = true;
/// <summary>
/// Whether or not the request is a dynamic type. Default is false.
/// </summary>
public bool Dynamic = false;
/// <summary>
/// Creates a default route request.
/// </summary>
public RouteRequest() { }
/// <summary>
/// Creates a route request with the request type.
/// </summary>
/// <param name="requestType"></param>
public RouteRequest(Type requestType)
{
this.RequestType = requestType;
}
}
}

49
Rest.Net/RouteResponse.cs Normal file
View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an attribute that defines a routes response type.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RouteResponse : Attribute
{
/// <summary>
/// The type of the request.
/// </summary>
public Type ResponseType = null;
/// <summary>
/// Whether or not the response is Json Serialized, default is true.
/// </summary>
public bool Json = true;
/// <summary>
/// Whether or not the response is passed as a parameter to the route function. Default is false.
/// </summary>
public bool Parameter = false;
/// <summary>
/// Whether or not the response is a dynamic type. Default is false.
/// </summary>
public bool Dynamic = false;
/// <summary>
/// Creates a default route response.
/// </summary>
public RouteResponse() { }
/// <summary>
/// Creates a route response with the response type.
/// </summary>
/// <param name="responseType"></param>
public RouteResponse(Type responseType)
{
this.ResponseType = responseType;
}
}
}

25
Rest.Net/RouteTypeName.cs Normal file
View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MontoyaTech.Rest.Net
{
/// <summary>
/// The outline of an attribute that can be used to rename a type when it's used in routes.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public class RouteTypeName : Attribute
{
/// <summary>
/// The new name for this type.
/// </summary>
public string Name;
public RouteTypeName(string name)
{
this.Name = name;
}
}
}

View File

@ -18,5 +18,39 @@ namespace MontoyaTech.Rest.Net
return count;
}
public static string Separate(this IList<string> input, char separator)
{
if (input == null || input.Count == 0)
return "";
else if (input.Count < 2)
return input[0];
var builder = new StringBuilder();
builder.Append(input[0]);
for (int i = 1; i < input.Count; i++)
builder.Append(separator).Append(input[i]);
return builder.ToString();
}
public static string Separate(this IList<string> input, string separator)
{
if (input == null || input.Count == 0)
return "";
else if (input.Count < 2)
return input[0];
var builder = new StringBuilder();
builder.Append(input[0]);
for (int i = 1; i < input.Count; i++)
builder.Append(separator).Append(input[i]);
return builder.ToString();
}
}
}