Menu
Menu
Contattaci
Il nostro settore richiede uno studio continuo e una forte attenzione all’innovazione. Incentiviamo quindi dei programmi formativi annuali per tutti i componenti del team, con ore dedicate (durante l’orario di lavoro) e serate formative sia online che in presenza. Sponsorizziamo eventi, sia come partner che semplicemente come partecipanti, e scriviamo articoli su quello che abbiamo imparato per essere, a nostra volta, dei divulgatori.
Vai alla sezione Team
dotnet new console -o SpectreExample.Weather
dotnet add package Spectre.Cli
dotnet add package Microsoft.Extensions.Hosting
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<!-- new project! why not? -->
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<!-- ... -->
</Project>
internal class ForecastCommand : AsyncCommand<ForecastCommand.Settings>
{
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
throw new NotImplementedException();
}
internal sealed class Settings : CommandSettings
{
public Settings(string location) => Location = location;
[CommandArgument(0, "<location>")]
[Description("The place where you want to know the forecast for")]
public string Location { get; }
}
}
internal sealed class TypeResolver : ITypeResolver
{
private readonly IServiceProvider _provider;
public TypeResolver(IServiceProvider provider) => _provider = provider;
public object? Resolve(Type? type)
{
if (type == null)
{
return null;
}
return _provider.GetService(type);
}
}
internal sealed class TypeRegistrar : ITypeRegistrar
{
private readonly IServiceCollection _provider;
public TypeRegistrar(IServiceCollection provider) => _provider = provider;
public ITypeResolver Build() => new TypeResolver(_provider.BuildServiceProvider());
public void Register(Type service, Type implementation) =>
_provider.AddSingleton(service, implementation);
public void RegisterInstance(Type service, object implementation) =>
_provider.AddSingleton(service, implementation);
public void RegisterLazy(Type service, Func<object> factory) =>
_provider.AddSingleton(service, _ => factory());
}
public static class HostingExtensions
{
public static IServiceCollection AddSpectre<TDefaultCommand>(
this IServiceCollection services,
Action<IConfigurator>? configurator = null
)
where TDefaultCommand : class, ICommand
{
var app = new CommandApp<TDefaultCommand>(new TypeRegistrar(services));
if (configurator != null)
{
app.Configure(configurator);
}
services.AddSingleton<ICommandApp>(app);
return services;
}
public static async Task<int> RunSpectre(this IHost host, string[] args)
{
if (host is null)
{
throw new ArgumentNullException(nameof(host));
}
var app = host.Services.GetService<ICommandApp>();
if (app == null)
{
throw new InvalidOperationException("Command application has not been configured.");
}
try
{
return await app.RunAsync(args);
}
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
return -99;
}
}
}
// Program.cs
var builder = Host.CreateDefaultBuilder();
builder.ConfigureLogging(
(context, loggingBuilder) =>
{
// da non fare in un applicazione reale!
// in questo esempio ci serve solo per disabilitare il logging in Console
if (context.HostingEnvironment.IsProduction())
{
loggingBuilder.ClearProviders();
}
}
);
builder.ConfigureServices(
(context, services) =>
{
services.AddSpectre<ForecastCommand>(
config =>
{
config.SetApplicationName("wth");
if (context.HostingEnvironment.IsDevelopment())
{
config.PropagateExceptions();
config.ValidateExamples();
}
}
);
}
);
var host = builder.Build();
return await host.RunSpectre(args);
Error: The method or operation is not implemented.
Error: Missing required argument 'location'.
USAGE: wth <location> [OPTIONS] ARGUMENTS: <location> The place where you want to know the forecast for OPTIONS: -h, --help Prints help information
dotnet add package Refit.HttpClientFactory
builder.ConfigureServices(
(context, services) =>
{
services.AddRefitClient<IOpenWeatherApiClient>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.openweathermap.org/data/2.5"));
// spectre stuff
}
);
internal class ForecastCommand : AsyncCommand<ForecastCommand.Settings>
{
private static readonly TextInfo TextInfo = CultureInfo.CurrentCulture.TextInfo;
private readonly IOpenWeatherApiClient _weatherApiClient;
// dependency injection grazie al boilerplate scritto in precedenza
public ForecastCommand(IOpenWeatherApiClient weatherApiClient)
{
_weatherApiClient = weatherApiClient;
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
// chiamata alle API di OpenWeatherMap
var response = await _weatherApiClient.GetWeatherForecastAsync("LA_VOSTRA_API_KEY", settings.Location);
// le API ritornano una previsione ogni 3 ore per 5 giorni.
// semplicemente ne prendiamo una sola occorennza per ogni giorno e ignoriamo quella per il giorno corrente
var forecasts = OneWeatherForEachDay(response).Skip(1).OrderBy(x => x.Date);
// questi 2 metodi sono quelli che andremo a implementare insieme
var title = Title(response.City);
var forecastPanels = forecasts.Select(CreateForecastPanel);
// scrittura sulla console dei Widget creati in precedenza
AnsiConsole.Write(title);
AnsiConsole.WriteLine();
// Columns è un helper per renderizzare una collezione di Widget in modo colonnare
AnsiConsole.Write(new Columns(forecastPanels).Collapse());
return 0;
}
private static Rule Title(City city)
{
throw new NotImplementedException();
}
private static Panel CreateForecastPanel(Forecast forecast)
{
throw new NotImplementedException();
}
private static IEnumerable<Forecast> OneWeatherForEachDay(WeatherForecastResponse res) =>
res.List.GroupBy(x => x.Date.Date).Select(x => x.First());
private static string WindDirection(int deg) => deg switch
{
> 0 and < 90 => "N/E",
> 90 and < 180 => "S/E",
> 180 and < 270 => "S/O",
> 270 => "N/O",
0 => "N",
90 => "E",
180 => "S",
270 => "O",
_ => throw new ArgumentOutOfRangeException(nameof(deg), deg, null)
};
internal sealed class Settings : CommandSettings
{
public Settings(string location)
{
Location = location;
}
[CommandArgument(0, "<location>")]
[Description("The place where you want to know the forecast for")]
public string Location { get; }
}
}
private static Rule Title(City city)
{
return new Rule($"[orange1 underline]Previsione a 5 giorni per [bold]{city.Name}, {city.Country}[/][/]")
.RuleStyle("deepskyblue1")
.LeftAligned();
}
private static Panel CreateForecastPanel(Forecast forecast)
{
var infoTable = CreateInfoTable(forecast);
return new Panel(infoTable)
.BorderColor(Color.Grey53)
.Header($"[white underline bold]{TextInfo.ToTitleCase(forecast.Date.ToString("dddd dd"))}[/]")
.HeaderAlignment(Justify.Center)
.RoundedBorder();
}
private static Table CreateInfoTable(Forecast forecast)
{
var infoTable = new Table()
.SimpleBorder()
.BorderColor(Color.Grey53)
.Collapse();
infoTable.AddColumn($"[bold orange1]{TextInfo.ToTitleCase(forecast.Weather[0].Description)}[/]");
infoTable.AddRow(
$"Temperatura [red]{forecast.Main.Temp:F0}°C[/], Percepita [deepskyblue1]{forecast.Main.FeelsLike:F0}°C[/]"
);
infoTable.AddRow($"Probabilità di precipitazioni {forecast.PrecipitationProbability:p0}");
infoTable.AddRow($"Nuvolosità {forecast.Clouds.All}%");
infoTable.AddRow($"Pressione {forecast.Main.Pressure} hPa");
infoTable.AddRow($"Umidità {forecast.Main.Humidity}%");
infoTable.AddRow($"Vento {forecast.Wind.Speed} m/s, {WindDirection(forecast.Wind.Deg)}");
return infoTable;
}
internal static class ApiKeyService
{
private static readonly string UserDirectory = Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify
);
private static readonly string SettingsFolder = Path.Join(UserDirectory, ".wth");
// la chiave sarà salvata all'interno della cartella del vostro profilo utente nel file .wth/api_key.txt
private static readonly string SettingsFilePath = Path.Join(SettingsFolder, "api_key.txt");
public static Task SaveApiKey(string apiKey)
{
if (!Directory.Exists(SettingsFolder))
{
Directory.CreateDirectory(SettingsFolder);
}
return File.WriteAllTextAsync(SettingsFilePath, apiKey);
}
public static async Task<string?> GetApiKey()
{
try
{
return await File.ReadAllTextAsync(SettingsFilePath);
}
catch (DirectoryNotFoundException)
{
return null;
}
}
}
internal class AddKeyCommand : AsyncCommand<AddKeyCommand.Settings>
{
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
await ApiKeyService.SaveApiKey(settings.ApiKey);
return 0;
}
internal sealed class Settings : CommandSettings
{
public Settings(string apiKey)
{
ApiKey = apiKey;
}
[CommandArgument(0, "<ApiKey>")]
[Description("Your OpenWeatherMap ApiKey")]
public string ApiKey { get; }
}
}
services.AddSpectre<ForecastCommand>(
config =>
{
/*...*/
config.AddCommand<AddKeyCommand>("add-key")
.WithDescription("Save your personal OpenWeatherMap ApiKey");
}
);
USAGE: wth <location> [OPTIONS] ARGUMENTS: <location> The place where do you want to know the forecast OPTIONS: -h, --help Prints help information COMMANDS: add-key <ApiKey> Save your personal OpenWeatherMap ApiKey
internal class ForecastCommand : AsyncCommand<ForecastCommand.Settings>
{
/*...*/
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
var apiKey = await ApiKeyService.GetApiKey();
if (apiKey is null)
{
AnsiConsole.MarkupLine(
"[red][bold]No ApiKey configured![/] Please run the 'wth add-key <ApiKey>' command.[/]"
);
return -99;
}
var response = await _weatherApiClient.GetWeatherForecastAsync(apiKey, settings.Location);
var forecasts = OneWeatherForEachDay(response).Skip(1).OrderBy(x => x.Date);
var title = Title(response.City);
var forecastPanels = forecasts.Select(CreateForecastPanel);
AnsiConsole.Write(title);
AnsiConsole.WriteLine();
AnsiConsole.Write(new Columns(forecastPanels).Collapse());
return 0;
}
/*...*/
}
<PropertyGroup> <PackAsTool>true</PackAsTool> <ToolCommandName>wth</ToolCommandName> <PackageOutputPath>./nupkg</PackageOutputPath> </PropertyGroup>
dotnet pack -c Release
dotnet tool install --global --add-source ./nupkg SpectreExample.Weather
You can invoke the tool using the following command: wth Tool 'spectreexample.weather' (version '1.0.0') was successfully installed.
wth Londra
dotnet tool uninstall --global SpectreExample.Weather
Segui Giuneco sui social