diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 00000000..f9a78fb4 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,20 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - dev + - main + +jobs: + qodana: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2023.2 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/EliteAPI.Abstractions/EliteAPI.Abstractions.csproj b/EliteAPI.Abstractions/EliteAPI.Abstractions.csproj index 2a2624ab..ccce42c0 100644 --- a/EliteAPI.Abstractions/EliteAPI.Abstractions.csproj +++ b/EliteAPI.Abstractions/EliteAPI.Abstractions.csproj @@ -5,10 +5,10 @@ 10 3.0.0.0 3.0.0.0 - 3.0.15+13.Branch.main.Sha.aa3bd21915f470956d90f87a2d089e6d67be09ae + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de 3.0.0-alpha5155 - 3.0.15.0 - 3.0.15.0 + 3.1.0.0 + 3.1.0.0 https://github.com/EliteAPI/EliteAPI https://github.com/EliteAPI/EliteAPI @@ -18,7 +18,7 @@ Abstractions for EliteAPI © Somfic 2022 false - 3.0.15 + 3.1.0-alpha0182 true icon.png true diff --git a/EliteAPI.Abstractions/Events/IEvents.cs b/EliteAPI.Abstractions/Events/IEvents.cs index b84df3d3..4f9df52c 100644 --- a/EliteAPI.Abstractions/Events/IEvents.cs +++ b/EliteAPI.Abstractions/Events/IEvents.cs @@ -9,7 +9,7 @@ public interface IEvents IReadOnlyCollection<(IEvent @event, EventContext context)> Backlog { get; } /// All event types that have been registered. - IEnumerable EventTypes { get; } + IReadOnlyCollection EventTypes { get; } /// A collection of previous events since the API was started. IReadOnlyCollection<(IEvent @event, EventContext context)> PreviousEvents { get; } diff --git a/EliteAPI.Events/Carriers/CarrierBankTransferEvent.cs b/EliteAPI.Events/Carriers/CarrierBankTransferEvent.cs index 18fd22cf..36059789 100644 --- a/EliteAPI.Events/Carriers/CarrierBankTransferEvent.cs +++ b/EliteAPI.Events/Carriers/CarrierBankTransferEvent.cs @@ -5,6 +5,12 @@ namespace EliteAPI.Events; public struct CarrierBankTransferEvent : IEvent { + [JsonProperty("timestamp")] + public DateTime Timestamp { get; init; } + + [JsonProperty("event")] + public string Event { get; init; } + [JsonProperty("CarrierID")] public string CarrierId { get; init; } @@ -19,8 +25,4 @@ public struct CarrierBankTransferEvent : IEvent [JsonProperty("CarrierBalance")] public long CarrierBalance { get; init; } - - public DateTime Timestamp { get; } - - public string Event { get; } } \ No newline at end of file diff --git a/EliteAPI.Events/Carriers/CarrierNameChangedEvent.cs b/EliteAPI.Events/Carriers/CarrierNameChangeEvent.cs similarity index 89% rename from EliteAPI.Events/Carriers/CarrierNameChangedEvent.cs rename to EliteAPI.Events/Carriers/CarrierNameChangeEvent.cs index eb295bff..700e5c2a 100644 --- a/EliteAPI.Events/Carriers/CarrierNameChangedEvent.cs +++ b/EliteAPI.Events/Carriers/CarrierNameChangeEvent.cs @@ -3,7 +3,7 @@ namespace EliteAPI.Events; -public readonly struct CarrierNameChangedEvent : IEvent +public readonly struct CarrierNameChangeEvent : IEvent { [JsonProperty("timestamp")] public DateTime Timestamp { get; init; } diff --git a/EliteAPI.Events/Combat/DropShipDeplyEvent.cs b/EliteAPI.Events/Combat/DropShipDeployEvent.cs similarity index 88% rename from EliteAPI.Events/Combat/DropShipDeplyEvent.cs rename to EliteAPI.Events/Combat/DropShipDeployEvent.cs index 595358e7..15d4f08f 100644 --- a/EliteAPI.Events/Combat/DropShipDeplyEvent.cs +++ b/EliteAPI.Events/Combat/DropShipDeployEvent.cs @@ -24,8 +24,8 @@ namespace EliteAPI.Events; public string BodyId { get; init; } [JsonProperty("OnStation")] - public bool OnStation { get; init; } + public bool IsOnStation { get; init; } [JsonProperty("OnPlanet")] - public bool OnPlanet { get; init; } + public bool IsOnPlanet { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/EliteAPI.Events.csproj b/EliteAPI.Events/EliteAPI.Events.csproj index 0bfa3209..f37856ed 100644 --- a/EliteAPI.Events/EliteAPI.Events.csproj +++ b/EliteAPI.Events/EliteAPI.Events.csproj @@ -3,9 +3,9 @@ enable enable 10 - 3.0.15.0 - 3.0.15.0 - 3.0.15+13.Branch.main.Sha.aa3bd21915f470956d90f87a2d089e6d67be09ae + 3.1.0.0 + 3.1.0.0 + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de 3.0.0-alpha5167 true https://github.com/EliteAPI/EliteAPI @@ -17,7 +17,7 @@ Events for EliteAPI © Somfic 2022 false - 3.0.15 + 3.1.0-alpha0182 true icon.png netstandard2.0 diff --git a/EliteAPI.Events/Other/BuyMicroResourceEvent.cs b/EliteAPI.Events/Other/BuyMicroResourcesEvent.cs similarity index 95% rename from EliteAPI.Events/Other/BuyMicroResourceEvent.cs rename to EliteAPI.Events/Other/BuyMicroResourcesEvent.cs index 9a25d44e..da4c9f95 100644 --- a/EliteAPI.Events/Other/BuyMicroResourceEvent.cs +++ b/EliteAPI.Events/Other/BuyMicroResourcesEvent.cs @@ -3,7 +3,7 @@ namespace EliteAPI.Events; -public readonly struct BuyMicroResourceEvent : IEvent +public readonly struct BuyMicroResourcesEvent : IEvent { [JsonProperty("timestamp")] public DateTime Timestamp { get; init; } diff --git a/EliteAPI.Events/Station/ResupplyEvent.cs b/EliteAPI.Events/Station/ResupplyEvent.cs new file mode 100644 index 00000000..1c4db5c6 --- /dev/null +++ b/EliteAPI.Events/Station/ResupplyEvent.cs @@ -0,0 +1,13 @@ +using EliteAPI.Abstractions.Events; +using Newtonsoft.Json; + +namespace EliteAPI.Events; + +public readonly struct ResupplyEvent : IEvent +{ + [JsonProperty("timestamp")] + public DateTime Timestamp { get; init; } + + [JsonProperty("event")] + public string Event { get; init; } +} \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/AltitudeStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/AltitudeStatusEvent.cs index 93113f37..1282a72a 100644 --- a/EliteAPI.Events/Status/Ship/Events/AltitudeStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/AltitudeStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct AltitudeStatusEvent : IStatusEvent +public readonly struct AltitudeStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Altitude"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/BodyRadiusStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/BodyRadiusStatusEvent.cs index 2beb55e8..a68e691c 100644 --- a/EliteAPI.Events/Status/Ship/Events/BodyRadiusStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/BodyRadiusStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct BodyRadiusStatusEvent : IStatusEvent +public readonly struct BodyRadiusStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "BodyRadius"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/GravityStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/GravityStatusEvent.cs index 7af41a76..f9811a79 100644 --- a/EliteAPI.Events/Status/Ship/Events/GravityStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/GravityStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct GravityStatusEvent : IStatusEvent +public readonly struct GravityStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Gravity"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/HeadingStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/HeadingStatusEvent.cs index b9d12443..d9e00a43 100644 --- a/EliteAPI.Events/Status/Ship/Events/HeadingStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/HeadingStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct HeadingStatusEvent : IStatusEvent +public readonly struct HeadingStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Heading"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/HealthStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/HealthStatusEvent.cs index 549785ca..bc1e3700 100644 --- a/EliteAPI.Events/Status/Ship/Events/HealthStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/HealthStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct HealthStatusEvent : IStatusEvent +public readonly struct HealthStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Health"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/LatitudeStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/LatitudeStatusEvent.cs index 6ea06459..a5f3b39a 100644 --- a/EliteAPI.Events/Status/Ship/Events/LatitudeStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/LatitudeStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct LatitudeStatusEvent : IStatusEvent +public readonly struct LatitudeStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Latitude"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/LongitudeStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/LongitudeStatusEvent.cs index b5fbbfa2..77b273bf 100644 --- a/EliteAPI.Events/Status/Ship/Events/LongitudeStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/LongitudeStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct LongitudeStatusEvent : IStatusEvent +public readonly struct LongitudeStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Longitude"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/OxygenStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/OxygenStatusEvent.cs index 8a91d122..97e33bcb 100644 --- a/EliteAPI.Events/Status/Ship/Events/OxygenStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/OxygenStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct OxygenStatusEvent : IStatusEvent +public readonly struct OxygenStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Oxygen"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/Events/TemperatureStatusEvent.cs b/EliteAPI.Events/Status/Ship/Events/TemperatureStatusEvent.cs index b5ead477..cf1dd8be 100644 --- a/EliteAPI.Events/Status/Ship/Events/TemperatureStatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/Events/TemperatureStatusEvent.cs @@ -2,11 +2,11 @@ namespace EliteAPI.Events.Status.Ship.Events; -public readonly struct TemperatureStatusEvent : IStatusEvent +public readonly struct TemperatureStatusEvent : IStatusEvent { public DateTime Timestamp => DateTime.Now; public string Event => "Temperature"; - public float Value { get; init; } + public double Value { get; init; } } \ No newline at end of file diff --git a/EliteAPI.Events/Status/Ship/StatusEvent.cs b/EliteAPI.Events/Status/Ship/StatusEvent.cs index 0607bbdd..624dda6f 100644 --- a/EliteAPI.Events/Status/Ship/StatusEvent.cs +++ b/EliteAPI.Events/Status/Ship/StatusEvent.cs @@ -100,34 +100,34 @@ namespace EliteAPI.Events.Status.Ship; public ShipDestination Destination { get; init; } [JsonProperty("PlanetRadius")] - public float BodyRadius { get; init; } + public double BodyRadius { get; init; } [JsonProperty("Oxygen")] - public float Oxygen { get; init; } + public double Oxygen { get; init; } [JsonProperty("Health")] - public float Health { get; init; } + public double Health { get; init; } [JsonProperty("Temperature")] - public float Temperature { get; init; } + public double Temperature { get; init; } [JsonProperty("SelectedWeapon")] public Localised SelectedWeapon { get; init; } [JsonProperty("Gravity")] - public float Gravity { get; init; } + public double Gravity { get; init; } [JsonProperty("Latitude")] - public float Latitude { get; init; } + public double Latitude { get; init; } [JsonProperty("Longitude")] - public float Longitude { get; init; } + public double Longitude { get; init; } [JsonProperty("Heading")] - public float Heading { get; init; } + public double Heading { get; init; } [JsonProperty("Altitude")] - public long Altitude { get; init; } + public double Altitude { get; init; } [JsonProperty("BodyName")] public string Body { get; init; } diff --git a/EliteAPI.Tests/Conventions.cs b/EliteAPI.Tests/Conventions.cs new file mode 100644 index 00000000..fa43e7a4 --- /dev/null +++ b/EliteAPI.Tests/Conventions.cs @@ -0,0 +1,114 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using System.Xml; +using EliteAPI.Abstractions.Bindings; +using EliteAPI.Abstractions.Bindings.Models; +using EliteAPI.Abstractions.Events; +using EliteAPI.Bindings; +using EliteAPI.Events; +using Microsoft.Extensions.Logging; +using Moq; + +namespace EliteAPI.Tests; + +[TestFixture] +public class Convetions +{ + [Test(Description = "Event types")] + [TestCaseSource(nameof(GetTypes))] + public void Events(Type type) + { + // Namespace should be EliteAPI.Events + Assert.That(type.Namespace!.StartsWith("EliteAPI.Events"), + $"Type {type.Name} should be in namespace EliteAPI.Events"); + + // Name should not have underscores + Assert.That(!type.Name.Contains("_"), + $"Type {type.Name} should not contain underscores"); + + // Name should be PascalCase + Assert.That(char.IsUpper(type.Name[0]), + $"Type {type.Name} should start with a capital letter"); + + // Should be a struct + Assert.That(type.IsValueType, + $"Type {type.Name} should be a struct"); + + // Should implement IEvent + Assert.That(type.GetInterfaces().Contains(typeof(IEvent)), + $"Type {type.Name} should implement IEvent"); + } + + [Test(Description = "Properties")] + [TestCaseSource(nameof(GetProperties))] + public void Properties(PropertyInfo property) + { + var name = property.DeclaringType!.Name + "." + property.Name; + + // Namespace should be EliteAPI.Events + Assert.That(property.DeclaringType!.Namespace!.StartsWith("EliteAPI.Events"), + $"Property {name} should be in namespace EliteAPI.Events"); + + // Name should not have underscores + Assert.That(!property.Name.Contains("_"), + $"Property {name} should not contain underscores"); + + // Name should be PascalCase + Assert.That(char.IsUpper(property.Name[0]), + $"Property {name} should start with a capital letter"); + + // Name should not be too long + Assert.That(property.Name.Length <= 32, + $"Property {name} should not be longer than 32 characters"); + + if (property.PropertyType == typeof(float) || property.PropertyType == typeof(decimal)) + { + // Property should be of type double + Assert.That(property.PropertyType == typeof(double), + $"Property {name} should be of type double"); + } + + if (property.PropertyType == typeof(bool) && !property.DeclaringType.Namespace.StartsWith("EliteAPI.Events.Status")) + { + // Property should be named IsX or HasX or WasX + var prefixes = new[] {"Is", "Has", "Was", "Allows", "Can", "Should"}; + Assert.That(prefixes.Any(x => property.Name.StartsWith(x)), + $"Property {name} should start with {string.Join(" or ", prefixes)}"); + } + } + + static IImmutableList GetProperties() + { + var eventParser = new EventParser(Mock.Of()); + eventParser.Use(); + var events = new Events.Events(Mock.Of>(), eventParser); + events.Register(); + + var properties = new List(); + + if(events == null) + throw new Exception("Events not initialized"); + + foreach (var eventType in events.EventTypes) + { + properties.AddRange(eventType.GetProperties()); + } + + return properties.ToImmutableList(); + } + + static IImmutableList GetTypes() + { + var eventParser = new EventParser(Mock.Of()); + eventParser.Use(); + + var events = new Events.Events(Mock.Of>(), eventParser); + events.Register(); + + if (events == null) + throw new Exception("Events not initialized"); + + return events.EventTypes.ToImmutableList(); + } +} \ No newline at end of file diff --git a/EliteAPI.Tests/EliteAPI.Tests.csproj b/EliteAPI.Tests/EliteAPI.Tests.csproj index 376af5a0..16cec1f3 100644 --- a/EliteAPI.Tests/EliteAPI.Tests.csproj +++ b/EliteAPI.Tests/EliteAPI.Tests.csproj @@ -5,10 +5,10 @@ enable false true - 3.0.15.0 - 3.0.15.0 - 3.0.15+13.Branch.main.Sha.aa3bd21915f470956d90f87a2d089e6d67be09ae - 3.0.15 + 3.1.0.0 + 3.1.0.0 + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de + 3.1.0-alpha0182 diff --git a/EliteAPI.Tests/JournalManual.cs b/EliteAPI.Tests/JournalManual.cs index 4ce81e63..268e4421 100644 --- a/EliteAPI.Tests/JournalManual.cs +++ b/EliteAPI.Tests/JournalManual.cs @@ -17,18 +17,19 @@ namespace EliteAPI.Tests; public class JournalManual { private static IEvents _events; - private static string[] _legacyEvents = { "BackpackMaterials", "BuyMicroResources", "ShipTargetted" }; + private static string[] _legacyEvents = { "BackpackMaterials", "BuyMicroResources", "ShipTargetted", "CarrierNameChanged" }; private static string[] _legacyExamples = { "\"timestamp\":\"2020-04-27T08:02:52Z\", \"event\":\"Route\"", - "\"timestamp\":\"2020-04-27T08:02:52Z\", \"event\":\"Route\"" + "\"timestamp\":\"2020-04-27T08:02:52Z\", \"event\":\"Route\"", + "\"timestamp\":\"2020-10-07T14:01:08Z\", \"event\":\"BuyMicroResource\"", }; [OneTimeSetUp] public void Setup() { var eventParser = new EventParser(Mock.Of()); - eventParser.Use();; + eventParser.Use(); _events = new Events.Events(Mock.Of>(), eventParser); _events.Register(); } @@ -66,12 +67,22 @@ public void Json(string json) return; } - _events.Invoke(json, new EventContext()); + var invokedEvent = _events.Invoke(json, new EventContext()); + + Assert.That(invokedEvent, Is.Not.Null, $"Event is null"); + + // Check if the event is the correct type + var eventType = invokedEvent.GetType(); + var eventName = eventType.Name; + if (eventName.EndsWith("Event")) + eventName = eventName.Substring(0, eventName.Length - 5); + + Assert.That(string.Equals(eventName, invokedEvent.Event, StringComparison.CurrentCultureIgnoreCase), $"Event is not of type {eventName} but {invokedEvent.Event}"); } [Test(Description = "Properties")] [TestCaseSource(nameof(GetProperties))] - [Ignore("Tests still in progress")] + [Ignore("This test is not finished")] public void Properties((string eventName, Property property) propertyInfo) { var (eventName, expectedProperty) = propertyInfo; @@ -86,7 +97,7 @@ public void Properties((string eventName, Property property) propertyInfo) // Get the property by the JsonProperty attribute var property = properties.FirstOrDefault(x => string.Equals(x.GetCustomAttribute()?.PropertyName, expectedProperty.Name, StringComparison.CurrentCultureIgnoreCase)); - Warn.If(property, Is.Not.Null, $"Type '{eventType.Name}' does not contain expected property '{expectedProperty.Name}'"); + Assert.That(property, Is.Not.Null, $"Type '{eventType.Name}' does not contain expected property '{expectedProperty.Name}'"); if (property == null) return; @@ -95,13 +106,13 @@ public void Properties((string eventName, Property property) propertyInfo) foreach (var expectedChild in expectedProperty.Children) { var childProperty = property.PropertyType.GetProperties().FirstOrDefault(x => string.Equals(x.GetCustomAttribute()?.PropertyName, expectedChild.Name, StringComparison.CurrentCultureIgnoreCase)); - Warn.If(childProperty, Is.Not.Null, $"Type '{property.PropertyType.Name}' does not contain expected child property '{expectedChild.Name}'"); + Assert.That(childProperty, Is.Not.Null, $"Type '{property.PropertyType.Name}' does not contain expected child property '{expectedChild.Name}'"); // Check the child's children foreach (var expectedGrandChild in expectedChild.Children) { var childChildProperty = childProperty.PropertyType.GetProperties().FirstOrDefault(x => string.Equals(x.GetCustomAttribute()?.PropertyName, expectedGrandChild.Name, StringComparison.CurrentCultureIgnoreCase)); - Warn.If(childChildProperty, Is.Not.Null, $"Type '{childProperty.PropertyType.Name}' does not contain expected child property '{expectedGrandChild.Name}'"); + Assert.That(childChildProperty, Is.Not.Null, $"Type '{childProperty.PropertyType.Name}' does not contain expected child property '{expectedGrandChild.Name}'"); } } } diff --git a/EliteAPI.Tests/Schemas.cs b/EliteAPI.Tests/Schemas.cs index 99b752ee..76723b2b 100644 --- a/EliteAPI.Tests/Schemas.cs +++ b/EliteAPI.Tests/Schemas.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Immutable; using System.Diagnostics; using System.Reflection; using EliteAPI.Abstractions.Events; @@ -11,130 +12,208 @@ namespace EliteAPI.Tests; [TestFixture] -[Ignore("Tests still in progress")] public class Schemas { private static IEvents _events; - - private static string[] _legacyEvents = { "BackpackMaterials", "BuyMicroResources", "ShipTargetted" }; - private static string[] _legacyExamples = - { - "\"timestamp\":\"2020-04-27T08:02:52Z\", \"event\":\"Route\"", - "\"timestamp\":\"2020-04-27T08:02:52Z\", \"event\":\"Route\"" - }; - + [OneTimeSetUp] public void Setup() { var eventParser = new EventParser(Mock.Of()); - eventParser.Use();; + eventParser.Use(); + _events = new Events.Events(Mock.Of>(), eventParser); _events.Register(); } - + [Test(Description = "Properties")] - [TestCaseSource(nameof(GetSchemas))] - public void Properties((string name, string schema) schemaInfo) + [TestCaseSource(nameof(GetProperties))] + [Ignore("Tests are being worked on")] + public void Properties((string name, string type) schemaInfo) { - var (name, schema) = schemaInfo; - name += "Event"; - - var eventType = _events.EventTypes.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.CurrentCultureIgnoreCase)); + var eventName = schemaInfo.name.Split('.')[0] + "Event"; + var name = string.Join(".", schemaInfo.name.Split('.').Skip(1)); + var type = schemaInfo.type; - Assert.That(eventType, Is.Not.Null, $"Event {name} not found"); - if (eventType == null) - return; - - var properties = eventType.GetProperties(); - var expectedProperties = JObject.Parse(schema)["properties"]!.ToObject>(); + var eventType = _events.EventTypes.FirstOrDefault(x => x.Name == eventName); + Assert.That(eventType, Is.Not.Null, $"Event type {eventName} not found"); - foreach (var expectedProperty in expectedProperties!) + if (type == "object") { - if(expectedProperty.Key.EndsWith("_Localised")) - continue; + // Skip + Assert.Pass(); + } + else if (type == "array") + { + // Skip + Assert.Pass(); + } + else + { + // Skip nested properties + if(name.Any(x => x == '.')) + Assert.Pass(); - var property = properties.FirstOrDefault(x => - string.Equals(x.GetCustomAttribute()?.PropertyName, expectedProperty.Key, - StringComparison.CurrentCultureIgnoreCase)); + var properties = eventType! + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.GetCustomAttributes().Any()); - var expectedType = expectedProperty.Value["type"]!.ToObject(); - - Assert.That(property, Is.Not.Null, $"Property {name}.{expectedProperty.Key} was not found (type {expectedType})"); - if (property == null) - continue; + var jsonProperty = + properties.FirstOrDefault(x => string.Equals(x.GetCustomAttribute()!.PropertyName, name, StringComparison.InvariantCultureIgnoreCase)); + Assert.That(jsonProperty, Is.Not.Null, $"Property {name} not found on event {eventName}"); - // Make all IDs strings - if(expectedProperty.Key.EndsWith("ID") || expectedProperty.Key.EndsWith("Address")) - expectedType = "string"; - - var type = ConvertToJTokenType(property.PropertyType); + var jsonPropertyType = jsonProperty!.PropertyType; + var jsonPropertyTypeName = ConvertToJTokenType(jsonPropertyType); - Assert.That(string.Equals(type.ToString(), expectedType, StringComparison.CurrentCultureIgnoreCase), Is.True, - $"Property {name}.{expectedProperty.Key} is not the expected type '{expectedType}' (is {type})"); + Assert.That(jsonPropertyTypeName, Is.EqualTo(type), + $"Property {name} on event {eventName} is of type {jsonPropertyTypeName} but should be of type {type}"); } } - + [Test(Description = "Events")] [TestCaseSource(nameof(GetSchemas))] public void Event((string name, string schema) schemaInfo) { var (name, schema) = schemaInfo; + name += "Event"; - + Assert.That(_events.EventTypes.Select(x => x.Name.ToLower()), Does.Contain(name.ToLower())); } - - private static IEnumerable<(string name, string schema)> GetSchemas() + + + static IImmutableList<(string name, string type)> GetProperties() { - Process.Start("git", "clone https://github.com/jixxed/ed-journal-schemas.git schemas-repo").WaitForExit(); - + var result = new List<(string, string)>(); + + foreach (var (name, schema) in GetSchemas()) + { + Console.WriteLine($"Processing {name}"); + + var schemaObject = JObject.Parse(schema); + var properties = schemaObject["properties"]?.Children(); + + if (properties == null) + continue; + + result.AddRange(ExtractProperties(properties, name)); + } + + return result.ToImmutableList(); + } + + static IEnumerable<(string name, string type)> ExtractProperties(IEnumerable properties, + string namePrefix = "") + { + foreach (var property in properties) + { + var type = property.Value["type"]?.Value(); + var name = property.Value["title"]?.Value(); + + if (type == null || name == null) + continue; + + if(name.EndsWith("_Localised")) + continue; + + if(name.EndsWith("ID") || name.EndsWith("Address") || name.EndsWith("Market")) + type = "string"; + + var propertyName = (namePrefix + "." + name).TrimStart('.').TrimEnd('.').Replace("..", "."); + yield return (propertyName, type); + + switch (type) + { + case "array": + { + var items = property.Value.Value("items")?.Value("properties") + ?.Children(); + + if (items == null) + continue; + + var arrayProperties = ExtractProperties(items, namePrefix + "." + name); + + foreach (var arrayProperty in arrayProperties) + yield return arrayProperty; + break; + } + case "object": + { + var items = property.Value["properties"]?.Children(); + + if (items == null) + continue; + + var objectProperties = ExtractProperties(items, namePrefix + "." + name); + + foreach (var objectProperty in objectProperties) + yield return objectProperty; + break; + } + } + } + } + + static IEnumerable<(string name, string schema)> GetSchemas() + { + Process.Start("git", "clone https://github.com/Somfic/journal-schemas.git schemas-repo").WaitForExit(); + var files = Directory.GetFiles("schemas-repo", "*.json", SearchOption.AllDirectories); var schemas = new List<(string, string)>(); - + foreach (var file in files) { var name = Path.GetFileNameWithoutExtension(file); + + if (name is "_Event" or "ShipLockerBackpack" or "ShipLockerMaterials") + continue; + var schema = File.ReadAllText(file); - + schemas.Add((name, schema)); } - + return schemas; } - + private static string ConvertToJTokenType(Type type) { if (type == typeof(string) || type == typeof(Localised)) { return "string"; } - else if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte)) + + if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte)) { return "integer"; } - else if (type == typeof(float) || type == typeof(double) || type == typeof(decimal)) + + if (type == typeof(float) || type == typeof(double) || type == typeof(decimal)) { return "number"; } - else if (type == typeof(bool)) + + if (type == typeof(bool)) { return "boolean"; } - else if (type == typeof(DateTime)) + + if (type == typeof(DateTime)) { return "string"; } - else if (type.GetInterfaces().Any(x => x == typeof(IEnumerable))) + + if (type.GetInterfaces().Any(x => x == typeof(IEnumerable))) { return "array"; } - else if (type.IsValueType) + + if (type.IsValueType) { return "object"; } - else - { - return "unknown"; - } + + return "unknown"; } } \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/EliteAPI.Web.Spansh.csproj b/EliteAPI.Web.Spansh/EliteAPI.Web.Spansh.csproj new file mode 100644 index 00000000..b2147031 --- /dev/null +++ b/EliteAPI.Web.Spansh/EliteAPI.Web.Spansh.csproj @@ -0,0 +1,21 @@ + + + netstandard2.0 + enable + enable + default + 3.1.0.0 + 3.1.0.0 + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de + 3.1.0-alpha0182 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/RoutePlanner/Requests/NeutronRequest.cs b/EliteAPI.Web.Spansh/RoutePlanner/Requests/NeutronRequest.cs new file mode 100644 index 00000000..f254767b --- /dev/null +++ b/EliteAPI.Web.Spansh/RoutePlanner/Requests/NeutronRequest.cs @@ -0,0 +1,32 @@ +using EliteAPI.Web.Attributes; +using EliteAPI.Web.Models; + +namespace EliteAPI.Web.Spansh.RoutePlanner.Requests; + +public class NeutronRequest : IWebApiRequest +{ + public NeutronRequest(string from, string to, int range = 10) + { + From = from; + To = to; + Range = range; + } + + [QueryParameter("efficiency")] + public int Efficiency { get; init; } = 60; + + [QueryParameter("range")] + public int Range { get; init; } + + [QueryParameter("from")] + public string From { get; init; } + + [QueryParameter("via")] + public string Via { get; init; } + + [QueryParameter("to")] + public string To { get; init; } + + public string Endpoint => "route"; + public HttpMethod Method => HttpMethod.Post; +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/RoutePlanner/Requests/TradeRequest.cs b/EliteAPI.Web.Spansh/RoutePlanner/Requests/TradeRequest.cs new file mode 100644 index 00000000..0a26b318 --- /dev/null +++ b/EliteAPI.Web.Spansh/RoutePlanner/Requests/TradeRequest.cs @@ -0,0 +1,57 @@ +using EliteAPI.Web.Attributes; +using EliteAPI.Web.Models; + +namespace EliteAPI.Web.Spansh.RoutePlanner.Requests; + +public class TradeRequest : IWebApiRequest +{ + public TradeRequest(string startingSystem, string startingStation, int range, int capacity) + { + StartingSystem = startingSystem; + StartingStation = startingStation; + JumpRange = range; + CargoCapacity = capacity; + } + + [QueryParameter("system")] + public string StartingSystem { get; init; } + + [QueryParameter("station")] + public string StartingStation { get; init; } + + [QueryParameter("starting_capital")] + public int StartingCapital { get; init; } = 100_000_000; + + [QueryParameter("max_hop_distance")] + public int JumpRange { get; init; } + + [QueryParameter("max_cargo")] + public int CargoCapacity { get; init; } + + [QueryParameter("max_hops")] + public int AmountOfStops { get; init; } = 5; + + [QueryParameter("max_system_distance")] + public int MaxDistanceToArrival { get; init; } = 10_000; + + [QueryParameter("max_price_age")] + public int MaxPriceAge { get; init; } = 60 * 60 * 24 * 7; // 7 days + + [QueryParameter("requires_large_pad")] + public bool RequiresLargePad { get; init; } = false; + + [QueryParameter("allow_planetary")] + public bool AllowPlanetary { get; init; } = false; + + [QueryParameter("allow_prohibited")] + public bool AllowProhibited { get; init; } = false; + + [QueryParameter("permit")] + public bool AllowPermitSystems { get; init; } = false; + + [QueryParameter("unique")] + public bool AvoidLoops { get; init; } = false; + + public string Endpoint => "trade/route"; + public HttpMethod Method => HttpMethod.Post; +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/RoutePlanner/Responses/NeutronResponse.cs b/EliteAPI.Web.Spansh/RoutePlanner/Responses/NeutronResponse.cs new file mode 100644 index 00000000..e7d73b67 --- /dev/null +++ b/EliteAPI.Web.Spansh/RoutePlanner/Responses/NeutronResponse.cs @@ -0,0 +1,64 @@ +using EliteAPI.Web.Models; +using Newtonsoft.Json; + +namespace EliteAPI.Web.Spansh.RoutePlanner.Responses; + +public class NeutronResponse +{ + [JsonProperty("destination_system")] + public string DestinationSystem { get; init; } + + [JsonProperty("distance")] + public double Distance { get; init; } + + [JsonProperty("efficiency")] + public long Efficiency { get; init; } + + [JsonProperty("job")] + public string Job { get; init; } + + [JsonProperty("range")] + public long Range { get; init; } + + [JsonProperty("source_system")] + public string SourceSystem { get; init; } + + [JsonProperty("system_jumps")] + public IReadOnlyCollection SystemJumps { get; init; } + + [JsonProperty("total_jumps")] + public long TotalJumps { get; init; } + + [JsonProperty("via")] + public string[] Via { get; init; } + + public class SystemJump + { + [JsonProperty("distance_jumped")] + public double DistanceJumped { get; init; } + + [JsonProperty("distance_left")] + public double DistanceLeft { get; init; } + + [JsonProperty("id64")] + public long Id { get; init; } + + [JsonProperty("jumps")] + public long Jumps { get; init; } + + [JsonProperty("neutron_star")] + public bool NeutronStar { get; init; } + + [JsonProperty("system")] + public string System { get; init; } + + [JsonProperty("x")] + public double X { get; init; } + + [JsonProperty("y")] + public double Y { get; init; } + + [JsonProperty("z")] + public double Z { get; init; } + } +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/RoutePlanner/Responses/TradeResponse.cs b/EliteAPI.Web.Spansh/RoutePlanner/Responses/TradeResponse.cs new file mode 100644 index 00000000..7fb82a21 --- /dev/null +++ b/EliteAPI.Web.Spansh/RoutePlanner/Responses/TradeResponse.cs @@ -0,0 +1,67 @@ +using EliteAPI.Web.Models; +using Newtonsoft.Json; + +namespace EliteAPI.Web.Spansh.RoutePlanner.Responses; + +public class TradeResponse : IWebApiResponse +{ + [JsonProperty("commodities")] public IReadOnlyCollection Commodities { get; set; } + + [JsonProperty("cumulative_profit")] public long CumulativeProfit { get; set; } + + [JsonProperty("destination")]public SystemInfo Destination { get; set; } + + [JsonProperty("distance")] public double Distance { get; set; } + + [JsonProperty("source")] public SystemInfo System { get; set; } + + [JsonProperty("total_profit")] public long TotalProfit { get; set; } + + public class Commodity + { + [JsonProperty("amount")] public long Amount { get; set; } + + [JsonProperty("destination_commodity")] + public TradeCommodity DestinationCommodity { get; set; } + + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("profit")] public long Profit { get; set; } + + [JsonProperty("source_commodity")] public TradeCommodity SourceCommodity { get; set; } + + [JsonProperty("total_profit")] public long TotalProfit { get; set; } + } + + public class TradeCommodity + { + [JsonProperty("buy_price")] public long BuyPrice { get; set; } + + [JsonProperty("demand")] public long Demand { get; set; } + + [JsonProperty("sell_price")] public long SellPrice { get; set; } + + [JsonProperty("supply")] public long Supply { get; set; } + } + + public class SystemInfo + { + [JsonProperty("distance_to_arrival")] public long DistanceToArrival { get; set; } + + [JsonProperty("market_id")] public long MarketId { get; set; } + + [JsonProperty("market_updated_at")] public long MarketUpdatedAt { get; set; } + + [JsonProperty("station")] public string Station { get; set; } + + [JsonProperty("system")] public string Name { get; set; } + + [JsonProperty("system_id64")] public long Id { get; set; } + + [JsonProperty("x")] public double X { get; set; } + + [JsonProperty("y")] public double Y { get; set; } + + [JsonProperty("z")] public double Z { get; set; } + } +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/RoutePlanner/RoutePlannerApi.cs b/EliteAPI.Web.Spansh/RoutePlanner/RoutePlannerApi.cs new file mode 100644 index 00000000..321cf021 --- /dev/null +++ b/EliteAPI.Web.Spansh/RoutePlanner/RoutePlannerApi.cs @@ -0,0 +1,38 @@ +using EliteAPI.Web.Models; +using EliteAPI.Web.Spansh.RoutePlanner.Requests; +using EliteAPI.Web.Spansh.RoutePlanner.Responses; +using EliteAPI.Web.Spansh.Utilities.Requests; +using EliteAPI.Web.Spansh.Utilities.Responses; +using Microsoft.Extensions.Logging; +using Somfic.Common; + +namespace EliteAPI.Web.Spansh.RoutePlanner; + +public class RoutePlannerApi : WebApiCategory +{ + private readonly ILogger _log; + + public RoutePlannerApi(WebApi api, ILogger log) : base(api) + { + _log = log; + } + + protected override string Endpoint => ""; + + public async Task> Neutron(NeutronRequest request) + { + var api = Api as SpanshApi; + + var job = await Execute(request); + + return await api.Utilities.FromJob(job); + } + + public async Task>> Trade(TradeRequest request) + { + var api = Api as SpanshApi; + var job = await Execute(request); + return await api.Utilities.FromJob>(job); + } + +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/SpanshApi.cs b/EliteAPI.Web.Spansh/SpanshApi.cs new file mode 100644 index 00000000..26229861 --- /dev/null +++ b/EliteAPI.Web.Spansh/SpanshApi.cs @@ -0,0 +1,21 @@ +using EliteAPI.Web.Models; +using EliteAPI.Web.Spansh.RoutePlanner; +using EliteAPI.Web.Spansh.Utilities; +using Newtonsoft.Json; + +namespace EliteAPI.Web.Spansh; + +public class SpanshApi : WebApi +{ + public SpanshApi(IServiceProvider services) : base(services) + { + Routes = AddCategory(); + Utilities = AddCategory(); + } + + protected override string BaseUrl => "https://www.spansh.co.uk/api"; + + public RoutePlannerApi Routes { get; } + + internal UtilitiesApi Utilities { get; } +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/Utilities/Requests/JobRequest.cs b/EliteAPI.Web.Spansh/Utilities/Requests/JobRequest.cs new file mode 100644 index 00000000..e31b1261 --- /dev/null +++ b/EliteAPI.Web.Spansh/Utilities/Requests/JobRequest.cs @@ -0,0 +1,15 @@ +using EliteAPI.Web.Models; + +namespace EliteAPI.Web.Spansh.Utilities.Requests; + +public class JobRequest : IWebApiRequest +{ + public JobRequest(string job) + { + Job = job; + } + + public string Job { get; init; } + public string Endpoint => $"results/{Job}"; + public HttpMethod Method => HttpMethod.Get; +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/Utilities/Requests/SystemRequest.cs b/EliteAPI.Web.Spansh/Utilities/Requests/SystemRequest.cs new file mode 100644 index 00000000..0a766940 --- /dev/null +++ b/EliteAPI.Web.Spansh/Utilities/Requests/SystemRequest.cs @@ -0,0 +1,18 @@ +using EliteAPI.Web.Attributes; +using EliteAPI.Web.Models; + +namespace EliteAPI.Web.Spansh.Utilities.Requests; + +public class SystemRequest : IWebApiRequest +{ + public SystemRequest(string query) + { + Query = query; + } + + [QueryParameter("q")] + public string Query { get; init; } + + public string Endpoint => "systems"; + public HttpMethod Method => HttpMethod.Post; +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/Utilities/Responses/JobResponse.cs b/EliteAPI.Web.Spansh/Utilities/Responses/JobResponse.cs new file mode 100644 index 00000000..ca6d624e --- /dev/null +++ b/EliteAPI.Web.Spansh/Utilities/Responses/JobResponse.cs @@ -0,0 +1,22 @@ +using EliteAPI.Web.Models; +using Newtonsoft.Json; + +namespace EliteAPI.Web.Spansh.Utilities.Responses; + +public class JobResponse : IWebApiResponse +{ + [JsonProperty("result")] + public T Result { get; init; } + + [JsonProperty("status")] + public string Status { get; init; } +} + +public class JobResponse : IWebApiResponse +{ + [JsonProperty("job")] + public string Job { get; init; } + + [JsonProperty("status")] + public string Status { get; init; } +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/Utilities/Responses/SystemResponse.cs b/EliteAPI.Web.Spansh/Utilities/Responses/SystemResponse.cs new file mode 100644 index 00000000..24b5a88c --- /dev/null +++ b/EliteAPI.Web.Spansh/Utilities/Responses/SystemResponse.cs @@ -0,0 +1,10 @@ +using EliteAPI.Web.Models; +using Newtonsoft.Json; + +namespace EliteAPI.Web.Spansh.Utilities.Responses; + +public class SystemResponse : IWebApiResponse +{ + [JsonProperty("root")] // Json root attribute + public string[] Systems { get; init; } +} \ No newline at end of file diff --git a/EliteAPI.Web.Spansh/Utilities/UtilitiesApi.cs b/EliteAPI.Web.Spansh/Utilities/UtilitiesApi.cs new file mode 100644 index 00000000..37c7d4da --- /dev/null +++ b/EliteAPI.Web.Spansh/Utilities/UtilitiesApi.cs @@ -0,0 +1,60 @@ +using EliteAPI.Web.Models; +using EliteAPI.Web.Spansh.RoutePlanner.Responses; +using EliteAPI.Web.Spansh.Utilities.Requests; +using EliteAPI.Web.Spansh.Utilities.Responses; +using Microsoft.Extensions.Logging; +using Somfic.Common; + +namespace EliteAPI.Web.Spansh.Utilities; + +public class UtilitiesApi : WebApiCategory +{ + private readonly ILogger _log; + + public UtilitiesApi(ILogger log, WebApi api) : base(api) + { + _log = log; + } + + protected override string Endpoint => ""; + + public async Task>> System(SystemRequest request) + { + return await Execute(request); + } + + internal async Task>>> Job(JobRequest request) + { + while (true) + { + var result = await Execute>(request); + + if (result.IsError) + return result; + + var response = result.Expect(); + + if (response.Content.Status == "queued") + { + await Task.Delay(1000); + continue; + } + + return result; + } + } + + internal async Task> FromJob(WebApiResponse> job) where TResponse : class + { + var api = Api as SpanshApi; + + var result = await job.MapAsync( + ok: x => api.Utilities.Job(new JobRequest(x.Content.Job)), + error: x => x + ); + + return result.Map>( + ok: x => x.Content.Result as TResponse, + error: x => x); + } +} \ No newline at end of file diff --git a/EliteAPI.Web/Abstractions/IWebApiCategory.cs b/EliteAPI.Web/Abstractions/IWebApiCategory.cs new file mode 100644 index 00000000..1d5207d7 --- /dev/null +++ b/EliteAPI.Web/Abstractions/IWebApiCategory.cs @@ -0,0 +1,22 @@ +using EliteAPI.Web.Models; + +namespace EliteAPI.Web.Abstractions; + +public interface IWebApiCategory +{ + /// Executes a HTTP request. + /// The generic type. The request will be instantiated using the provider + /// The generic type + /// The parsed generic with the type response + internal Task> Execute() + where TResponse : IWebApiResponse where TRequest : IWebApiRequest; + + /// Executes a HTTP request. + /// The instance of the type + /// The generic type + /// The generic type + /// The parsed generic with the type response + /// Throws a if the body could not be parsed into + internal Task> Execute(TRequest? request) + where TResponse : IWebApiResponse where TRequest : IWebApiRequest; +} \ No newline at end of file diff --git a/EliteAPI.Web/Attributes/BodyParameterAttribute.cs b/EliteAPI.Web/Attributes/BodyParameterAttribute.cs new file mode 100644 index 00000000..6a03efaa --- /dev/null +++ b/EliteAPI.Web/Attributes/BodyParameterAttribute.cs @@ -0,0 +1,27 @@ +namespace EliteAPI.Web.Attributes; + +/// A parameter in the body of a request. +[AttributeUsage(AttributeTargets.Property)] +public class BodyParameterAttribute : Attribute +{ + public BodyParameterAttribute(string key) + { + Key = key; + } + + /// The key of the parameter in the body. + public string Key { get; } +} + +/// A secret value extracted through previous authentication. +[AttributeUsage(AttributeTargets.Property)] +public class SecretAttribute : Attribute +{ + public SecretAttribute(string key) + { + Key = key; + } + + /// The key of the secret. + public string Key { get; } +} \ No newline at end of file diff --git a/EliteAPI.Web/Attributes/QueryParameterAttribute.cs b/EliteAPI.Web/Attributes/QueryParameterAttribute.cs new file mode 100644 index 00000000..ea2f2bcd --- /dev/null +++ b/EliteAPI.Web/Attributes/QueryParameterAttribute.cs @@ -0,0 +1,14 @@ +namespace EliteAPI.Web.Attributes; + +/// A parameter in the query of a request. +[AttributeUsage(AttributeTargets.Property)] +public class QueryParameterAttribute : Attribute +{ + public QueryParameterAttribute(string key) + { + Key = key; + } + + /// The key of the parameter in the query. + public string Key { get; } +} \ No newline at end of file diff --git a/EliteAPI.Web/EliteAPI.Web.csproj b/EliteAPI.Web/EliteAPI.Web.csproj new file mode 100644 index 00000000..58d006bb --- /dev/null +++ b/EliteAPI.Web/EliteAPI.Web.csproj @@ -0,0 +1,21 @@ + + + enable + enable + default + netstandard2.0 + 3.1.0.0 + 3.1.0.0 + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de + 3.1.0-alpha0182 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/EliteAPI.Web/Exceptions/WebApiException.cs b/EliteAPI.Web/Exceptions/WebApiException.cs new file mode 100644 index 00000000..9b6bac2f --- /dev/null +++ b/EliteAPI.Web/Exceptions/WebApiException.cs @@ -0,0 +1,18 @@ +namespace EliteAPI.Web.Exceptions; + +public class WebApiException : Exception +{ + public WebApiException(string error, HttpResponseMessage response) : base(BuildMessage(error, response)) + { + } + + public WebApiException(string error, HttpResponseMessage response, Exception innerException) : base( + BuildMessage(error, response), innerException) + { + } + + private static string BuildMessage(string error, HttpResponseMessage response) + { + return $"{(int) response.StatusCode} request failed ({response.StatusCode}). {response.ReasonPhrase}: {error}."; + } +} \ No newline at end of file diff --git a/EliteAPI.Web/Extensions/WebApiHttpContentExtensions.cs b/EliteAPI.Web/Extensions/WebApiHttpContentExtensions.cs new file mode 100644 index 00000000..e11e5dd4 --- /dev/null +++ b/EliteAPI.Web/Extensions/WebApiHttpContentExtensions.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +// ReSharper disable once CheckNamespace +namespace System.Net.Http; + +public static class WebApiHttpContentExtensions +{ + public static async Task<(T parsed, string json)> ReadAsJsonAsync(this HttpContent content) + { + var json = await content.ReadAsStringAsync(); + + var jObject = JToken.Parse(json); + if (jObject.Type == JTokenType.Array) json = "{ root: " + json + " }"; // todo: fix this lol + + var body = JsonConvert.DeserializeObject(json); + + if (body == null) + throw new JsonException("Could not parse the request's response into the given JSON schema.") + {Data = {["Json"] = json, ["Schema"] = typeof(T).Name}}; + + return (body, json); + } +} \ No newline at end of file diff --git a/EliteAPI.Web/Extensions/WebApiServiceCollectionExtensions.cs b/EliteAPI.Web/Extensions/WebApiServiceCollectionExtensions.cs new file mode 100644 index 00000000..adda5953 --- /dev/null +++ b/EliteAPI.Web/Extensions/WebApiServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using EliteAPI.Web; +using Microsoft.Extensions.DependencyInjection.Extensions; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection; + +public static class WebApiServiceCollectionExtensions +{ + /// Adds EliteAPI services to the specified . + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddWebApi(this IServiceCollection services) where T : WebApi + { + services.AddHttpClient(typeof(T).Name); + + services.TryAddSingleton(); + + return services; + } + + // todo: add overload with configuration +} \ No newline at end of file diff --git a/EliteAPI.Web/Models/IWebApiRequest.cs b/EliteAPI.Web/Models/IWebApiRequest.cs new file mode 100644 index 00000000..6ddf0382 --- /dev/null +++ b/EliteAPI.Web/Models/IWebApiRequest.cs @@ -0,0 +1,10 @@ +namespace EliteAPI.Web.Models; + +public interface IWebApiRequest +{ + /// The endpoint of the request, based on the category. + public string Endpoint { get; } + + /// The HTTP method of the request. + public HttpMethod Method { get; } +} \ No newline at end of file diff --git a/EliteAPI.Web/Models/IWebApiResponse.cs b/EliteAPI.Web/Models/IWebApiResponse.cs new file mode 100644 index 00000000..cfca68da --- /dev/null +++ b/EliteAPI.Web/Models/IWebApiResponse.cs @@ -0,0 +1,5 @@ +namespace EliteAPI.Web.Models; + +public interface IWebApiResponse +{ +} \ No newline at end of file diff --git a/EliteAPI.Web/Models/WebApiResponse.cs b/EliteAPI.Web/Models/WebApiResponse.cs new file mode 100644 index 00000000..c585becd --- /dev/null +++ b/EliteAPI.Web/Models/WebApiResponse.cs @@ -0,0 +1,59 @@ +namespace EliteAPI.Web.Models; + +public readonly struct WebApiResponse +{ + private readonly TValue? _value; + private readonly Exception? _error; + + public static implicit operator WebApiResponse(TValue value) => new(value); + private WebApiResponse(TValue value) + { + _value = value; + _error = default; + IsOkay = true; + } + + public static implicit operator WebApiResponse(Exception error) => new(error); + private WebApiResponse(Exception error) + { + _value = default; + _error = error; + IsOkay = false; + } + + public bool IsOkay { get; } + + public bool IsError => !IsOkay; + + public WebApiResponse On(Action ok, Action error) + { + if (IsOkay) + ok(_value!); + else + error(_error!); + return this; + } + + + public TResult Map(Func ok, Func error) => + IsOkay ? ok(_value!) : error(_error!); + + public WebApiResponse Map(Func ok) => + IsOkay ? ok(_value!) : _error!; + + public async Task MapAsync(Func> ok, Func error) + { + return IsOkay ? await ok(_value!) : error(_error!); + } + + public async Task> MapAsync(Func> ok) + { + return IsOkay ? await ok(_value!) : _error!; + } + + public TValue Expect() + { + if (IsOkay) return _value!; + throw _error!; + } +} \ No newline at end of file diff --git a/EliteAPI.Web/Models/WebApiResult.cs b/EliteAPI.Web/Models/WebApiResult.cs new file mode 100644 index 00000000..7c128e05 --- /dev/null +++ b/EliteAPI.Web/Models/WebApiResult.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace EliteAPI.Web.Models; + +public readonly struct WebApiResult where T : IWebApiResponse +{ + private readonly HttpResponseMessage _response; + private readonly Exception _exception; + + public WebApiResult(HttpResponseMessage response, (T parsed, string raw) result) + { + _response = response; + Content = result.parsed; + RawContent = result.raw; + } + + public T Content { get; } + public string RawContent { get; } + public HttpContentHeaders ContentHeaders { get; } + public HttpResponseHeaders Headers { get; } + public bool IsSuccessStatusCode { get; } + public Version Version { get; } + public HttpStatusCode StatusCode { get; } + public string? ReasonPhrase { get; } + + public void EnsureSuccessStatusCode() + { + _response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/EliteAPI.Web/WebApi.cs b/EliteAPI.Web/WebApi.cs new file mode 100644 index 00000000..e7e15494 --- /dev/null +++ b/EliteAPI.Web/WebApi.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace EliteAPI.Web; + +public abstract class WebApi +{ + internal readonly IServiceProvider Services; + private readonly IDictionary _secrets = new Dictionary(); + + protected WebApi(IServiceProvider services) + { + Services = services; + } + + protected internal abstract string BaseUrl { get; } + + protected T AddCategory(TWebApi api) where T : WebApiCategory + { + return ActivatorUtilities.CreateInstance(Services, api); + } + + protected T AddCategory() where T : WebApiCategory + { + return ActivatorUtilities.CreateInstance(Services, this); + } + + protected void SetSecret(string key, string value) + { + _secrets[key] = value; + } + + protected internal string GetSecret(string key) + { + if (!_secrets.ContainsKey(key)) throw new Exception($"Secret {key} not found"); + + return _secrets[key]; + } + + protected internal bool HasSecret(string key) + { + return _secrets.ContainsKey(key); + } +} \ No newline at end of file diff --git a/EliteAPI.Web/WebApiCategory.cs b/EliteAPI.Web/WebApiCategory.cs new file mode 100644 index 00000000..ac68aadf --- /dev/null +++ b/EliteAPI.Web/WebApiCategory.cs @@ -0,0 +1,129 @@ +using System.Reflection; +using System.Web; +using EliteAPI.Web.Attributes; +using EliteAPI.Web.Exceptions; +using EliteAPI.Web.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace EliteAPI.Web; + +public abstract class WebApiCategory +{ + private readonly ILogger _log; + + protected WebApiCategory(WebApi api) + { + Api = api; + Http = api.Services.GetRequiredService().CreateClient(); //todo: config through name here? + _log = api.Services.GetRequiredService>(); + } + + protected WebApi Api { get; } + protected HttpClient Http { get; } + + protected abstract string Endpoint { get; } + + protected internal async Task>> Execute() where TResponse : IWebApiResponse + where TRequest : IWebApiRequest + { + try + { + var request = ActivatorUtilities.CreateInstance(Api.Services); + return await Execute(request); + } + catch (Exception ex) + { + return ex; + } + } + + protected internal async Task>> Execute(TRequest? request) + where TResponse : IWebApiResponse where TRequest : IWebApiRequest + { + if (request == null) + return await Execute(); + + var requestMessage = BuildRequest(this, request); + + var response = await Http.SendAsync(requestMessage); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + var errorObject = JObject.Parse(errorContent); + var error = errorObject["error"]?.ToString() ?? "Unknown error"; + + var ex = new WebApiException(error, response); + + return ex; + } + + // Deserialize the response + var body = await response.Content.ReadAsJsonAsync(); + + return new WebApiResult(response, body); + } + + private HttpRequestMessage BuildRequest(WebApiCategory category, IWebApiRequest request) + { + // Trim the url segments + var baseUri = category.Api.BaseUrl.Trim('/'); + var categoryEndpoint = category.Endpoint.Trim('/'); + var requestEndpoint = request.Endpoint.Trim('/'); + + var apiPart = $"{categoryEndpoint}/{requestEndpoint}".Replace("//", "/").Trim('/'); + + // Build the request URI + var uriBuilder = new UriBuilder($"{baseUri}/{apiPart}"); + + // Create a query builder + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + + // Find all the properties with the QueryParameter attribute + var queryProperties = request + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.GetCustomAttributes(typeof(QueryParameterAttribute), true).Any()); + + // Add the query parameters + foreach (var property in queryProperties) + { + var key = property.GetCustomAttribute()?.Key; + var value = GetPropertyValue(property, request); + + query.Add(key, value); + } + + // Apply the query parameters + uriBuilder.Query = query.ToString(); + + // Extract the URI + var uri = uriBuilder.Uri; + + //todo: add headers + //todo: add body + + return new HttpRequestMessage(request.Method, uri); + } + + private string GetPropertyValue(PropertyInfo property, object instance) + { + var value = property.GetValue(instance)?.ToString(); + + var attribute = property.GetCustomAttribute(); + + if (attribute != null) + { + var key = attribute.Key; + + if (Api.HasSecret(key)) + value = Api.GetSecret(key); + else + _log.LogWarning("Secret key {Key} not found in secrets", key); + } + + return value; + } +} \ No newline at end of file diff --git a/EliteAPI.sln b/EliteAPI.sln index d56f1594..58e5793d 100644 --- a/EliteAPI.sln +++ b/EliteAPI.sln @@ -7,14 +7,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EliteAPI", "EliteAPI\EliteA EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EliteAPI.Events", "EliteAPI.Events\EliteAPI.Events.csproj", "{9E3E2DA3-51DF-487A-B85E-CDACE5F1AA29}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{98F4FCB9-F5A2-4159-97F0-3FEEE6620364}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EliteAPI.Abstractions", "EliteAPI.Abstractions\EliteAPI.Abstractions.csproj", "{D8D89BC9-D7AA-447E-BBC7-E744F00720B0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{2EF6A15E-9838-41A6-9A02-DB1CF9FC0203}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EliteAPI.Tests", "EliteAPI.Tests\EliteAPI.Tests.csproj", "{856F6019-7B19-40C7-9E45-A0FC155734E0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{C7227294-F9EC-4261-A3EC-46905B102AAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EliteAPI.Web", "EliteAPI.Web\EliteAPI.Web.csproj", "{5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EliteAPI.Web.Spansh", "EliteAPI.Web.Spansh\EliteAPI.Web.Spansh.csproj", "{CD529DF1-E744-4CC5-8403-D9B4491CF039}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Web", "Examples\Example.Web\Example.Web.csproj", "{227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Examples\Example\Example.csproj", "{3081E506-FF91-4C57-A687-F5E125AC1EB3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,14 +55,6 @@ Global {9E3E2DA3-51DF-487A-B85E-CDACE5F1AA29}.Release|Any CPU.Build.0 = Release|Any CPU {9E3E2DA3-51DF-487A-B85E-CDACE5F1AA29}.Release|x86.ActiveCfg = Release|Any CPU {9E3E2DA3-51DF-487A-B85E-CDACE5F1AA29}.Release|x86.Build.0 = Release|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Debug|Any CPU.Build.0 = Debug|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Debug|x86.ActiveCfg = Debug|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Debug|x86.Build.0 = Debug|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Release|Any CPU.ActiveCfg = Release|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Release|Any CPU.Build.0 = Release|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Release|x86.ActiveCfg = Release|Any CPU - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364}.Release|x86.Build.0 = Release|Any CPU {D8D89BC9-D7AA-447E-BBC7-E744F00720B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D8D89BC9-D7AA-447E-BBC7-E744F00720B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {D8D89BC9-D7AA-447E-BBC7-E744F00720B0}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -71,12 +71,47 @@ Global {856F6019-7B19-40C7-9E45-A0FC155734E0}.Release|Any CPU.Build.0 = Release|Any CPU {856F6019-7B19-40C7-9E45-A0FC155734E0}.Release|x86.ActiveCfg = Release|Any CPU {856F6019-7B19-40C7-9E45-A0FC155734E0}.Release|x86.Build.0 = Release|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Debug|x86.Build.0 = Debug|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Release|Any CPU.Build.0 = Release|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Release|x86.ActiveCfg = Release|Any CPU + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1}.Release|x86.Build.0 = Release|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Debug|x86.Build.0 = Debug|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Release|Any CPU.Build.0 = Release|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Release|x86.ActiveCfg = Release|Any CPU + {CD529DF1-E744-4CC5-8403-D9B4491CF039}.Release|x86.Build.0 = Release|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Debug|x86.Build.0 = Debug|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Release|Any CPU.Build.0 = Release|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Release|x86.ActiveCfg = Release|Any CPU + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8}.Release|x86.Build.0 = Release|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Debug|x86.ActiveCfg = Debug|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Debug|x86.Build.0 = Debug|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Release|Any CPU.Build.0 = Release|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Release|x86.ActiveCfg = Release|Any CPU + {3081E506-FF91-4C57-A687-F5E125AC1EB3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {98F4FCB9-F5A2-4159-97F0-3FEEE6620364} = {2EF6A15E-9838-41A6-9A02-DB1CF9FC0203} + {5D61C5AF-0D5F-4CFC-9F16-8D57463BC3D1} = {C7227294-F9EC-4261-A3EC-46905B102AAA} + {CD529DF1-E744-4CC5-8403-D9B4491CF039} = {C7227294-F9EC-4261-A3EC-46905B102AAA} + {227FD5FE-1EC3-4EF6-A91F-258A2212CDD8} = {2EF6A15E-9838-41A6-9A02-DB1CF9FC0203} + {3081E506-FF91-4C57-A687-F5E125AC1EB3} = {2EF6A15E-9838-41A6-9A02-DB1CF9FC0203} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0BCC51F1-912C-4035-9484-7A45F70F1790} diff --git a/EliteAPI/EliteAPI.csproj b/EliteAPI/EliteAPI.csproj index 2be30af1..cbf03196 100644 --- a/EliteAPI/EliteAPI.csproj +++ b/EliteAPI/EliteAPI.csproj @@ -1,7 +1,7 @@  - 3.0.15.0 - 3.0.15.0 + 3.1.0.0 + 3.1.0.0 https://github.com/EliteAPI/EliteAPI https://github.com/EliteAPI/EliteAPI @@ -11,14 +11,14 @@ A powerful event based .NET API for Elite: Dangerous © Somfic 2022 false - 3.0.15 + 3.1.0-alpha0182 true icon.png true 10 true Somfic - 3.0.15+13.Branch.main.Sha.aa3bd21915f470956d90f87a2d089e6d67be09ae + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de enable netstandard2.0 diff --git a/EliteAPI/EliteAPI.xml b/EliteAPI/EliteAPI.xml index 9b8dad0f..c08500ca 100644 --- a/EliteAPI/EliteAPI.xml +++ b/EliteAPI/EliteAPI.xml @@ -223,6 +223,9 @@ + + + diff --git a/EliteAPI/Events/Events.cs b/EliteAPI/Events/Events.cs index 7cc8dd64..02e1515a 100644 --- a/EliteAPI/Events/Events.cs +++ b/EliteAPI/Events/Events.cs @@ -36,7 +36,7 @@ public Events(ILogger? log, IEventParser eventParser) } /// - public IEnumerable EventTypes => _eventHandlers.Keys; + public IReadOnlyCollection EventTypes => _eventHandlers.Keys; /// public IReadOnlyCollection<(IEvent @event, EventContext context)> PreviousEvents => _previousEvents.AsReadOnly(); diff --git a/Examples/Example.Web/Example.Web.csproj b/Examples/Example.Web/Example.Web.csproj new file mode 100644 index 00000000..41c4614a --- /dev/null +++ b/Examples/Example.Web/Example.Web.csproj @@ -0,0 +1,19 @@ + + + Exe + net7.0 + enable + enable + false + 3.1.0.0 + 3.1.0.0 + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de + 3.1.0-alpha0182 + + + + + + + + \ No newline at end of file diff --git a/Examples/Example.Web/Program.cs b/Examples/Example.Web/Program.cs new file mode 100644 index 00000000..e868d59c --- /dev/null +++ b/Examples/Example.Web/Program.cs @@ -0,0 +1,40 @@ +using EliteAPI.Web.Spansh; +using EliteAPI.Web.Spansh.RoutePlanner.Requests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var spansh = new HostBuilder() + .ConfigureServices(s => + { + s.AddWebApi(); + }) + .ConfigureLogging(s => + { + s.AddSimpleConsole(); + s.AddFilter("Microsoft", LogLevel.Warning); + s.AddFilter("System", LogLevel.Warning); + + }) + .Build() + .Services + .GetRequiredService(); + +Console.WriteLine("Getting route ... "); +var response = await spansh.Routes.Trade(new TradeRequest("Shinrarta Dezhra", "Jameson Memorial", 50, 720)); + +response.On( + ok: route => + { + foreach (var stop in route) + { + Console.WriteLine(stop.System.Name + " - " + stop.System.Station); + foreach (var commodity in stop.Commodities) + { + Console.WriteLine($" - {commodity.Name} {commodity.Amount}x (+{commodity.TotalProfit:N0}cr)"); + } + + Console.WriteLine(); + } + }, + error: e => Console.WriteLine(e.Message)); \ No newline at end of file diff --git a/Example/Example.csproj b/Examples/Example/Example.csproj similarity index 64% rename from Example/Example.csproj rename to Examples/Example/Example.csproj index ffb65a22..696ded5d 100644 --- a/Example/Example.csproj +++ b/Examples/Example/Example.csproj @@ -2,10 +2,10 @@ enable enable - 3.0.15.0 - 3.0.15.0 - 3.0.15+13.Branch.main.Sha.aa3bd21915f470956d90f87a2d089e6d67be09ae - 3.0.15 + 3.1.0.0 + 3.1.0.0 + 3.1.0-alpha.182+Branch.dev.Sha.0fd6d5a6f5a17cbe313c176d121f8bf359e348de + 3.1.0-alpha0182 false 10 Exe @@ -15,6 +15,8 @@ + + diff --git a/Example/Program.cs b/Examples/Example/Program.cs similarity index 100% rename from Example/Program.cs rename to Examples/Example/Program.cs diff --git a/Example/appsettings.json b/Examples/Example/appsettings.json similarity index 100% rename from Example/appsettings.json rename to Examples/Example/appsettings.json diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 00000000..99a40de6 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-dotnet:latest