Reorganizing the structure of the project, adding unit testing, adding PokemonService and ShakespeareService.

This commit is contained in:
Eugene ;) 2020-11-22 18:38:59 +01:00
parent 6c5d77d696
commit d47641ccb4
26 changed files with 353 additions and 86 deletions

View File

@ -1,45 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using PokeApiNet;
using Pokespearean.Models.Generic;
using Pokespearean.Models.Pokemon;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Pokespearean.Controllers
{
[ApiController]
[Route("[controller]")]
public class PokemonController : ControllerBase
{
private readonly ILogger<PokemonController> _logger;
public PokemonController(ILogger<PokemonController> logger)
{
_logger = logger;
}
[Route("{pokemon}")]
public async Task<IActionResult> Get([FromRoute] string pokemon = "ho-oh")
{
var result = new WebResult();
try
{
var client = new PokeApiClient();
var pokedex = await client.GetResourceAsync<Pokedex>(pokemon);
return Ok(new PokeResult
{
Name = pokedex.Name,
Description = pokedex.Descriptions.FirstOrDefault(d => d.Language.Name == "en").Description
});
}
catch (Exception ex)
{
return BadRequest(result.Invalidate(ex.Message, ex));
}
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Text.Json.Serialization;
namespace Pokespearean.Models.Generic
{
public class WebResult
{
[JsonIgnore]
public bool IsValid { get; set; } = true;
[JsonIgnore]
public Exception Exception { get; set; }
[JsonIgnore]
public object Data { get; set; }
public string ErrorMessage { get; set; }
public WebResult Invalidate(string errorMessage, Exception exception = default)
{
IsValid = false;
ErrorMessage = errorMessage;
Exception = exception;
return this;
}
}
}

View File

@ -0,0 +1,8 @@
namespace Pokespearean.Models.Pokemon
{
public class PokeResult
{
public string Name { get; set; }
public string Description { get; set; }
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="5.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,18 @@
namespace Pokespearean.Models.Shakespeare
{
public class ShakespearResponse
{
public ShakespearResponseSuccess Success { get; set; }
public ShakespearResponseContents Contents { get; set; }
}
public class ShakespearResponseSuccess
{
public int Total { get; set; }
}
public class ShakespearResponseContents
{
public string Translated { get; set; }
public string Text { get; set; }
public string Translation { get; set; }
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Pokespearean.Controllers;
using Pokespearean.Models.Generic;
using Pokespearean.Models.Pokemon;
using Pokespearean.Services;
using System.Threading.Tasks;
using Xunit;
namespace Pokespearean.Tests
{
public class PokemonControllerTests
{
private readonly IPokemonService PokemonService;
private readonly IShakespeareService ShakespeareService;
public PokemonControllerTests(IPokemonService pokemonService, IShakespeareService shakespeareService)
{
PokemonService = pokemonService;
ShakespeareService = shakespeareService;
}
[Theory]
[InlineData("charizard")]
public async Task GetTest(string pokemonName)
{
var controller = new PokemonController(PokemonService, ShakespeareService);
Assert.NotNull(controller);
var result = await controller.Get(pokemonName);
Assert.NotNull(result);
Assert.IsType<OkObjectResult>(result);
var okResult = result as OkObjectResult;
Assert.IsType<PokeResult>(okResult.Value);
var pokemon = okResult.Value as PokeResult;
Assert.Equal("charizard", pokemon.Name);
Assert.Equal("Charizard flies 'round the sky in search of powerful opponents. 't breathes fire of such most wondrous heat yond 't melts aught. However, 't nev'r turns its fiery breath on any opponent weaker than itself.",
pokemon.Description);
}
[Theory]
[InlineData("amIaPokemon?")]
public async Task GetWithWrongPokemonTest(string pokemonName)
{
var controller = new PokemonController(PokemonService, ShakespeareService);
Assert.NotNull(controller);
var result = await controller.Get(pokemonName);
Assert.NotNull(result);
Assert.IsType<BadRequestObjectResult>(result);
var badRequestResult = result as BadRequestObjectResult;
Assert.IsType<WebResult>(badRequestResult.Value);
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Xunit.DependencyInjection" Version="7.1.0" />
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Pokespearean\Pokespearean.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Pokespearean.Models;
using Pokespearean.Services;
namespace Pokespearean.Tests
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPokemonService, PokemonService>();
services.AddScoped<IShakespeareService, ShakespeareService>();
services.Configure<Settings>(s => s.ShakespeareApi = "https://api.funtranslations.com");
}
}
}

View File

@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 16
VisualStudioVersion = 16.0.30711.63 VisualStudioVersion = 16.0.30711.63
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pokespearean", "Pokespearean.csproj", "{B9920ADA-7244-4E6A-8935-4EE1D66273D7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pokespearean", "Pokespearean\Pokespearean.csproj", "{9EE4EE5B-670B-44C4-B327-FED8A35AABA8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pokespearean.Models", "..\Pokespearean.Models\Pokespearean.Models.csproj", "{B62A4320-ED4F-4B29-9CE7-56EC1EBA4A26}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pokespearean.Models", "Pokespearean.Models\Pokespearean.Models.csproj", "{AA12865F-0EC1-4057-AC55-A1CC9B0A66AF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pokespearean.Tests", "Pokespearean.Tests\Pokespearean.Tests.csproj", "{2B740E78-E3E7-4285-BE87-27B188BE0B01}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -13,14 +15,18 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B9920ADA-7244-4E6A-8935-4EE1D66273D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9EE4EE5B-670B-44C4-B327-FED8A35AABA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9920ADA-7244-4E6A-8935-4EE1D66273D7}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EE4EE5B-670B-44C4-B327-FED8A35AABA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9920ADA-7244-4E6A-8935-4EE1D66273D7}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EE4EE5B-670B-44C4-B327-FED8A35AABA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9920ADA-7244-4E6A-8935-4EE1D66273D7}.Release|Any CPU.Build.0 = Release|Any CPU {9EE4EE5B-670B-44C4-B327-FED8A35AABA8}.Release|Any CPU.Build.0 = Release|Any CPU
{B62A4320-ED4F-4B29-9CE7-56EC1EBA4A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AA12865F-0EC1-4057-AC55-A1CC9B0A66AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B62A4320-ED4F-4B29-9CE7-56EC1EBA4A26}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA12865F-0EC1-4057-AC55-A1CC9B0A66AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B62A4320-ED4F-4B29-9CE7-56EC1EBA4A26}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA12865F-0EC1-4057-AC55-A1CC9B0A66AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B62A4320-ED4F-4B29-9CE7-56EC1EBA4A26}.Release|Any CPU.Build.0 = Release|Any CPU {AA12865F-0EC1-4057-AC55-A1CC9B0A66AF}.Release|Any CPU.Build.0 = Release|Any CPU
{2B740E78-E3E7-4285-BE87-27B188BE0B01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B740E78-E3E7-4285-BE87-27B188BE0B01}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B740E78-E3E7-4285-BE87-27B188BE0B01}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B740E78-E3E7-4285-BE87-27B188BE0B01}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Mvc;
using Pokespearean.Models.Generic;
using Pokespearean.Models.Pokemon;
using Pokespearean.Services;
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
namespace Pokespearean.Controllers
{
[ApiController]
[Route("[controller]")]
public class PokemonController : ControllerBase
{
private readonly IPokemonService PokemonService;
private readonly IShakespeareService ShakespeareService;
public PokemonController(IPokemonService pokemonService,
IShakespeareService shakespeareService)
{
PokemonService = pokemonService;
ShakespeareService = shakespeareService;
}
[Route("{pokemonName}")]
public async Task<IActionResult> Get([FromRoute, Required] string pokemonName)
{
var result = new WebResult();
try
{
result = await PokemonService.GetPokemonDescription(pokemonName);
if (!result.IsValid)
return BadRequest(result);
var pokemonDescription = result.Data as string;
result = await ShakespeareService.ToShakespearean(pokemonDescription);
if (!result.IsValid)
return BadRequest(result);
var shakespeareTranslation = result.Data as string;
var pokeResult = new PokeResult
{
Name = pokemonName,
Description = shakespeareTranslation
};
return Ok(pokeResult);
}
catch (Exception ex)
{
return BadRequest(result.Invalidate(ex.Message, ex));
}
}
}
}

View File

@ -1,7 +1,6 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
USER 1001
WORKDIR /app WORKDIR /app
EXPOSE 80 EXPOSE 80

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Pokespearean.Models
{
public class Settings
{
public string ShakespeareApi { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using Pokespearean.Models.Generic;
using System.Threading.Tasks;
namespace Pokespearean.Services
{
public interface IPokemonService
{
Task<WebResult> GetPokemonDescription(string pokemonName);
}
}

View File

@ -0,0 +1,10 @@
using Pokespearean.Models.Generic;
using System.Threading.Tasks;
namespace Pokespearean.Services
{
public interface IShakespeareService
{
Task<WebResult> ToShakespearean(string text);
}
}

View File

@ -0,0 +1,38 @@
using PokeApiNet;
using Pokespearean.Models.Generic;
using System;
using System.Threading.Tasks;
using System.Linq;
namespace Pokespearean.Services
{
public class PokemonService : IPokemonService
{
private readonly PokeApiClient pokeApiClient;
public PokemonService()
{
pokeApiClient = new PokeApiClient();
}
public async Task<WebResult> GetPokemonDescription(string pokemonName)
{
var result = new WebResult();
try
{
var pokemonSpecies = await pokeApiClient.GetResourceAsync<PokemonSpecies>(pokemonName);
var hasRubyVersion = pokemonSpecies.FlavorTextEntries.Where(fte => fte.Language.Name == "en" && fte.Version.Name == "ruby").Any();
result.Data = pokemonSpecies.FlavorTextEntries
.FirstOrDefault(fte => (hasRubyVersion ? fte.Version.Name == "ruby" : true) && fte.Language.Name == "en")?
.FlavorText?.Replace("\n", " ")?.Replace("\f", " ");
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return result.Invalidate(ex.Message, ex);
}
}
}
}

View File

@ -0,0 +1,43 @@
using Pokespearean.Models.Generic;
using System;
using System.Threading.Tasks;
using Pokespearean.Models;
using Pokespearean.Models.Shakespeare;
using Flurl;
using Flurl.Http;
using Microsoft.Extensions.Options;
namespace Pokespearean.Services
{
public class ShakespeareService : IShakespeareService
{
private string shakespeareApi;
public ShakespeareService(IOptions<Settings> settingsOption)
{
shakespeareApi = settingsOption.Value.ShakespeareApi;
}
public async Task<WebResult> ToShakespearean(string text)
{
var result = new WebResult();
try
{
var response = await shakespeareApi
.AppendPathSegment("translate")
.AppendPathSegment("shakespeare.json")
.SetQueryParam(nameof(text), text, isEncoded: true)
.GetJsonAsync<ShakespearResponse>();
result.Data = response.Contents.Translated;
return result;
}
catch (System.Exception ex)
{
Console.WriteLine(ex.Message);
return result.Invalidate(ex.Message, ex);
}
}
}
}

View File

@ -1,15 +1,10 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Pokespearean.Models;
using System; using Pokespearean.Services;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace Pokespearean namespace Pokespearean
{ {
@ -22,21 +17,19 @@ namespace Pokespearean
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddScoped<IPokemonService, PokemonService>();
services.AddScoped<IShakespeareService, ShakespeareService>();
services.Configure<Settings>(Configuration.GetSection(nameof(Settings)));
services.AddControllers().AddJsonOptions(options => services.AddControllers().AddJsonOptions(options =>
{ {
//options.JsonSerializerOptions.MaxDepth = 0;
//options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
//options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.IgnoreReadOnlyFields = true; options.JsonSerializerOptions.IgnoreReadOnlyFields = true;
options.JsonSerializerOptions.IgnoreReadOnlyProperties = true; options.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
}); });
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())
@ -45,9 +38,6 @@ namespace Pokespearean
} }
app.UseRouting(); app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers(); endpoints.MapControllers();

View File

@ -1,4 +1,7 @@
{ {
"Settings": {
"ShakespeareApi": "https://api.funtranslations.com"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",

View File

@ -1,15 +0,0 @@
using System;
namespace Pokespearean
{
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
}
}