feat: Upload current version of server

This commit is contained in:
2024-03-17 21:33:48 +01:00
parent 827f70d12d
commit 5a78295dfc
63 changed files with 1343 additions and 0 deletions

View File

@@ -0,0 +1 @@
REST API

15
src/Server/.idea/.idea.Server/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.Server.iml
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="theme" value="material" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
</component>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Laag_1" data-name="Laag 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 335.1 198.19">
<defs>
<style>
.cls-1 {
fill: #3c3c3b;
}
</style>
</defs>
<polygon class="cls-1" points="98.06 0 129.42 46.97 131.35 46.97 161.81 0 172.77 0 204 46.97 205.16 46.97 224.65 18.37 237.16 0 298.06 0 205.29 141.94 204.13 141.94 168.13 86.84 166.95 87.1 131.1 141.94 129.94 141.94 71.1 52 58.97 52 129.81 159.87 130.97 159.87 166.95 104.9 168 105.03 204 159.87 205.16 159.87 311.1 0 335.1 0 205.16 198.19 204 198.19 167.42 142.58 130.97 198.19 129.81 198.19 20.52 31.61 82.19 31.61 129.81 104.13 130.84 104.13 166.95 48.9 168.13 48.9 203.87 103.87 205.03 103.87 259.23 21.03 246.97 21.03 205.16 84.77 204 84.77 168.13 29.68 166.95 29.68 130.97 84.9 129.94 84.77 87.74 20.9 13.16 20.9 0 0 98.06 0"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
</component>
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\LibParse\LibParse.csproj" />
<ProjectReference Include="..\LibServer\LibServer.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,24 @@
namespace Console;
using Routes;
using LibServer;
using LibServer.Router;
internal static class Program {
private static readonly Dictionary<string, IRoute> Routes = new() {
{ "/", new Root() },
{ "/routeclass", new RouteClass() }
};
public static void Main() {
var tcpServer = new Server(5000);
var router = new Router(Routes);
// EXPERIMENTAL! Current code is only meant to be used as REST API.
// router.RouteDirectory("D:\\Documents\\[01] Development\\[00] HTML\\[00] portfolio\\v6");
while (tcpServer.Listening) {
tcpServer.AwaitMessage(router.Handler);
}
}
}

View File

@@ -0,0 +1,20 @@
using LibParse.Json;
namespace Console.Routes;
using LibHttp;
using LibServer.Router;
using LibParse;
internal class RequestData {
public int Id;
}
public class Root: IRoute {
public HtmlResponse Get(HtmlRequest request) {
// var data = request.Body?.FromJson<RequestData>();
var data = request.Body?.FromJson<object>();
var response = new HtmlResponse($"{{\"message\": \"you are at /\",\r\n{data?.ToJson()} }}");
return response;
}
}

View File

@@ -0,0 +1,17 @@
using LibParse;
namespace Console.Routes;
using LibServer.Router;
using LibHttp;
using LibParse.Json;
public class RouteClass : IRoute {
public HtmlResponse Get(HtmlRequest request) {
// var data = request.Body?.FromJson<RequestData>();
var response = new HtmlResponse("test\nyes");
response.Body = response.ToJson();
return response;
}
}

Binary file not shown.

View File

@@ -0,0 +1,60 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"Console/1.0.0": {
"dependencies": {
"LibParse": "1.0.0",
"LibServer": "1.0.0"
},
"runtime": {
"Console.dll": {}
}
},
"LibHttp/1.0.0": {
"runtime": {
"LibHttp.dll": {}
}
},
"LibParse/1.0.0": {
"runtime": {
"LibParse.dll": {}
}
},
"LibServer/1.0.0": {
"dependencies": {
"LibHttp": "1.0.0"
},
"runtime": {
"LibServer.dll": {}
}
}
}
},
"libraries": {
"Console/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"LibHttp/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"LibParse/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"LibServer/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,25 @@
namespace LibHttp;
public enum Mime {
Json,
Html,
Text
};
public static class MimeTypes {
public static string MimeToHeader(Mime mime) {
return mime switch {
Mime.Json => "application/json",
Mime.Html => "text/html",
_ => "text/plain"
};
}
public static Mime HeaderToMime(string header) {
return header switch {
"application/json" => Mime.Json,
"text/html" => Mime.Html,
_ => Mime.Text
};
}
}

View File

@@ -0,0 +1,59 @@
namespace LibHttp;
public class HtmlRequest {
private const string LogPrefix = "\u001b[47m[ WWW ]\u001b[0m";
public string? Route;
private string? _httpVersion, _host, _body, _header, _method;
public string? Method => _method;
public string? Body => _body;
public int Parse(string raw) {
// _body starts after 2 new lines, cast to `_body`
var body = raw.Split("\r\n\r\n");
_body = body.Length > 1 ? string.Join("\r\n\r\n", body.Skip(1).ToArray()) : "";
// Split the raw request into parts (one line per part)
var parts = raw.Split("\r\n");
if (parts.Length == 0) return -1;
var isHeader = true;
for (var i = 0; i < parts.Length; i++) {
try {
if (i == 0) {
if (!parts[0].Contains("HTTP")) return -1;
var first = parts[0].Split(" ");
_method = first[0].ToUpper();
Route = first[1];
_httpVersion = first[2].Split("/")[1];
} else {
var components = parts[i].Split(": ");
switch (components[0]) {
case "_host":
_host = components[1];
break;
default:
if (parts[i] == "") isHeader = false;
else if (isHeader) _header += parts[i] + "\r\n";
break;
}
}
}
catch { return -1; }
}
return 0;
}
public void Print() {
Console.WriteLine("{0} _httpVersion:\t{1}", LogPrefix, _httpVersion);
Console.WriteLine("{0} _host:\t\t{1}", LogPrefix, _host);
Console.WriteLine("{0} Route:\t\t{1}", LogPrefix, Route);
Console.WriteLine("{0} _method:\t\t{1}", LogPrefix, _method);
Console.WriteLine("{0} _headers:\n{1}", LogPrefix, _header);
Console.WriteLine("{0} _body:\n{1}", LogPrefix, _body);
}
}

View File

@@ -0,0 +1,74 @@
namespace LibHttp;
public class HtmlResponse {
private static readonly Dictionary<int, string> StatusCodes = new() {
{200, "200 Success"},
{403, "403 Forbidden"},
{404, "404 Not Found"},
{405, "405 Method Not Allowed"},
{500, "500 Internal Server Error"},
{501, "501 Not Implemented"},
{502, "502 Bad Gateway"},
};
private string _httpVersion = "1.1";
private string _status, _body;
private readonly Dictionary<string, string> _header;
public string HttpVersion {
get => _httpVersion;
set => _httpVersion = value;
}
public string Body {
get => _body;
set => _body = value;
}
public Dictionary<string, string> Headers {
get => _header;
}
public HtmlResponse(string body, int statusCode = 200, Mime mime = Mime.Json) {
_body = body;
_header = new Dictionary<string, string>();
_status = StatusToString(statusCode);
if (SetHeader("Content-Type", MimeTypes.MimeToHeader(mime)) != 0) {
throw new Exception($"Failed to set `mime` header of value `{mime}`");
}
}
private static string StatusToString(int statusCode) {
return StatusCodes.GetValueOrDefault(statusCode, "400 Bad Request");
}
public int SetHeader(string key, string value) {
if (_header.TryAdd(key, value)) return 0;
return -1;
}
public void SendFile(string path) {
try {
_body = File.ReadAllText(path);
} catch (Exception e) {
_status = StatusToString(500);
Console.WriteLine(e.Message);
}
}
public string Build(string linebreak = "\r\n") {
return String.Join(linebreak,
[
$"HTTP/{_httpVersion} {_status}",
$"Content-Length: {_body.Length}",
.._header
.Select(pair => $"{pair.Key}:{pair.Value}")
.ToArray(),
"",
_body
]);
}
}

View File

@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"LibHttp/1.0.0": {
"runtime": {
"LibHttp.dll": {}
}
}
}
},
"libraries": {
"LibHttp/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,414 @@
namespace LibParse.Json;
using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
public static class JsonReader {
// `ThreadStatic` due the fields needing to be static in order to be accessed in static functions.
// Declaring them here prevents the need for them to be passed as parameters to every function.
[ThreadStatic] private static Stack<List<string>>? _splitArrayPool;
[ThreadStatic] private static StringBuilder? _stringBuilder;
[ThreadStatic] private static Dictionary<Type, Dictionary<string, FieldInfo>>? _fieldInfoCache;
[ThreadStatic] private static Dictionary<Type, Dictionary<string, PropertyInfo>>? _propertyInfoCache;
/// <summary>
/// Deserializes a JSON string into an object of type T.
/// </summary>
/// <param name="json">The JSON string to be deserialized.</param>
/// <typeparam name="T">The type of the object to be created. Can be either a class containing public data fields
/// or an anonymous `object`.</typeparam>
/// <returns>An object of type T populated with the data from the JSON string, or the default value of type T if the
/// JSON string is empty.</returns>
/// <remarks>
/// This method initializes thread-static variables, removes all whitespace characters not within strings from the JSON
/// string, and then parses the JSON string into an object of type T.
/// </remarks>
public static T? FromJson<T>(this string json) {
if (json.Length < 1) return default;
// Initialize, if needed, the ThreadStatic variables
InitThreadStatic();
// Remove all whitespace not within strings to make parsing simpler
json.RemoveWhitespaces();
// Start parsing the JSON string and cast it into <T>
return (T)ParseValue(typeof(T), _stringBuilder?.ToString()!)!;
}
/// <summary>
/// Initializes thread-static variables used in the parsing process.
/// </summary>
/// <remarks>
/// This method checks if each of the thread-static variables is null. If a variable is null, it initializes it.
/// For the StringBuilder variable, it also ensures that it is empty by calling the Clear method.
/// </remarks>
private static void InitThreadStatic() {
_stringBuilder ??= new StringBuilder();
_splitArrayPool ??= new Stack<List<string>>();
_fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
_propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
// Make sure `_stringBuilder` is empty
_stringBuilder.Clear();
}
/// <summary>
/// Appends characters to the StringBuilder until the end of a string is found in the JSON.
/// </summary>
/// <param name="appendEscapeCharacter">Whether to append escape characters.</param>
/// <param name="startIdx">The starting index in the JSON string.</param>
/// <param name="json">The JSON string.</param>
/// <returns>The index of the closing quote.</returns>
private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) {
// Write the starting quote to the string builder
_stringBuilder?.Append(json[startIdx]);
// Loop through each character in the string and replace double backslashes with singular, stop if a closing quote is found.
for (var i = startIdx + 1; i < json.Length; i++) {
switch (json[i]) {
case '\\':
if (appendEscapeCharacter) _stringBuilder?.Append(json[i]);
_stringBuilder?.Append(json[i + 1]);
// Skip the next backslash as it will be replaced
i++;
break;
case '"':
_stringBuilder?.Append(json[i]);
// Return the index of the closing quote
return i;
default: _stringBuilder?.Append(json[i]); break;
}
}
return json.Length - 1;
}
/// <summary>
/// Removes all whitespace not within strings to make parsing simpler. Writes the outcome to `_stringBuilder`.
/// </summary>
/// <param name="str">The string to remove all whitespaces from.</param>
private static void RemoveWhitespaces(this string str) {
for (var i = 0; i < str.Length; i++) {
var c = str[i];
if (c == '"') {
i = AppendUntilStringEnd(true, i, str);
continue;
}
if (char.IsWhiteSpace(c)) continue;
_stringBuilder?.Append(c);
}
}
/// <summary>
/// Parses a value from a JSON string into an object of a specified type.
/// </summary>
/// <param name="type">The type of the object to be created.</param>
/// <param name="json">The JSON string representing the object.</param>
/// <returns>An object of the specified type populated with the data from the JSON string.</returns>
private static object? ParseValue(Type type, string json) {
if (json == "null") return null;
if (type == typeof(string)) return ParseString(json);
if (type == typeof(decimal)) return ParseDecimal(json);
if (type == typeof(DateTime)) return ParseDateTime(json);
if (type.IsEnum) return type.ParseEnum(json);
if (type.IsArray) return type.ParseArray(json);
if (type == typeof(object)) return ParseAnonymousValue(json);
if (json[0] == '{' && json[^1] == '}') return type.ParseObject(json);
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) return type.ParseList(json);
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) return type.ParseDict(json);
if (type.IsPrimitive) return Convert.ChangeType(json, type, CultureInfo.InvariantCulture);
return null;
}
//Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
/// <summary>
/// Splits a JSON string into a list of strings.
/// </summary>
/// <param name="json">The JSON string to be split.</param>
/// <returns>A list of strings obtained by splitting the JSON string.</returns>
private static List<string> Split(string json) {
var splitArray = _splitArrayPool?.Count > 0 ? _splitArrayPool.Pop() : [];
splitArray.Clear();
// return [] for empty arrays/objects
if (json.Length == 2) return splitArray;
_stringBuilder ??= new StringBuilder();
var parseDepth = 0;
_stringBuilder.Length = 0;
for (var i = 1; i < json.Length - 1; i++) {
switch (json[i]) {
case '[':
case '{':
parseDepth++;
break;
case ']':
case '}':
parseDepth--;
break;
case '"':
i = AppendUntilStringEnd(true, i, json);
continue;
case ',':
case ':':
if (parseDepth == 0) {
splitArray.Add(_stringBuilder.ToString());
_stringBuilder.Length = 0;
continue;
}
break;
}
_stringBuilder.Append(json[i]);
}
splitArray.Add(_stringBuilder.ToString());
return splitArray;
}
/// <summary>
/// Parses a decimal value from a JSON string.
/// </summary>
/// <param name="json">The JSON string representing the decimal value.</param>
/// <returns>The parsed decimal value.</returns>
private static decimal ParseDecimal(string json) {
decimal.TryParse(json, NumberStyles.Float, CultureInfo.InvariantCulture, out var result);
return result;
}
/// <summary>
/// Parses a DateTime value from a JSON string.
/// </summary>
/// <param name="json">The JSON string representing the DateTime value.</param>
/// <returns>The parsed DateTime value.</returns>
private static DateTime ParseDateTime(string json) {
DateTime.TryParse(json.Replace("\"", ""), CultureInfo.InvariantCulture, DateTimeStyles.None, out var result);
return result;
}
/// <summary>
/// Parses an enum value from a JSON string.
/// </summary>
/// <param name="type">The type of the enum.</param>
/// <param name="json">The JSON string representing the enum value.</param>
/// <returns>The parsed enum value.</returns>
private static object ParseEnum(this Type type, string json) {
if (json[0] == '"') json = json.Substring(1, json.Length - 2);
try {
return Enum.Parse(type, json, false);
} catch {
return 0;
}
}
/// <summary>
/// Parses a string value from a JSON string.
/// </summary>
/// <param name="json">The JSON string representing the string value.</param>
/// <returns>The parsed string value.</returns>
private static string ParseString(string json) {
if (json.Length <= 2) return string.Empty;
var parseStringBuilder = new StringBuilder(json.Length);
for (var i = 1; i < json.Length - 1; ++i) {
if (json[i] == '\\' && i + 1 < json.Length - 1) {
var j = "\"\\nrtbf/".IndexOf(json[i + 1]);
if (j >= 0) {
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
++i;
continue;
}
if (json[i + 1] == 'u' && i + 5 < json.Length - 1) {
if (int.TryParse(json.AsSpan(i + 2, 4), NumberStyles.AllowHexSpecifier, null, out var c)) {
parseStringBuilder.Append((char)c);
i += 5;
continue;
}
}
}
parseStringBuilder.Append(json[i]);
}
return parseStringBuilder.ToString();
}
/// <summary>
/// Parses an array from a JSON string.
/// </summary>
/// <param name="type">The type of the array.</param>
/// <param name="json">The JSON string representing the array.</param>
/// <returns>The parsed array.</returns>
private static Array? ParseArray(this Type type, string json) {
var arrayType = type.GetElementType();
if (json[0] != '[' || json[^1] != ']' || arrayType == null) return null;
var elems = Split(json);
var newArray = Array.CreateInstance(arrayType, elems.Count);
for (var i = 0; i < elems.Count; i++) newArray.SetValue(ParseValue(arrayType, elems[i]), i);
_splitArrayPool?.Push(elems);
return newArray;
}
/// <summary>
/// Parses a list from a JSON string.
/// </summary>
/// <param name="type">The type of the list.</param>
/// <param name="json">The JSON string representing the list.</param>
/// <returns>The parsed list.</returns>
private static IList? ParseList(this Type type, string json) {
var listType = type.GetGenericArguments()[0];
if (json[0] != '[' || json[^1] != ']') return null;
var elems = Split(json);
var list = (IList?)type.GetConstructor([ typeof(int) ])?.Invoke(new object[] { elems.Count });
foreach (var elem in elems) list?.Add(ParseValue(listType, elem));
_splitArrayPool?.Push(elems);
return list;
}
/// <summary>
/// Parses a dictionary from a JSON string.
/// </summary>
/// <param name="type">The type of the dictionary.</param>
/// <param name="json">The JSON string representing the dictionary.</param>
/// <returns>The parsed dictionary.</returns>
private static IDictionary? ParseDict(this Type type, string json) {
Type keyType, valueType; {
var args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
}
//Refuse to parse dictionary keys that aren't of type string
if (keyType != typeof(string)) return null;
//Must be a valid dictionary element
if (json[0] != '{' || json[^1] != '}') return null;
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
var elems = Split(json);
if (elems.Count % 2 != 0) return null;
var dictionary = (IDictionary?)type.GetConstructor([ typeof(int) ])?.Invoke(new object[] { elems.Count / 2 });
for (var i = 0; i < elems.Count; i += 2) {
if (elems[i].Length <= 2) continue;
var keyValue = elems[i].Substring(1, elems[i].Length - 2);
var val = ParseValue(valueType, elems[i + 1]);
dictionary![keyValue] = val;
}
return dictionary;
}
/// <summary>
/// Parses an anonymous value from a JSON string.
/// </summary>
/// <param name="json">The JSON string representing the anonymous value.</param>
/// <returns>The parsed anonymous value.</returns>
private static object? ParseAnonymousValue(string json) {
if (json.Length == 0) return null;
if (json[0] == '{' && json[^1] == '}') {
var elems = Split(json);
if (elems.Count % 2 != 0) return null;
var dict = new Dictionary<string, object>(elems.Count / 2);
for (var i = 0; i < elems.Count; i += 2)
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]) ?? string.Empty;
return dict;
}
if (json[0] == '[' && json[^1] == ']') {
var items = Split(json);
var finalList = new List<object>(items.Count);
foreach (var item in items) finalList.Add(ParseAnonymousValue(item) ?? string.Empty);
return finalList;
}
if (json[0] == '"' && json[^1] == '"') {
var str = json.Substring(1, json.Length - 2);
return str.Replace("\\", string.Empty);
}
if (char.IsDigit(json[0]) || json[0] == '-') {
if (json.Contains('.')) {
double.TryParse(json, NumberStyles.Float, CultureInfo.InvariantCulture, out var result);
return result;
} else {
int.TryParse(json, out var result);
return result;
}
}
return json switch {
"true" => true,
"false" => false,
_ => null
};
}
/// <summary>
/// Creates a dictionary mapping member names to members.
/// </summary>
/// <param name="members">The members to be included in the dictionary.</param>
/// <returns>A dictionary mapping member names to members.</returns>
private static Dictionary<string, T> CreateMemberNameDictionary<T>(IEnumerable<T> members) where T : MemberInfo {
var nameToMember = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
foreach (var member in members) {
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue;
var name = member.Name;
if (member.IsDefined(typeof(DataMemberAttribute), true)) {
var dataMemberAttribute = (DataMemberAttribute?)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
if (!string.IsNullOrEmpty(dataMemberAttribute?.Name)) name = dataMemberAttribute.Name;
}
nameToMember.Add(name, member);
}
return nameToMember;
}
/// <summary>
/// Parses a JSON object into an instance of a specified type.
/// </summary>
/// <param name="type">The type of the object to be created.</param>
/// <param name="json">The JSON string representing the object.</param>
/// <returns>An object of the specified type populated with the data from the JSON string.</returns>
/// <remarks>
/// This method uses reflection to create an instance of the specified type and populate its public fields and properties
/// with the data from the JSON string. It supports complex types with nested objects and arrays.
/// If a field or property is marked with the IgnoreDataMemberAttribute, it will be ignored.
/// If a field or property is marked with the DataMemberAttribute, the name specified in the attribute will be used as the key in the JSON object.
/// </remarks>
private static object ParseObject(this Type type, string json) {
var instance = type.GetConstructor(Type.EmptyTypes)?.Invoke(null);
if (instance == null) return type;
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
var elems = Split(json);
if (elems.Count % 2 != 0) return instance;
if (!_fieldInfoCache!.TryGetValue(type, out var nameToField)) {
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
_fieldInfoCache.Add(type, nameToField);
}
if (!_propertyInfoCache!.TryGetValue(type, out var nameToProperty)) {
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
_propertyInfoCache.Add(type, nameToProperty);
}
for (var i = 0; i < elems.Count; i += 2) {
if (elems[i].Length <= 2) continue;
var key = elems[i].Substring(1, elems[i].Length - 2);
var value = elems[i + 1];
if (nameToField.TryGetValue(key, out var fieldInfo))
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
else if (nameToProperty.TryGetValue(key, out var propertyInfo))
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
}
return instance;
}
}

View File

@@ -0,0 +1,185 @@
namespace LibParse.Json;
using System.Reflection;
using System.Globalization;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
public static class JsonWriter {
/// <summary>
/// Converts an object into a JSON string.
/// </summary>
/// <param name="item">The object to be converted into a JSON string.</param>
/// <returns>A JSON string representation of the object.</returns>
public static string ToJson(this object item) {
// A `StringWriter` is used instead of just `StringBuilder` due to its higher efficiency with smaller strings.
var writer = new StringWriter();
// Start the recursive call to append the values of `item` to the writer.
AppendValue(writer, item);
// Return the string representation of `item` as JSON object.
return writer.ToString();
}
/// <summary>
/// Appends the value of `item` to the writer.
/// If type = any decimal numeric type, boolean or DateTime, append the value as is.
/// If type = any other type, append the value as is.
/// If type = string or char, append the value as "string", escaping special characters.
/// If type = object, check if it's an enum, list, or dictionary, go as deep as the nested object goes and append the value:
/// - If it's an enum, append the value as "enum"
/// - If it's a list, append the value as "[item1,item2,...]"
/// - If it's a dictionary, append the value as "{\"key1\":value1,\"key2\":value2,...}"
/// - For any other object type (class, ...), loop through each field and property in the object and append the value.
/// </summary>
/// <param name="writer">The `TextWriter` to write the variable into</param>
/// <param name="item">The item to cast into `writer`</param>
private static void AppendValue(TextWriter writer, object item) {
// Get the type of `item`. This is used to determine how to append the value to the writer.
var type = item.GetType();
switch (Type.GetTypeCode(type)) {
case TypeCode.String:
case TypeCode.Char:
writer.Write('"');
// Loop through each character in the string and escape special characters.
foreach (var c in item.ToString()!) writer.Write(EscapeCharacter(c));
writer.Write('"');
break;
case TypeCode.Object: item.CastIntoWriter(writer, type); break;
case TypeCode.Single: writer.Write(((float)item).ToString(CultureInfo.InvariantCulture)); break;
case TypeCode.Double: writer.Write(((double)item).ToString(CultureInfo.InvariantCulture)); break;
case TypeCode.Decimal: writer.Write(((decimal)item).ToString(CultureInfo.InvariantCulture)); break;
case TypeCode.Boolean: writer.Write((bool)item ? "true" : "false"); break;
case TypeCode.DateTime: writer.Write($"\"{((DateTime)item).ToString(CultureInfo.InvariantCulture)}\""); break;
default: writer.Write(item); break;
}
}
/// <summary>
/// Escapes special characters in a string.
/// </summary>
/// <param name="c">The character to be escaped.</param>
/// <returns>The escaped character as a string.</returns>
private static string EscapeCharacter(char c) {
if (c is not (< ' ' or '"' or '\\')) return c.ToString();
var j = "\"\\nrtbf/".IndexOf(c);
return j > -1 ? "\"\\\n\r\t\b\f/"[j].ToString() : $"\\u{(int)c:X4}";
}
/// <summary>
/// Casts an object into a TextWriter.
/// </summary>
/// <param name="item">The object to be cast.</param>
/// <param name="writer">The TextWriter to cast the object into.</param>
/// <param name="type">The type of the object.</param>
private static void CastIntoWriter(this object item, TextWriter writer, Type type) {
if (type.IsEnum) writer.Write($"\"{item}\"");
else if (item is IList list) list.ListToString(writer);
else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
(item as IDictionary)!.DictionaryToString(writer, type);
else item.ClassToString(writer, type);
}
/// <summary>
/// Converts a list into a string and appends it to a TextWriter.
/// </summary>
/// <param name="list">The list to be converted into a string.</param>
/// <param name="writer">The TextWriter to append the string to.</param>
private static void ListToString(this IEnumerable list, TextWriter writer) {
var isFirst = true;
writer.Write('[');
// Loop through each item in the list and append the value.
foreach (var e in list) {
if (isFirst) isFirst = false;
else writer.Write(',');
AppendValue(writer, e);
}
writer.Write(']');
}
/// <summary>
/// Converts a dictionary into a string and appends it to a TextWriter.
/// </summary>
/// <param name="dict">The dictionary to be converted into a string.</param>
/// <param name="writer">The TextWriter to append the string to.</param>
/// <param name="type">The type of the dictionary.</param>
private static void DictionaryToString(this IDictionary dict, TextWriter writer, Type type) {
// Get type of the dictionary key. Refuse to output dictionary keys that aren't of type string
var keyType = type.GetGenericArguments()[0];
if (keyType != typeof(string)) {
writer.Write("{}");
return;
}
var isFirst = true;
writer.Write('{');
// Loop through each key-value pair in the dictionary and append the value.
foreach (var key in dict.Keys) {
if (isFirst) isFirst = false;
else writer.Write(',');
writer.Write($"\"{(string)key}\":");
// Recursive call to append the value of the dictionary key to the writer. Will go as deep as the nested object goes.
AppendValue(writer, dict[key]!);
}
writer.Write('}');
}
/// <summary>
/// Gets the name of a member.
/// </summary>
/// <param name="member">The member whose name is to be gotten.</param>
/// <returns>The name of the member.</returns>
private static string GetMemberName(MemberInfo member) {
if (member.IsDefined(typeof(DataMemberAttribute), true)) return member.Name;
var dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true)!;
return !string.IsNullOrEmpty(dataMemberAttribute?.Name) ? dataMemberAttribute.Name : member.Name;
}
/// <summary>
/// Converts an object into a string and appends it to a TextWriter.
/// </summary>
/// <param name="item">The object to be converted into a string.</param>
/// <param name="writer">The TextWriter to append the string to.</param>
/// <param name="type">The type of the object.</param>
private static void ClassToString(this object item, TextWriter writer, Type type) {
var isFirst = true;
var fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
writer.Write('{');
foreach (var field in fieldInfos) {
if (field.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue;
var value = field.GetValue(item);
if (value == null) continue;
if (isFirst) isFirst = false;
else writer.Write(',');
writer.Write($"\"{GetMemberName(field)}\":");
AppendValue(writer, value);
}
var propertyInfo = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
foreach (var property in propertyInfo) {
// Ignore private fields
if (!property.CanRead || property.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue;
var value = property.GetValue(item, null);
if (value == null) continue;
if (isFirst) isFirst = false;
else writer.Write(',');
writer.Write($"\"{GetMemberName(property)}\":");
AppendValue(writer, value);
}
writer.Write('}');
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"LibParse/1.0.0": {
"runtime": {
"LibParse.dll": {}
}
}
}
},
"libraries": {
"LibParse/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,95 @@
namespace LibServer;
using LibHttp;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
public class Server {
private const string LogPrefix = "\u001b[44m[ TCP ]\u001b[0m";
private readonly TcpListener _listener;
private readonly string _ip;
public readonly bool Listening;
public Server(int port, string ip = "127.0.0.1") {
_ip = ip;
// Parse the IP and initialize the TCPListener
var localAddr = IPAddress.Parse(_ip);
_listener = new TcpListener(localAddr, port);
// Log the server initialization options
Console.WriteLine("{0} Server initialized on {1}:{2}", LogPrefix, localAddr, port);
// Start the TCPListener
_listener.Start();
Listening = true;
}
public void AwaitMessage(Func<HtmlRequest, HtmlResponse> handler, string endRegex = @"\r\n?|\n", string replacer=@"\r\n") {
// Console.WriteLine("{0} Server awaiting incoming client", LogPrefix);
var client = _listener.AcceptTcpClient(); // .ConfigureAwait(false);
// Get the incoming data from the user
// Console.WriteLine("{0} Client received, listening on input stream", LogPrefix);
var receivedBuffer = new byte[client.ReceiveBufferSize];
var message = "";
using (var stream = client.GetStream()) {
// read incoming `stream`
var bytesRead = stream.Read(receivedBuffer, 0, client.ReceiveBufferSize);
// convert the data received into a string, append it to `message`
message += Encoding.ASCII.GetString(receivedBuffer, 0, bytesRead);
// Create stream writer to write back to client
var writer = new StreamWriter(stream);
// Parse HTML data, if it fails, return 400 Bad Request
var request = new HtmlRequest();
if (request.Parse(message) != 0) {
var badReq = CreateBadRequestResponse();
writer.Write(badReq.Build());
writer.Flush();
return;
}
// Print the parsed request
request.Print();
// Send the request to `handler`, then send its response back to the client
var response = handler(request);
WriteBaseApiHeaders(response);
var builtResponse = response.Build();
// Console.WriteLine("{0} Sending response to client:\n{1}", LogPrefix, builtResponse);
try {
writer.Write(builtResponse);
writer.Flush();
}
catch (IOException) { client.Close(); }
}
// Filter `EndRegex` to not flood the console
// var messageFiltered = Regex.Replace(message, endRegex, replacer);
// Write final log message and close the client connection
// Console.WriteLine("{0} Transaction finished, closing client connection\n\tFinal message:\t{1}", LogPrefix, messageFiltered);
client.Close();
}
private void WriteBaseApiHeaders(HtmlResponse response) {
response.SetHeader("Server", _ip);
response.SetHeader("Date", DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss UTC"));
response.SetHeader("Cache-Control", "max-age=0, private, must-revalidate");
}
private HtmlResponse CreateBadRequestResponse() {
var badReq = new HtmlResponse("{ \"statusCode\": 400 }", statusCode:400);
WriteBaseApiHeaders(badReq);
return badReq;
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LibHttp\LibHttp.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
namespace LibServer.Router;
using LibHttp;
public interface IRoute {
public HtmlResponse Get(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse Post(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse Put(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse Patch(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse Delete(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse Head(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse Options(HtmlRequest _) { return MethodNotAllowed(); }
public HtmlResponse MethodNotAllowed() {
var response = new HtmlResponse("{ \"message\": \"Method Not Allowed\"}", statusCode:405);
response.SetHeader("Content-Type", "application/json");
return response;
}
}

View File

@@ -0,0 +1,54 @@
namespace LibServer.Router;
using LibHttp;
public class Router(Dictionary<string, IRoute> routes) {
private Dictionary<string, IRoute> Routes { get; } = routes;
public HtmlResponse Handler(HtmlRequest request) {
// Enumerable is considerably slower on small collections, that's why a `foreach` loop has been used.
foreach (var route in Routes) {
if (request.Route == route.Key) {
return request.Method switch {
"GET" => route.Value.Get(request),
"POST" => route.Value.Post(request),
"PUT" => route.Value.Put(request),
"PATCH" => route.Value.Patch(request),
"DELETE" => route.Value.Delete(request),
"HEAD" => route.Value.Head(request),
"OPTIONS" => route.Value.Options(request),
_ => route.Value.MethodNotAllowed()
};
}
}
return new HtmlResponse("{ \"message\": \"Not Found\" }", statusCode:404);
}
// EXPERIMENTAL!
public void RouteDirectoryRecursive(string directory) {
List<string> collectedDirectories = [];
RecursiveDirectories(directory, collectedDirectories).ForEach(f => {
var location = f.Replace(directory, "").Replace("\\", "/");
var fileRoute = new StaticFile(f);
fileRoute.InitHeaders();
Console.WriteLine($"Adding route: {location} to {f}");
Routes.Add(location, fileRoute);
});
}
private static List<string> RecursiveDirectories(string directory, List<string> collectedDirectories) {
try {
Directory.GetDirectories(directory).ToList().ForEach(d => {
Directory.GetFiles(d).ToList().ForEach(collectedDirectories.Add);
RecursiveDirectories(d, collectedDirectories);
});
} catch (System.Exception e) {
Console.WriteLine(e.Message);
}
return collectedDirectories;
}
}

View File

@@ -0,0 +1,24 @@
namespace LibServer.Router;
using LibHttp;
class StaticFile(string file) : IRoute {
private string File { get; } = file;
private string _mime = "text/html";
public HtmlResponse Get(HtmlRequest req) {
var response = new HtmlResponse("");
response.SetHeader("Content-Type", _mime);
response.SendFile(File);
return response;
}
public void InitHeaders() {
switch (File.Split('.').Last()) {
case "js": _mime = "application/javascript"; break;
case "css": _mime = "text/css"; break;
case "html": _mime = "text/html"; break;
case "json": _mime = "application/json"; break;
};
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,36 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"LibServer/1.0.0": {
"dependencies": {
"LibHttp": "1.0.0"
},
"runtime": {
"LibServer.dll": {}
}
},
"LibHttp/1.0.0": {
"runtime": {
"LibHttp.dll": {}
}
}
}
},
"libraries": {
"LibServer/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"LibHttp/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,36 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"LibServer/1.0.0": {
"dependencies": {
"LibHttp": "1.0.0"
},
"runtime": {
"LibServer.dll": {}
}
},
"LibHttp/1.0.0": {
"runtime": {
"LibHttp.dll": {}
}
}
}
},
"libraries": {
"LibServer/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"LibHttp/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

40
src/Server/REST API.sln Normal file
View File

@@ -0,0 +1,40 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "Console\Console.csproj", "{FCF181D7-7D8A-4DDD-80E0-C66EBEFD8D13}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibHttp", "LibHttp\LibHttp.csproj", "{C7E8D1E7-E6DF-4806-9509-06CB6DDB2FD3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibServer", "LibServer\LibServer.csproj", "{7EF92BDA-67F9-4A14-9267-087875E0EAB9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibParse", "LibParse\LibParse.csproj", "{07747992-18E2-4532-9F91-C6ECA99D55C4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FCF181D7-7D8A-4DDD-80E0-C66EBEFD8D13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCF181D7-7D8A-4DDD-80E0-C66EBEFD8D13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCF181D7-7D8A-4DDD-80E0-C66EBEFD8D13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCF181D7-7D8A-4DDD-80E0-C66EBEFD8D13}.Release|Any CPU.Build.0 = Release|Any CPU
{C7E8D1E7-E6DF-4806-9509-06CB6DDB2FD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C7E8D1E7-E6DF-4806-9509-06CB6DDB2FD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7E8D1E7-E6DF-4806-9509-06CB6DDB2FD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7E8D1E7-E6DF-4806-9509-06CB6DDB2FD3}.Release|Any CPU.Build.0 = Release|Any CPU
{7EF92BDA-67F9-4A14-9267-087875E0EAB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EF92BDA-67F9-4A14-9267-087875E0EAB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EF92BDA-67F9-4A14-9267-087875E0EAB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EF92BDA-67F9-4A14-9267-087875E0EAB9}.Release|Any CPU.Build.0 = Release|Any CPU
{07747992-18E2-4532-9F91-C6ECA99D55C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07747992-18E2-4532-9F91-C6ECA99D55C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07747992-18E2-4532-9F91-C6ECA99D55C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07747992-18E2-4532-9F91-C6ECA99D55C4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">INFO</s:String></wpf:ResourceDictionary>

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">INFO</s:String></wpf:ResourceDictionary>