Commit: fd20689
Parent: 3feb1de

Add Vasttrafik panel(s)

Mårten Åsberg committed on 2026-06-06 at 19:15
.containerfile +3 -1
diff --git a/.containerfile b/.containerfile
index f20d906..2625a0e 100644
@@ -4,10 +4,12 @@ WORKDIR /app
COPY ./MMirror.slnx ./global.json ./dotnet-tools.json ./Directory.Packages.props ./Directory.Build.props ./.gitignore ./.editorconfig ./
COPY ./src/Integrations/Vasttrafik/Vasttrafik.csproj ./src/Integrations/Vasttrafik/packages.lock.json ./src/Integrations/Vasttrafik/
COPY ./src/App/App.csproj ./src/App/packages.lock.json ./src/App/
RUN dotnet restore --locked-mode
RUN dotnet restore --locked-mode ./src/App/App.csproj
COPY ./src/Integrations/Vasttrafik/ ./src/Integrations/Vasttrafik/
COPY ./src/App/ ./src/App/
RUN dotnet build ./src/App/App.csproj --no-restore --configuration Release
Directory.Packages.props +8 -5
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 366d36b..471c8d9 100644
@@ -15,9 +15,12 @@
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.4" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion
Include="Microsoft.Extensions.DependencyInjection"
Version="11.0.0-preview.4.26230.115"
/>
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="11.0.0-preview.4.26230.115" />
<PackageVersion Include="MSTest" Version="4.2.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="11.0.0-preview.4.26230.115" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="11.0.0-preview.4.26230.115" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="11.0.0-preview.4.26230.115" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
</ItemGroup>
</Project>
</Project>
\ No newline at end of file
MMirror.slnx +6 -0
diff --git a/MMirror.slnx b/MMirror.slnx
index 13e9ae3..d8a9d94 100644
@@ -2,4 +2,10 @@
<Folder Name="/src/">
<Project Path="src/App/App.csproj" />
</Folder>
<Folder Name="/src/Integrations/">
<Project Path="src/Integrations/Vasttrafik/Vasttrafik.csproj" />
</Folder>
<Folder Name="/tests/Integrations/">
<Project Path="tests/Integrations/Vasttrafik/Vasttrafik.csproj" />
</Folder>
</Solution>
src/App/App.axaml.cs +16 -3
diff --git a/src/App/App.axaml.cs b/src/App/App.axaml.cs
index 453fe5a..ce09514 100644
@@ -1,14 +1,19 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MMirror.App.ViewModels;
using MMirror.App.Views;
using MMirror.Integrations.Vasttrafik;
namespace MMirror.App;
public partial class App : Application
{
private IHost? host;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -16,10 +21,18 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection().AddViews().AddViewModels().BuildServiceProvider();
var builder = Host.CreateApplicationBuilder();
#if DEBUG
builder.Configuration.AddUserSecrets<App>();
#endif
builder.Services.AddViews().AddViewModels().AddVasttrafikServices();
host = builder.Build();
host.Start();
DataTemplates.Add(new ViewLocator(services));
var vm = services.GetRequiredService<MainViewModel>();
DataTemplates.Add(new ViewLocator(host.Services));
var vm = host.Services.GetRequiredService<MainViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
src/App/App.csproj +5 -1
diff --git a/src/App/App.csproj b/src/App/App.csproj
index 985ef05..6eddacb 100644
@@ -4,11 +4,15 @@
<AssemblyName>MMirror.App</AssemblyName>
<OutputType>Exe</OutputType>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<UserSecretsId>97e2d350-b20e-4c6b-b7cd-35a8f3ec03fa</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Integrations\Vasttrafik\Vasttrafik.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.LinuxFramebuffer">
@@ -22,6 +26,6 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
</Project>
src/App/Styles/MainStyles.axaml +6 -0
diff --git a/src/App/Styles/MainStyles.axaml b/src/App/Styles/MainStyles.axaml
index 7e94c1b..452e244 100644
@@ -4,6 +4,12 @@
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Light"/>
</Style>
<Style Selector="TextBlock.h2">
<Setter Property="FontSize" Value="20"/>
</Style>
<Style Selector="TextBlock.h3">
<Setter Property="FontSize" Value="16"/>
</Style>
<Style Selector=":is(Control).fade">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="RenderTransform" Value="translateY(10px)" />
src/App/ViewLocator.cs +1 -0
diff --git a/src/App/ViewLocator.cs b/src/App/ViewLocator.cs
index 0dcdd52..dcc8716 100644
@@ -19,6 +19,7 @@ public class ViewLocator(IServiceProvider services) : IDataTemplate
{
MainViewModel => services.GetControlOrDefault<MainView>(),
DateTimePanelViewModel => services.GetControlOrDefault<DateTimePanel>(),
VasttrafikPanelViewModel => services.GetControlOrDefault<VasttrafikPanel>(),
_ => new TextBlock { Text = $"No view for {data?.GetType().Name}" },
};
src/App/ViewModels/MainViewModel.cs +21 -2
diff --git a/src/App/ViewModels/MainViewModel.cs b/src/App/ViewModels/MainViewModel.cs
index 2ef0763..298d185 100644
@@ -5,9 +5,28 @@ using MMirror.App.ViewModels.Panels;
namespace MMirror.App.ViewModels;
public partial class MainViewModel(DateTimePanelViewModel dateTimePanelViewModel) : ViewModelBase
public partial class MainViewModel : ViewModelBase
{
public ViewModelBase UpperRight { get; } = dateTimePanelViewModel;
public ViewModelBase Olskrokstorget { get; }
public ViewModelBase Svingeln { get; }
public ViewModelBase UpperRight { get; }
public MainViewModel(
VasttrafikPanelViewModel olskrokstorget,
VasttrafikPanelViewModel svingeln,
DateTimePanelViewModel dateTimePanelViewModel
)
{
olskrokstorget.StopName = "Olskrokstorget";
olskrokstorget.StopId = "9021014005160000";
Olskrokstorget = olskrokstorget;
svingeln.StopName = "Svingeln";
svingeln.StopId = "9021014006480000";
Svingeln = svingeln;
UpperRight = dateTimePanelViewModel;
}
}
internal static class MainViewModelServiceCollectionExtensions
src/App/ViewModels/Panels/ServiceCollectionExtensions.cs +2 -1
diff --git a/src/App/ViewModels/Panels/ServiceCollectionExtensions.cs b/src/App/ViewModels/Panels/ServiceCollectionExtensions.cs
index 1f65e56..8d896ad 100644
@@ -6,6 +6,7 @@ internal static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddPanelViewModels() => services.AddDateTimePanelViewModel();
public IServiceCollection AddPanelViewModels() =>
services.AddDateTimePanelViewModel().AddVasttrafikPanelViewModel();
}
}
src/App/ViewModels/Panels/VasttrafikPanelViewModel.cs +91 -0
diff --git a/src/App/ViewModels/Panels/VasttrafikPanelViewModel.cs b/src/App/ViewModels/Panels/VasttrafikPanelViewModel.cs
new file mode 100644
index 0000000..e8e7f53
@@ -0,0 +1,91 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using MMirror.Integrations.Vasttrafik;
namespace MMirror.App.ViewModels.Panels;
public partial class VasttrafikPanelViewModel : ViewModelBase
{
private readonly TimeProvider timeProvider;
private readonly VasttrafikClient vasttrafikClient;
private readonly DispatcherTimer secondTimer;
private CancellationTokenSource cts = new();
[ObservableProperty]
public partial DateTimeOffset CurrentTime { get; private set; }
[ObservableProperty]
public partial ObservableCollection<Departure> Departures { get; private set; }
[ObservableProperty]
public required partial string StopName { get; set; }
public required string StopId { get; set; }
public VasttrafikPanelViewModel(TimeProvider timeProvider, VasttrafikClient vasttrafikClient)
{
this.timeProvider = timeProvider;
this.vasttrafikClient = vasttrafikClient;
CurrentTime = timeProvider.GetLocalNow();
Departures = [];
secondTimer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(15) };
secondTimer.Tick += OnSecondTick;
}
public void SubscribeToUpdates()
{
secondTimer.Start();
Task.Run(() => FetchLoop(cts.Token));
}
private void OnSecondTick(object? sender, EventArgs e)
{
CurrentTime = timeProvider.GetLocalNow();
Departures =
[
.. Departures
.Where(d => CurrentTime < d.Next || (d.NextNext.HasValue && CurrentTime < d.NextNext.Value))
.Select(d => CurrentTime < d.Next ? d : d with { Next = d.NextNext!.Value, NextNext = null })
.OrderBy(d => d.Next),
];
}
private async Task FetchLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Departures = [.. await vasttrafikClient.GetDeparturesFrom(StopId, cancellationToken)];
CurrentTime = timeProvider.GetLocalNow();
await Task.Delay(TimeSpan.FromMinutes(1), timeProvider, cancellationToken);
}
}
public void UnsubscribeFromUpdates()
{
secondTimer.Stop();
cts.Cancel();
cts = new();
}
}
internal static class VasttrafikPanelViewModelServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddVasttrafikPanelViewModel()
{
services.AddTransient<VasttrafikPanelViewModel>();
return services;
}
}
}
src/App/Views/MainView.axaml +6 -2
diff --git a/src/App/Views/MainView.axaml b/src/App/Views/MainView.axaml
index 47851cb..7308d0c 100644
@@ -10,7 +10,11 @@
<vm:MainViewModel />
</Design.DataContext>
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,*,*">
<ContentControl Grid.Row="0" Grid.Column="2" Content="{Binding UpperRight}" />
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,*,*" Margin="64">
<StackPanel Classes="fade" Grid.Row="0" Grid.Column="0" Spacing="16">
<ContentControl Content="{Binding Olskrokstorget}" />
<ContentControl Content="{Binding Svingeln}" />
</StackPanel>
<ContentControl Classes="fade" Grid.Row="0" Grid.Column="2" Content="{Binding UpperRight}" />
</Grid>
</UserControl>
src/App/Views/Panels/DateTimePanel.axaml +1 -1
diff --git a/src/App/Views/Panels/DateTimePanel.axaml b/src/App/Views/Panels/DateTimePanel.axaml
index 0c33ab9..7e04639 100644
@@ -40,7 +40,7 @@
</Style>
</UserControl.Styles>
<StackPanel Classes="fade" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8" TextElement.FontFeatures="+tnum">
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Spacing="8" TextElement.FontFeatures="+tnum">
<TextBlock Classes="h1" Text="{Binding CurrentTime, StringFormat=yyyy-MM-dd}"/>
<StackPanel Name="clock" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Classes="h1" Text="{Binding CurrentTime, StringFormat=HH:mm}" />
src/App/Views/Panels/ServiceCollectionExtensions.cs +1 -1
diff --git a/src/App/Views/Panels/ServiceCollectionExtensions.cs b/src/App/Views/Panels/ServiceCollectionExtensions.cs
index 42355e1..55b87b0 100644
@@ -6,6 +6,6 @@ internal static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddPanels() => services.AddTransient<DateTimePanel>();
public IServiceCollection AddPanels() => services.AddTransient<DateTimePanel>().AddTransient<VasttrafikPanel>();
}
}
src/App/Views/Panels/VasttrafikPanel.axaml +88 -0
diff --git a/src/App/Views/Panels/VasttrafikPanel.axaml b/src/App/Views/Panels/VasttrafikPanel.axaml
new file mode 100644
index 0000000..5169ebb
@@ -0,0 +1,88 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:MMirror.App.ViewModels.Panels"
xmlns:v="using:MMirror.App.Views.Panels"
mc:Ignorable="d"
d:DesignWidth="720"
d:DesignHeight="1280"
x:Class="MMirror.App.Views.Panels.VasttrafikPanel"
x:DataType="vm:VasttrafikPanelViewModel">
<Design.DataContext>
<vm:VasttrafikPanelViewModel/>
</Design.DataContext>
<UserControl.Resources>
<x:Double x:Key="spacing">4</x:Double>
<Thickness x:Key="spacingTop">0,4,0,0</Thickness>
<Thickness x:Key="spacingRight">0,0,4,0</Thickness>
<Thickness x:Key="spacingAll">4,2</Thickness>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="TextBlock.label">
<Setter Property="VerticalAlignment" Value="Bottom" />
</Style>
<Style Selector="TextBlock.time">
<Setter Property="FontFeatures" Value="+tnum" />
<Setter Property="TextAlignment" Value="End" />
</Style>
</UserControl.Styles>
<Grid Grid.IsSharedSizeScope="True" RowDefinitions="Auto,*" RowSpacing="{StaticResource spacing}" ColumnSpacing="{StaticResource spacing}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition Width="*" />
<ColumnDefinition SharedSizeGroup="Next" />
<ColumnDefinition SharedSizeGroup="NextNext" />
<ColumnDefinition SharedSizeGroup="Platform" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Classes="h1 label" Text="{Binding StopName}" />
<TextBlock Grid.Row="0" Grid.Column="3" Classes="label" Text="Nästa" />
<TextBlock Grid.Row="0" Grid.Column="4" Classes="label" Text="Därefter" />
<TextBlock Grid.Row="0" Grid.Column="5" Classes="label" Text="Läge" />
<ItemsControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="6" ItemsSource="{Binding Departures}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="{StaticResource spacing}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid ColumnSpacing="{StaticResource spacing}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition Width="*" />
<ColumnDefinition SharedSizeGroup="Next" />
<ColumnDefinition SharedSizeGroup="NextNext" />
<ColumnDefinition SharedSizeGroup="Platform" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Center" Padding="{StaticResource spacingAll}"
BorderThickness="2" CornerRadius="4" BorderBrush="{DynamicResource SystemControlForegroundBaseHighBrush}">
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" TextAlignment="Center"
Text="{Binding Name}" />
</Border>
<!-- TODO: Add "Type" icons -->
<TextBlock Grid.Column="2" Classes="h2" IsVisible="{Binding Via, Converter={x:Static ObjectConverters.IsNull}}" TextWrapping="Wrap" Text="{Binding Direction}" />
<TextBlock Grid.Column="2" Classes="h2" IsVisible="{Binding Via, Converter={x:Static ObjectConverters.IsNotNull}}" TextWrapping="Wrap">
<Run Text="{Binding Direction}" />
<Span FontSize="16">via <Run Text="{Binding Via}" /></Span>
</TextBlock>
<TextBlock Grid.Column="3" Classes="h2 time" Text="{v:MinutesLeftBinding {Binding Next}, CurrentTime={Binding $parent[UserControl].DataContext.CurrentTime}}" />
<TextBlock Grid.Column="4" Classes="h2 time" Text="{v:MinutesLeftBinding {Binding NextNext}, CurrentTime={Binding $parent[UserControl].DataContext.CurrentTime}}" />
<Ellipse Grid.Column="5" Width="{Binding #platform.Bounds.Height}" Height="{Binding #platform.Bounds.Height}"
HorizontalAlignment="Center" VerticalAlignment="Top" Margin="{StaticResource spacingTop}"
Fill="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<TextBlock Name="platform" Grid.Column="5"
HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="{StaticResource spacingTop}"
Foreground="{DynamicResource SystemRegionBrush}"
TextAlignment="Center" FontWeight="ExtraBold"
Text="{Binding Platform}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
src/App/Views/Panels/VasttrafikPanel.axaml.cs +52 -0
diff --git a/src/App/Views/Panels/VasttrafikPanel.axaml.cs b/src/App/Views/Panels/VasttrafikPanel.axaml.cs
new file mode 100644
index 0000000..60eca00
@@ -0,0 +1,52 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using MMirror.App.ViewModels.Panels;
namespace MMirror.App.Views.Panels;
public partial class VasttrafikPanel : UserControl
{
public VasttrafikPanel()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (DataContext is VasttrafikPanelViewModel vm)
{
vm.SubscribeToUpdates();
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (DataContext is VasttrafikPanelViewModel vm)
{
vm.UnsubscribeFromUpdates();
}
base.OnDetachedFromVisualTree(e);
}
}
public sealed class MinutesLeftBinding(BindingBase target)
{
public required BindingBase CurrentTime { get; set; }
public object ProvideValue() =>
new MultiBinding()
{
Bindings = [target, CurrentTime],
Converter = new FuncMultiValueConverter<DateTimeOffset, string>(times =>
{
var minutesLeft = (int)double.Floor((times[0] - times[1]).TotalMinutes);
return minutesLeft is 0 ? "Nu" : minutesLeft.ToString();
}),
};
}
src/App/packages.lock.json +270 -2
diff --git a/src/App/packages.lock.json b/src/App/packages.lock.json
index 48a3ef9..6aee8a9 100644
@@ -76,11 +76,29 @@
"resolved": "1.2.6",
"contentHash": "KMSJG+jfk7vjP52QkWB99qWespXCPAzG/IaMCMRHYWumJEAGKQYm2HtyWG6eqnOwDitH96i1cqq5EVesyOtPmg=="
},
"Microsoft.Extensions.DependencyInjection": {
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "v+F34r5W5XgR95xG0Te2KyYmCDnkc4kljLMGEZ30WKQjAzuXz/pLTEvsRNYxNJUzfKPI0H4oqLglyHZkCe1lYw=="
"contentHash": "JabRZnPjt57VHxcOm7G7runakIkv3PunM2GTJF32KF5hJChgpUh2gnuL4VQ0GsVtHXGsbefj9tddYs2K0YJmpA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.CommandLine": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.FileExtensions": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.Json": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.UserSecrets": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.DependencyInjection": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Diagnostics": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.FileProviders.Physical": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Console": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Debug": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.EventLog": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.EventSource": "11.0.0-preview.4.26230.115"
}
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
@@ -201,6 +219,181 @@
"resolved": "0.11.4",
"contentHash": "yZ8+Lgwo+KtRI29TB2mIOEMzV+csMJ+pKZg4YHReAP3vRewWLKKeYfrBDo5FS69rWnEbCfU3sbM+ZEQr+GDLtg=="
},
"Microsoft.Bcl.Cryptography": {
"type": "Transitive",
"resolved": "10.0.2",
"contentHash": "LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "CPb3DPC6kcgx+kuSTJ55H/A6Z+IZeW++VM+UiFpiwq6eQC2Mk3gHcFpqdQoK2MjWovWYN4Sz6n1RXhLWHVB35w=="
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "IOkgB4CnLUlKH+l0slH5Q+6m9pWfcCBocFPHc7czATwIx5jnVPgUMRj2gGhZtvGs1PKpeoZv7PCGK5cYIwlZzw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "AWgDyrMol+ywzG0MAAk+TkLIy4fMEsOCG1E/zb1kHE3Lp81GNNEMU+jOJTF1gEsLkDajY5AouNAWx3cvi+pxzg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "7vBrAODuFr5u548MNTaUybxQ+Hw/x9boZ2rWQynOS/KJkZhrriUFtgbQTeRamQFVna1eh78WjVAqEKm8GpQXBg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "//7zz5cgmkd/cfgKanPna9CHJ4o9Wp9JPpr+3rmSa7vd/+Wwxjb9Z9sUKvH0LsJl72LnUYWKJ7Ah02nr2yLsYA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.FileProviders.Physical": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "cXhGyFCd2LnPAMu5pVxf3CeIqYJ+wy866tNKjaAvLFQsJ1QDB4Xfmiy+6q0Zm8DvjektmB/IoDBl5F/0AKEBOA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.FileExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "GnJpXFEumu46mGMU++s54qgRwlkGhCP05xHOSgaAK/lLTUOt2mAMaIwFqrLFKfUFtkByjxSe/1FtzXx2zrIv9w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Json": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.FileProviders.Physical": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "L1+sxK5JT7X5jvZ0RInbDnyJZG9rkp4cbgzoL0aGRUOOMp3PkgCcUjAUhAprxBu9LgGdeqZg7JratDMHS1YN/Q==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Options.ConfigurationExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "L/+w2lo0fV2hcQuAe3YIBwXl4YP16c/xFUCHXjhzidByFbyECb2H8hJ5EEeszihGcT69nd+gJqrZyzyp1bObig==",
"dependencies": {
"Microsoft.Extensions.FileSystemGlobbing": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "y8hEW4rVMESwL9iMfc0PICy0ScXzj4MMXclEjf5AXZKOFrJe6nu8iU7K9VVZenNweXdLYvlcs2ZSnhVz7h8oYg=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "EpZS5qqhsNXwgeSCl2m1VkysA+pTh3qlhyjkjiu0peZt/V7IyANJXQQUuv3y2kp/jzd9XdfzuUJU0Fgp7rcDGg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "YkVeVCrOfKdYghClaYH/jcC2Irx6gtrWPAOjNkOeITQVQ41vvhPVp7EcXYD8IcfYH58YIS5WiX1ZJu6uLEVrAQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Options.ConfigurationExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "1ATxqXTZnR5M4tOeCtZjA+fw1b+jWPsmUAp3PUCCvndV5ghIcSvnwZ5RiQFoTtYFSdNP+pMoxCtviI71/xyRCw==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "kD1p/UZ/NqHTLt1iLjtkJpPtPWWziLkV6kG3TxLi67gWasixyvH1grMfkd14XR24RIooTkavxGZJV+xjeSSsiA==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "DOtIsmOENF8xmQi0uw0sx4/1aDHDgj1PuGmlom2zobrqbJp2b5fkZPCIOTmPZsXbk78nxVnPH7/NTbR1cYRnsA==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Configuration": "11.0.0-preview.4.26230.115",
"System.Diagnostics.EventLog": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "koDYnhbSTo+di3NMq13c0jhYFTIc7XUgnevONLwDpzR2GbbXfUR+dN5GuMeAVR4EQljUw5OYllrYflPNb0zoZA==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "9QNxho6sxC4RXafVZGWzYq6KcYVpp1FkHtL+HJ967UNyUjhOWnGKMM0dPXkHKtlZ/0GGEDqnBDun0kuF633a6g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "gFA8THIk23uNF/vMdOHnjIdXD1LyA2g12cHzMJ+Xag6WpgWLw6E/6uCXxvA0gp9d2yAvkRt3xzFzMUiO/hofnQ=="
},
"Microsoft.IdentityModel.JsonWebTokens": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "6eeY+y2QFyjj3XnCz/8gJdoP5smYHTS9ow1bw2nsZzDIPjPhBZlackYTIduSMipVpxnoT/B62LkrXX2jPggOXg==",
"dependencies": {
"Microsoft.IdentityModel.Tokens": "8.19.1"
}
},
"Microsoft.IdentityModel.Logging": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "H+sMrMpdbWnwkQnpb/ESkQovtOgdefmj0ecGCcP40mDKzE5i4dUYkH6599M9mWYFNGNJnTp92l/9wLubYXWimw==",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "8.19.1"
}
},
"Microsoft.IdentityModel.Tokens": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "KDiuSLXud2AFVNAOottd8ztVysfPeHyr4r8gofU3/VKUXlI7oytzGTnPsNJ/B3nui17rgz8wAdWNJOtzPjkUxw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.2",
"Microsoft.IdentityModel.Logging": "8.19.1"
}
},
"Microsoft.IO.RecyclableMemoryStream": {
"type": "Transitive",
"resolved": "3.0.1",
@@ -235,10 +428,55 @@
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"Tmds.DBus.Protocol": {
"type": "Transitive",
"resolved": "0.92.0",
"contentHash": "h7IMakm0PF2jxiagoysoAjrzzLQ0UBdnSXQL5kb17YW0Fvyo12Tg96A99QkzwktWRrd7H+Uw9EzjasNLUfGYlA=="
},
"MMirror.Integrations.Vasttrafik": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Caching.Memory": "[11.0.0-preview.4.26230.115, )",
"Microsoft.Extensions.Http": "[11.0.0-preview.4.26230.115, )",
"System.IdentityModel.Tokens.Jwt": "[8.19.1, )"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "JLwOVRLavnY5Z2uMISBxQm+2TsbW9b5pJ9NwvtrCgCNiH/OCuJyznD7HJGvMF+5ESakq5ZrTLECNN3FMGMeoIg=="
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "v+F34r5W5XgR95xG0Te2KyYmCDnkc4kljLMGEZ30WKQjAzuXz/pLTEvsRNYxNJUzfKPI0H4oqLglyHZkCe1lYw=="
},
"Microsoft.Extensions.Http": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "HTY8+4kSEcmghrSTifim/cwZ4hQkniUr1+c1OMLHS1DJ2d7R9FFQ/t7Wf/MYTRHlB85ng9hMQNMY/PWTIfEmKQ==",
"dependencies": {
"Microsoft.Extensions.Diagnostics": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"System.IdentityModel.Tokens.Jwt": {
"type": "CentralTransitive",
"requested": "[8.19.1, )",
"resolved": "8.19.1",
"contentHash": "2VHcRtT95GAcW1E3aVBLvL2rAAMxKHXKMXKXFyWzwgkdFXZPMMvP8tVOfnRydL4vTr1RirNuGC6T8VSEF2YsPQ==",
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "8.19.1",
"Microsoft.IdentityModel.Tokens": "8.19.1"
}
}
},
"net11.0/linux-arm64": {
@@ -284,6 +522,11 @@
"type": "Transitive",
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
}
},
"net11.0/linux-x64": {
@@ -329,6 +572,11 @@
"type": "Transitive",
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
}
},
"net11.0/osx-arm64": {
@@ -374,6 +622,11 @@
"type": "Transitive",
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
}
},
"net11.0/osx-x64": {
@@ -419,6 +672,11 @@
"type": "Transitive",
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
}
},
"net11.0/win-arm64": {
@@ -464,6 +722,11 @@
"type": "Transitive",
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
}
},
"net11.0/win-x64": {
@@ -509,6 +772,11 @@
"type": "Transitive",
"resolved": "3.119.4",
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
}
}
}
src/Integrations/Vasttrafik/ClientOptions.cs +20 -0
diff --git a/src/Integrations/Vasttrafik/ClientOptions.cs b/src/Integrations/Vasttrafik/ClientOptions.cs
new file mode 100644
index 0000000..fac35a8
@@ -0,0 +1,20 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace MMirror.Integrations.Vasttrafik;
internal sealed class ClientOptions
{
[Required]
public required Uri BaseAddress { get; set; }
[Required]
public required string ClientId { get; set; }
[Required]
public required string ClientSecret { get; set; }
}
[OptionsValidator]
internal sealed partial class ClientOptionsValidator : IValidateOptions<ClientOptions>;
src/Integrations/Vasttrafik/Dtos.cs +34 -0
diff --git a/src/Integrations/Vasttrafik/Dtos.cs b/src/Integrations/Vasttrafik/Dtos.cs
new file mode 100644
index 0000000..3ef6449
@@ -0,0 +1,34 @@
using System;
using System.Text.Json.Serialization;
namespace MMirror.Integrations.Vasttrafik;
[JsonSerializable(typeof(DeparturesResponse))]
internal sealed partial class VasttrafikJsonSerializerContext : JsonSerializerContext;
internal sealed record DeparturesResponse([property: JsonPropertyName("results")] DepartureDto[] Results);
internal sealed record DepartureDto(
[property: JsonPropertyName("serviceJourney")] ServiceJourney ServiceJourney,
[property: JsonPropertyName("stopPoint")] StopPoint StopPoint,
[property: JsonPropertyName("estimatedTime")] DateTimeOffset? EstimatedTime
);
internal sealed record ServiceJourney(
[property: JsonPropertyName("directionDetails")] DirectionDetails DirectionDetails,
[property: JsonPropertyName("line")] Line Line
);
internal sealed record DirectionDetails(
[property: JsonPropertyName("shortDirection")] string Direction,
[property: JsonPropertyName("via")] string? Via
);
internal sealed record Line(
[property: JsonPropertyName("shortName")] string Name,
[property: JsonPropertyName("backgroundColor")] string? BackgroundColor,
[property: JsonPropertyName("foregroundColor")] string? ForegroundColor,
[property: JsonPropertyName("transportMode")] string Type
);
internal sealed record StopPoint([property: JsonPropertyName("platform")] string Platform);
src/Integrations/Vasttrafik/OAuth2Handler.cs +61 -0
diff --git a/src/Integrations/Vasttrafik/OAuth2Handler.cs b/src/Integrations/Vasttrafik/OAuth2Handler.cs
new file mode 100644
index 0000000..fe7c40e
@@ -0,0 +1,61 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace MMirror.Integrations.Vasttrafik;
internal sealed class OAuth2Handler(IOptions<ClientOptions> options, IMemoryCache memoryCache, HttpClient httpClient)
: DelegatingHandler
{
private readonly string id = options.Value.ClientId;
private readonly string secret = options.Value.ClientSecret;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
)
{
request.Headers.Authorization = await GetAuthorization();
return await base.SendAsync(request, cancellationToken);
}
private async Task<AuthenticationHeaderValue> GetAuthorization() =>
(await memoryCache.GetOrCreateAsync("VasttrafikAuthorization", GetToken))!;
private async Task<AuthenticationHeaderValue> GetToken(ICacheEntry cacheEntry)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/token")
{
Headers =
{
Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{secret}"))),
},
Content = new FormUrlEncodedContent([new("grant_type", "client_credentials")]),
};
var authResponse = await httpClient.SendAsync(request);
var token =
await authResponse.Content.ReadFromJsonAsync(OAuth2JsonSerializerContext.Default.OAuth2TokenResponse)
?? throw new Exception("Parsed literal null from Vasttrafik token endpoint.");
var jwtHandler = new JwtSecurityTokenHandler();
var jwt = jwtHandler.ReadJwtToken(token.AccessToken);
cacheEntry.AbsoluteExpiration = jwt.ValidTo;
return new("Bearer", token.AccessToken);
}
}
[JsonSerializable(typeof(OAuth2TokenResponse))]
internal sealed partial class OAuth2JsonSerializerContext : JsonSerializerContext;
internal sealed record OAuth2TokenResponse([property: JsonPropertyName("access_token")] string AccessToken);
src/Integrations/Vasttrafik/ServiceCollectionExtensions.cs +35 -0
diff --git a/src/Integrations/Vasttrafik/ServiceCollectionExtensions.cs b/src/Integrations/Vasttrafik/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a1e7423
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace MMirror.Integrations.Vasttrafik;
public static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddVasttrafikServices()
{
services.AddMemoryCache();
services.AddOptions<ClientOptions>().BindConfiguration("Vasttrafik").ValidateOnStart();
services.AddTransient<IValidateOptions<ClientOptions>, ClientOptionsValidator>();
services
.AddHttpClient<OAuth2Handler>()
.ConfigureHttpClient(
(sp, client) =>
client.BaseAddress = sp.GetRequiredService<IOptions<ClientOptions>>().Value.BaseAddress
);
services
.AddHttpClient<VasttrafikClient>()
.AddHttpMessageHandler<OAuth2Handler>()
.ConfigureHttpClient(
(sp, client) =>
client.BaseAddress = sp.GetRequiredService<IOptions<ClientOptions>>().Value.BaseAddress
);
return services;
}
}
}
src/Integrations/Vasttrafik/Vasttrafik.csproj +11 -0
diff --git a/src/Integrations/Vasttrafik/Vasttrafik.csproj b/src/Integrations/Vasttrafik/Vasttrafik.csproj
new file mode 100644
index 0000000..1797adc
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MMirror.Integrations.Vasttrafik</RootNamespace>
<AssemblyName>MMirror.Integrations.Vasttrafik</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
</ItemGroup>
</Project>
src/Integrations/Vasttrafik/VasttrafikClient.cs +74 -0
diff --git a/src/Integrations/Vasttrafik/VasttrafikClient.cs b/src/Integrations/Vasttrafik/VasttrafikClient.cs
new file mode 100644
index 0000000..769c127
@@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace MMirror.Integrations.Vasttrafik;
public sealed class VasttrafikClient(HttpClient httpClient, TimeProvider timeProvider)
{
public async Task<Departure[]> GetDeparturesFrom(string id, CancellationToken cancellationToken)
{
var response =
await httpClient.GetFromJsonAsync(
$"/pr/v4/stop-areas/{id}/departures?timeSpanInMinutes=60&maxDeparturesPerLineAndDirection=2&limit=32",
VasttrafikJsonSerializerContext.Default.DeparturesResponse,
cancellationToken
) ?? throw new Exception("Received literal null from Vasttrafik departures endpoint.");
var currentTime = timeProvider.GetLocalNow();
return
[
.. response
.Results.Where(d => d.EstimatedTime.HasValue && currentTime < d.EstimatedTime.Value)
.GroupBy(d => (d.ServiceJourney.Line.Name, d.ServiceJourney.DirectionDetails.Direction))
.Select(g =>
{
var departures = g.ToArray();
var departureTimes = departures.Select(d => d.EstimatedTime!.Value).Order().ToArray();
return new Departure(
departures[0].ServiceJourney.Line.Name,
MapTransportType(departures[0].ServiceJourney.Line.Type),
departures[0].ServiceJourney.DirectionDetails.Direction,
departures[0].ServiceJourney.DirectionDetails.Via,
departureTimes[0],
departureTimes.Length > 1 ? departureTimes[1] : null,
departures[0].StopPoint.Platform
);
})
.OrderBy(d => d.Next)
.Take(8),
];
}
private static TransportType MapTransportType(string type) =>
type switch
{
"tram" => TransportType.Tram,
"bus" => TransportType.Bus,
"ferry" => TransportType.Ferry,
"train" => TransportType.Train,
_ => TransportType.Unknown,
};
}
public sealed record Departure(
string Name,
TransportType Type,
string Direction,
string? Via,
DateTimeOffset Next,
DateTimeOffset? NextNext,
string Platform
);
public enum TransportType
{
Unknown,
Tram,
Bus,
Ferry,
Train,
}
src/Integrations/Vasttrafik/packages.lock.json +124 -0
diff --git a/src/Integrations/Vasttrafik/packages.lock.json b/src/Integrations/Vasttrafik/packages.lock.json
new file mode 100644
index 0000000..cce661b
@@ -0,0 +1,124 @@
{
"version": 2,
"dependencies": {
"net11.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.2.6, )",
"resolved": "1.2.6",
"contentHash": "KMSJG+jfk7vjP52QkWB99qWespXCPAzG/IaMCMRHYWumJEAGKQYm2HtyWG6eqnOwDitH96i1cqq5EVesyOtPmg=="
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Direct",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "JLwOVRLavnY5Z2uMISBxQm+2TsbW9b5pJ9NwvtrCgCNiH/OCuJyznD7HJGvMF+5ESakq5ZrTLECNN3FMGMeoIg=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "HTY8+4kSEcmghrSTifim/cwZ4hQkniUr1+c1OMLHS1DJ2d7R9FFQ/t7Wf/MYTRHlB85ng9hMQNMY/PWTIfEmKQ==",
"dependencies": {
"Microsoft.Extensions.Diagnostics": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Direct",
"requested": "[8.19.1, )",
"resolved": "8.19.1",
"contentHash": "2VHcRtT95GAcW1E3aVBLvL2rAAMxKHXKMXKXFyWzwgkdFXZPMMvP8tVOfnRydL4vTr1RirNuGC6T8VSEF2YsPQ==",
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "8.19.1",
"Microsoft.IdentityModel.Tokens": "8.19.1"
}
},
"Microsoft.Bcl.Cryptography": {
"type": "Transitive",
"resolved": "10.0.2",
"contentHash": "LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "CPb3DPC6kcgx+kuSTJ55H/A6Z+IZeW++VM+UiFpiwq6eQC2Mk3gHcFpqdQoK2MjWovWYN4Sz6n1RXhLWHVB35w=="
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "IOkgB4CnLUlKH+l0slH5Q+6m9pWfcCBocFPHc7czATwIx5jnVPgUMRj2gGhZtvGs1PKpeoZv7PCGK5cYIwlZzw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "L1+sxK5JT7X5jvZ0RInbDnyJZG9rkp4cbgzoL0aGRUOOMp3PkgCcUjAUhAprxBu9LgGdeqZg7JratDMHS1YN/Q==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Options.ConfigurationExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "EpZS5qqhsNXwgeSCl2m1VkysA+pTh3qlhyjkjiu0peZt/V7IyANJXQQUuv3y2kp/jzd9XdfzuUJU0Fgp7rcDGg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "9QNxho6sxC4RXafVZGWzYq6KcYVpp1FkHtL+HJ967UNyUjhOWnGKMM0dPXkHKtlZ/0GGEDqnBDun0kuF633a6g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "gFA8THIk23uNF/vMdOHnjIdXD1LyA2g12cHzMJ+Xag6WpgWLw6E/6uCXxvA0gp9d2yAvkRt3xzFzMUiO/hofnQ=="
},
"Microsoft.IdentityModel.JsonWebTokens": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "6eeY+y2QFyjj3XnCz/8gJdoP5smYHTS9ow1bw2nsZzDIPjPhBZlackYTIduSMipVpxnoT/B62LkrXX2jPggOXg==",
"dependencies": {
"Microsoft.IdentityModel.Tokens": "8.19.1"
}
},
"Microsoft.IdentityModel.Logging": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "H+sMrMpdbWnwkQnpb/ESkQovtOgdefmj0ecGCcP40mDKzE5i4dUYkH6599M9mWYFNGNJnTp92l/9wLubYXWimw==",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "8.19.1"
}
},
"Microsoft.IdentityModel.Tokens": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "KDiuSLXud2AFVNAOottd8ztVysfPeHyr4r8gofU3/VKUXlI7oytzGTnPsNJ/B3nui17rgz8wAdWNJOtzPjkUxw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.2",
"Microsoft.IdentityModel.Logging": "8.19.1"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "v+F34r5W5XgR95xG0Te2KyYmCDnkc4kljLMGEZ30WKQjAzuXz/pLTEvsRNYxNJUzfKPI0H4oqLglyHZkCe1lYw=="
}
},
"net11.0/linux-arm64": {},
"net11.0/linux-x64": {},
"net11.0/osx-arm64": {},
"net11.0/osx-x64": {},
"net11.0/win-arm64": {},
"net11.0/win-x64": {}
}
}
\ No newline at end of file
tests/Integrations/Vasttrafik/MSTestSettings.cs +3 -0
diff --git a/tests/Integrations/Vasttrafik/MSTestSettings.cs b/tests/Integrations/Vasttrafik/MSTestSettings.cs
new file mode 100644
index 0000000..e466aa1
@@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
tests/Integrations/Vasttrafik/README.md +7 -0
diff --git a/tests/Integrations/Vasttrafik/README.md b/tests/Integrations/Vasttrafik/README.md
new file mode 100644
index 0000000..0756c6e
@@ -0,0 +1,7 @@
# Västtrafik tests
These are integration tests for the Västtrafik integration.
## Setup
Provide the Västtrafik base address, client ID, and client secret, as User Secrets before running the tests.
tests/Integrations/Vasttrafik/Vasttrafik.csproj +19 -0
diff --git a/tests/Integrations/Vasttrafik/Vasttrafik.csproj b/tests/Integrations/Vasttrafik/Vasttrafik.csproj
new file mode 100644
index 0000000..49e5e16
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MMirror.Integrations.Vasttrafik.Tests</RootNamespace>
<AssemblyName>MMirror.Integrations.Vasttrafik.Tests</AssemblyName>
<UserSecretsId>56558719-c98e-4775-b354-047b9642b3e9</UserSecretsId>
<EnableMSTestRunner>true</EnableMSTestRunner>
<OutputType>Exe</OutputType>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Integrations\Vasttrafik\Vasttrafik.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="MSTest" />
<PackageReference Include="Shouldly" />
</ItemGroup>
</Project>
tests/Integrations/Vasttrafik/VasttrafikClientTests/IntegrationTests.cs +26 -0
diff --git a/tests/Integrations/Vasttrafik/VasttrafikClientTests/IntegrationTests.cs b/tests/Integrations/Vasttrafik/VasttrafikClientTests/IntegrationTests.cs
new file mode 100644
index 0000000..4f3f799
@@ -0,0 +1,26 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
namespace MMirror.Integrations.Vasttrafik.Tests.VasttrafikClientTests;
[TestClass]
public sealed class IntegrationTests
{
[TestMethod]
public async Task IntegrationTest()
{
var host = Host.CreateApplicationBuilder();
host.Configuration.AddUserSecrets<IntegrationTests>();
host.Services.AddVasttrafikServices();
var app = host.Build();
var client = app.Services.GetRequiredService<VasttrafikClient>();
var departures = await client.GetDeparturesFrom("9021014005160000", CancellationToken.None);
departures.Select(d => d.Departures[0]).ShouldBeInOrder();
}
}
tests/Integrations/Vasttrafik/packages.lock.json +588 -0
diff --git a/tests/Integrations/Vasttrafik/packages.lock.json b/tests/Integrations/Vasttrafik/packages.lock.json
new file mode 100644
index 0000000..aef6255
@@ -0,0 +1,588 @@
{
"version": 2,
"dependencies": {
"net11.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.2.6, )",
"resolved": "1.2.6",
"contentHash": "KMSJG+jfk7vjP52QkWB99qWespXCPAzG/IaMCMRHYWumJEAGKQYm2HtyWG6eqnOwDitH96i1cqq5EVesyOtPmg=="
},
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "JabRZnPjt57VHxcOm7G7runakIkv3PunM2GTJF32KF5hJChgpUh2gnuL4VQ0GsVtHXGsbefj9tddYs2K0YJmpA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.CommandLine": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.FileExtensions": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.Json": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.UserSecrets": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.DependencyInjection": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Diagnostics": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.FileProviders.Physical": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Console": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Debug": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.EventLog": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.EventSource": "11.0.0-preview.4.26230.115"
}
},
"MSTest": {
"type": "Direct",
"requested": "[4.2.3, )",
"resolved": "4.2.3",
"contentHash": "fUaw2rGyhaSMRuAibJo+1PQkI4PHS1FZx/tASq1Lr3eZk5NlshQRyBBdUkhnvfvOG+tCKKy3T/BnMiI47qeZrw==",
"dependencies": {
"MSTest.TestAdapter": "4.2.3",
"MSTest.TestFramework": "4.2.3",
"Microsoft.NET.Test.Sdk": "18.3.0",
"Microsoft.Testing.Extensions.CodeCoverage": "18.5.2",
"Microsoft.Testing.Extensions.TrxReport": "2.2.3"
}
},
"Shouldly": {
"type": "Direct",
"requested": "[4.3.0, )",
"resolved": "4.3.0",
"contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
"dependencies": {
"DiffEngine": "11.3.0",
"EmptyFiles": "4.4.0"
}
},
"DiffEngine": {
"type": "Transitive",
"resolved": "11.3.0",
"contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
"dependencies": {
"EmptyFiles": "4.4.0",
"System.Management": "6.0.1"
}
},
"EmptyFiles": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
},
"Microsoft.ApplicationInsights": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
},
"Microsoft.Bcl.Cryptography": {
"type": "Transitive",
"resolved": "10.0.2",
"contentHash": "LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "18.3.0",
"contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g=="
},
"Microsoft.DiaSymReader": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "bhwzJfzyiJM0nXJyNB7Y9OfsEXyxLdDBHG99soIp5JjnPydwkOaBdRCtRtWgQh3noSLi2cSIZ/wpbHNNE9knxQ=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "CPb3DPC6kcgx+kuSTJ55H/A6Z+IZeW++VM+UiFpiwq6eQC2Mk3gHcFpqdQoK2MjWovWYN4Sz6n1RXhLWHVB35w=="
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "IOkgB4CnLUlKH+l0slH5Q+6m9pWfcCBocFPHc7czATwIx5jnVPgUMRj2gGhZtvGs1PKpeoZv7PCGK5cYIwlZzw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "AWgDyrMol+ywzG0MAAk+TkLIy4fMEsOCG1E/zb1kHE3Lp81GNNEMU+jOJTF1gEsLkDajY5AouNAWx3cvi+pxzg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "7vBrAODuFr5u548MNTaUybxQ+Hw/x9boZ2rWQynOS/KJkZhrriUFtgbQTeRamQFVna1eh78WjVAqEKm8GpQXBg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "//7zz5cgmkd/cfgKanPna9CHJ4o9Wp9JPpr+3rmSa7vd/+Wwxjb9Z9sUKvH0LsJl72LnUYWKJ7Ah02nr2yLsYA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.FileProviders.Physical": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "cXhGyFCd2LnPAMu5pVxf3CeIqYJ+wy866tNKjaAvLFQsJ1QDB4Xfmiy+6q0Zm8DvjektmB/IoDBl5F/0AKEBOA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.FileExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "GnJpXFEumu46mGMU++s54qgRwlkGhCP05xHOSgaAK/lLTUOt2mAMaIwFqrLFKfUFtkByjxSe/1FtzXx2zrIv9w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Json": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.FileProviders.Physical": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "8.0.2",
"contentHash": "mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "L1+sxK5JT7X5jvZ0RInbDnyJZG9rkp4cbgzoL0aGRUOOMp3PkgCcUjAUhAprxBu9LgGdeqZg7JratDMHS1YN/Q==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Options.ConfigurationExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "L/+w2lo0fV2hcQuAe3YIBwXl4YP16c/xFUCHXjhzidByFbyECb2H8hJ5EEeszihGcT69nd+gJqrZyzyp1bObig==",
"dependencies": {
"Microsoft.Extensions.FileSystemGlobbing": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "y8hEW4rVMESwL9iMfc0PICy0ScXzj4MMXclEjf5AXZKOFrJe6nu8iU7K9VVZenNweXdLYvlcs2ZSnhVz7h8oYg=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "EpZS5qqhsNXwgeSCl2m1VkysA+pTh3qlhyjkjiu0peZt/V7IyANJXQQUuv3y2kp/jzd9XdfzuUJU0Fgp7rcDGg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "YkVeVCrOfKdYghClaYH/jcC2Irx6gtrWPAOjNkOeITQVQ41vvhPVp7EcXYD8IcfYH58YIS5WiX1ZJu6uLEVrAQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Options.ConfigurationExtensions": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "1ATxqXTZnR5M4tOeCtZjA+fw1b+jWPsmUAp3PUCCvndV5ghIcSvnwZ5RiQFoTtYFSdNP+pMoxCtviI71/xyRCw==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Configuration": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "kD1p/UZ/NqHTLt1iLjtkJpPtPWWziLkV6kG3TxLi67gWasixyvH1grMfkd14XR24RIooTkavxGZJV+xjeSSsiA==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "DOtIsmOENF8xmQi0uw0sx4/1aDHDgj1PuGmlom2zobrqbJp2b5fkZPCIOTmPZsXbk78nxVnPH7/NTbR1cYRnsA==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging.Configuration": "11.0.0-preview.4.26230.115",
"System.Diagnostics.EventLog": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "koDYnhbSTo+di3NMq13c0jhYFTIc7XUgnevONLwDpzR2GbbXfUR+dN5GuMeAVR4EQljUw5OYllrYflPNb0zoZA==",
"dependencies": {
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "9QNxho6sxC4RXafVZGWzYq6KcYVpp1FkHtL+HJ967UNyUjhOWnGKMM0dPXkHKtlZ/0GGEDqnBDun0kuF633a6g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.4.26230.115"
}
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "gFA8THIk23uNF/vMdOHnjIdXD1LyA2g12cHzMJ+Xag6WpgWLw6E/6uCXxvA0gp9d2yAvkRt3xzFzMUiO/hofnQ=="
},
"Microsoft.IdentityModel.JsonWebTokens": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "6eeY+y2QFyjj3XnCz/8gJdoP5smYHTS9ow1bw2nsZzDIPjPhBZlackYTIduSMipVpxnoT/B62LkrXX2jPggOXg==",
"dependencies": {
"Microsoft.IdentityModel.Tokens": "8.19.1"
}
},
"Microsoft.IdentityModel.Logging": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "H+sMrMpdbWnwkQnpb/ESkQovtOgdefmj0ecGCcP40mDKzE5i4dUYkH6599M9mWYFNGNJnTp92l/9wLubYXWimw==",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "8.19.1"
}
},
"Microsoft.IdentityModel.Tokens": {
"type": "Transitive",
"resolved": "8.19.1",
"contentHash": "KDiuSLXud2AFVNAOottd8ztVysfPeHyr4r8gofU3/VKUXlI7oytzGTnPsNJ/B3nui17rgz8wAdWNJOtzPjkUxw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.2",
"Microsoft.IdentityModel.Logging": "8.19.1"
}
},
"Microsoft.NET.Test.Sdk": {
"type": "Transitive",
"resolved": "18.3.0",
"contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==",
"dependencies": {
"Microsoft.CodeCoverage": "18.3.0",
"Microsoft.TestPlatform.TestHost": "18.3.0"
}
},
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "mLdW+JOR3kXYGTdgR/qc/UZBA0r+eCR2k6bUxTcuDj5w9WdIQ7Lol5MBUU7YOSGd9bs9bvhSYWAptgz0YtQqCA==",
"dependencies": {
"Microsoft.ApplicationInsights": "2.23.0",
"Microsoft.Testing.Platform": "2.2.3"
}
},
"Microsoft.Testing.Extensions.TrxReport": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "9Hot3ty5ZVWHrW40k2NPfD0dCaPwIxj7j7VjujNYwpYkYw9AdbejPHjGNkL/gvUWorauJf5IkeDoUeIbS7LuUg==",
"dependencies": {
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.3",
"Microsoft.Testing.Platform": "2.2.3"
}
},
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "hntvxJEkmUAx6C2xXc/PO38DqEQl4rimzOgSvTR1hAMruMid7R4RcXOrzzF33J66gKaN7jRaQ0TMW/nNfaV9jw==",
"dependencies": {
"Microsoft.Testing.Platform": "2.2.3"
}
},
"Microsoft.Testing.Extensions.VSTestBridge": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "7WlJISO8QKUK+d+WhgnANwy4ACwUrvICnviY/mthPwjZ2gVeDaSUAeBnMy2cxfzZgm8VATtGUDbYzUxsgV2CyQ==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.3.0",
"Microsoft.Testing.Extensions.Telemetry": "2.2.3",
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.3",
"Microsoft.Testing.Platform": "2.2.3"
}
},
"Microsoft.Testing.Platform": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "LhM1/Qoi8Ams5QcD4r3f09CSOono9iQr3NEJQItFtyzWB55nWTgEOsVqXqMWWWIwk3nkPqc+XfnlJmp8xUI5fg=="
},
"Microsoft.Testing.Platform.MSBuild": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "Q22jJYJLx4srTinsAuoCskqmzjrBJC8YeGJMHHIcrf1dQeHoEZ7wsqDzTlENkMoke2qfufF7U+9u58nlZunH/Q==",
"dependencies": {
"Microsoft.Testing.Platform": "2.2.3"
}
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "18.3.0",
"contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
"resolved": "18.3.0",
"contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.3.0",
"Newtonsoft.Json": "13.0.3"
}
},
"MSTest.Analyzers": {
"type": "Transitive",
"resolved": "4.2.3",
"contentHash": "dxOZt8/LWuiox7rugInJoIa5Mmu3pBmXdfaoZOx/mxx8+sUFFpjBXPlWXQXGeWzpkVPNC3x1Jf7rt2h2Zjyvvg=="
},
"MSTest.TestAdapter": {
"type": "Transitive",
"resolved": "4.2.3",
"contentHash": "oVV/luk0bBghnVvLaw8MwFlD7It0Cx9P2nKobeqIafmTQqFFWY69Wo801dxjeNaLzO/o9WQ84WSK84gXJuubhg==",
"dependencies": {
"MSTest.TestFramework": "4.2.3",
"Microsoft.Testing.Extensions.VSTestBridge": "2.2.3",
"Microsoft.Testing.Platform.MSBuild": "2.2.3"
}
},
"MSTest.TestFramework": {
"type": "Transitive",
"resolved": "4.2.3",
"contentHash": "9zzij59YLh+tf+FRLNqhzHjmdspR91bol+jdQxLlxxTGMAML6LDbvuyXKMGdcrE84+QpKkk6KjTVBC5PBGrDeA==",
"dependencies": {
"MSTest.Analyzers": "4.2.3"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"System.CodeDom": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
},
"MMirror.Integrations.Vasttrafik": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Caching.Memory": "[11.0.0-preview.4.26230.115, )",
"Microsoft.Extensions.Http": "[11.0.0-preview.4.26230.115, )",
"System.IdentityModel.Tokens.Jwt": "[8.19.1, )"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "JLwOVRLavnY5Z2uMISBxQm+2TsbW9b5pJ9NwvtrCgCNiH/OCuJyznD7HJGvMF+5ESakq5ZrTLECNN3FMGMeoIg=="
},
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "v+F34r5W5XgR95xG0Te2KyYmCDnkc4kljLMGEZ30WKQjAzuXz/pLTEvsRNYxNJUzfKPI0H4oqLglyHZkCe1lYw=="
},
"Microsoft.Extensions.Http": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.4.26230.115, )",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "HTY8+4kSEcmghrSTifim/cwZ4hQkniUr1+c1OMLHS1DJ2d7R9FFQ/t7Wf/MYTRHlB85ng9hMQNMY/PWTIfEmKQ==",
"dependencies": {
"Microsoft.Extensions.Diagnostics": "11.0.0-preview.4.26230.115",
"Microsoft.Extensions.Logging": "11.0.0-preview.4.26230.115"
}
},
"System.IdentityModel.Tokens.Jwt": {
"type": "CentralTransitive",
"requested": "[8.19.1, )",
"resolved": "8.19.1",
"contentHash": "2VHcRtT95GAcW1E3aVBLvL2rAAMxKHXKMXKXFyWzwgkdFXZPMMvP8tVOfnRydL4vTr1RirNuGC6T8VSEF2YsPQ==",
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "8.19.1",
"Microsoft.IdentityModel.Tokens": "8.19.1"
}
}
},
"net11.0/linux-arm64": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
}
},
"net11.0/linux-x64": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
}
},
"net11.0/osx-arm64": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
}
},
"net11.0/osx-x64": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
}
},
"net11.0/win-arm64": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
}
},
"net11.0/win-x64": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.5.2",
"contentHash": "UNcGLx9pVtlXF8MPDR8KDp+/OKKNIJjpzwRyZSt609TSGvaD8mtuQMb5GKZvhMucPp0a5Juvn3kxXDceQZWmAg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.3",
"Microsoft.Extensions.DependencyModel": "8.0.2",
"Microsoft.Testing.Platform": "2.1.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.4.26230.115",
"contentHash": "/o76q92uK1eaosCQrIjp/V4OBHs9zIXxH+8ZEMFZg1GVH7fI24GUPx1BLawLCmdq/U1AdGeoto716k4BJEhi2g=="
},
"System.Management": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
"dependencies": {
"System.CodeDom": "6.0.0"
}
}
}
}
}
\ No newline at end of file