From 4a11ed61df73c85ecc783728e3aefcb0c74f298c Mon Sep 17 00:00:00 2001 From: Mikhail Shabanov Date: Sun, 7 Jun 2020 14:39:03 +0200 Subject: [PATCH] Vehicles dictionary updater --- .../Dictionaries/VehiclePriceInXp.cs | 18 ++ .../Dictionaries/VehiclesDictionary.cs | 96 +++++++++++ .../DictionariesDataAccessor.cs | 10 ++ .../IDictionariesDataAccessor.cs | 2 + .../Dictionaries/VehiclesDictionaryUpdater.cs | 51 +++++- .../Mappers/VehicleDictionaryProfile.cs | 39 +++++ .../Mappers/VehiclePriceProfile.cs | 19 ++ .../VehiclesDictionaryUpdaterTests.cs | 163 ++++++++++++++++++ .../IWargamingDictionariesApiClient.cs | 4 + .../Model/WotEncyclopediaVehiclesResponse.cs | 117 +++++++++++++ .../WargamingApiClient.cs | 10 ++ 11 files changed, 525 insertions(+), 4 deletions(-) create mode 100644 WotBlitzStatisticsPro.Common/Dictionaries/VehiclePriceInXp.cs create mode 100644 WotBlitzStatisticsPro.Common/Dictionaries/VehiclesDictionary.cs create mode 100644 WotBlitzStatisticsPro.Logic/Mappers/VehicleDictionaryProfile.cs create mode 100644 WotBlitzStatisticsPro.Logic/Mappers/VehiclePriceProfile.cs create mode 100644 WotBlitzStatisticsPro.Tests/DictionariesTests/VehiclesDictionaryUpdaterTests.cs create mode 100644 WotBlitzStatisticsPro.WgApiClient/Model/WotEncyclopediaVehiclesResponse.cs diff --git a/WotBlitzStatisticsPro.Common/Dictionaries/VehiclePriceInXp.cs b/WotBlitzStatisticsPro.Common/Dictionaries/VehiclePriceInXp.cs new file mode 100644 index 0000000..a207dd8 --- /dev/null +++ b/WotBlitzStatisticsPro.Common/Dictionaries/VehiclePriceInXp.cs @@ -0,0 +1,18 @@ +namespace WotBlitzStatisticsPro.Common.Dictionaries +{ + /// + /// Price of vehicle + /// + public class VehiclePriceInXp + { + /// + /// VehicleId + /// + public long TankId { get; set; } + + /// + /// Vehicle price in XP + /// + public long PricesXp { get; set; } + } +} \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Common/Dictionaries/VehiclesDictionary.cs b/WotBlitzStatisticsPro.Common/Dictionaries/VehiclesDictionary.cs new file mode 100644 index 0000000..4355ffd --- /dev/null +++ b/WotBlitzStatisticsPro.Common/Dictionaries/VehiclesDictionary.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; + +namespace WotBlitzStatisticsPro.Common.Dictionaries +{ + /// + /// Vehicle dictionary item + /// + public class VehiclesDictionary + { + /// + /// VehicleId + /// + public long TankId { get; set; } + + /// + /// Vehicle name + /// + public List Name { get; set; } + + /// + /// Vehicle description + /// + public List Description { get; set; } + + /// + /// Is vehicle premium + /// + public bool IsPremium { get; set; } + + /// + /// Vehicle type id + /// + public string TypeId { get; set; } + + /// + /// Vehicle nationId + /// + public string NationId { get; set; } + + /// + /// Vehicle tier + /// + public int Tier { get; set; } + + /// + /// Vehicle preview image + /// + public string PreviewImage { get; set; } + + /// + /// Vehicle normal image + /// + public string NormalImage { get; set; } + + /// + /// Vehicle price in credits + /// + public decimal PriceCredit { get; set; } + + /// + /// Vehicle price in gold + /// + public decimal PriceGold { get; set; } + + /// + /// Ids of nex tanks and their cost in XP + /// + public List NexTanksInTree { get; set; } + + /// + /// Price in XP for parent vehicle as key + /// + public List PriceXp { get; set; } + + /// + /// Engines id list + /// + public int[] Engines { get; set; } + + /// + /// Guns id list + /// + public int[] Guns { get; set; } + + /// + /// List of compatible suspensions + /// + public int[] Suspensions { get; set; } + + /// + /// List of compatible turrets ids + /// + public int[] Turrets { get; set; } + + } +} \ No newline at end of file diff --git a/WotBlitzStatisticsPro.DataAccess/DictionariesDataAccessor.cs b/WotBlitzStatisticsPro.DataAccess/DictionariesDataAccessor.cs index b7bd5f0..aa3c317 100644 --- a/WotBlitzStatisticsPro.DataAccess/DictionariesDataAccessor.cs +++ b/WotBlitzStatisticsPro.DataAccess/DictionariesDataAccessor.cs @@ -14,6 +14,7 @@ namespace WotBlitzStatisticsPro.DataAccess private const string ClanRolesCollectionName = "dictionary-clan-roles"; private const string AchievementsSectionsCollectionName = "dictionary-achievements-sections"; private const string AchievementsCollectionName = "dictionary-achievements"; + private const string VehiclesCollectionName = "dictionary-vehicles"; private readonly IMongoDatabase _database; @@ -72,5 +73,14 @@ namespace WotBlitzStatisticsPro.DataAccess await dbAchievementSections.DeleteManyAsync(Builders.Filter.Empty); await dbAchievementSections.InsertManyAsync(achievementDictionary); } + + public async Task UpdateVehicles(List vehiclesDictionary) + { + var dbVehicles = + _database.GetCollection(VehiclesCollectionName); + + await dbVehicles.DeleteManyAsync(Builders.Filter.Empty); + await dbVehicles.InsertManyAsync(vehiclesDictionary); + } } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.DataAccess/IDictionariesDataAccessor.cs b/WotBlitzStatisticsPro.DataAccess/IDictionariesDataAccessor.cs index add465b..01e3649 100644 --- a/WotBlitzStatisticsPro.DataAccess/IDictionariesDataAccessor.cs +++ b/WotBlitzStatisticsPro.DataAccess/IDictionariesDataAccessor.cs @@ -17,5 +17,7 @@ namespace WotBlitzStatisticsPro.DataAccess Task UpdateAchievementsSections(List achievementSections); Task UpdateAchievements(List achievementDictionary); + + Task UpdateVehicles(List vehiclesDictionary); } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Logic/Dictionaries/VehiclesDictionaryUpdater.cs b/WotBlitzStatisticsPro.Logic/Dictionaries/VehiclesDictionaryUpdater.cs index 299c601..40716ed 100644 --- a/WotBlitzStatisticsPro.Logic/Dictionaries/VehiclesDictionaryUpdater.cs +++ b/WotBlitzStatisticsPro.Logic/Dictionaries/VehiclesDictionaryUpdater.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using AutoMapper; +using WotBlitzStatisticsPro.Common.Dictionaries; using WotBlitzStatisticsPro.Common.Model; using WotBlitzStatisticsPro.DataAccess; using WotBlitzStatisticsPro.WgApiClient; +using WotBlitzStatisticsPro.WgApiClient.Model; namespace WotBlitzStatisticsPro.Logic.Dictionaries { @@ -16,19 +20,58 @@ namespace WotBlitzStatisticsPro.Logic.Dictionaries [Obsolete("Parameter-less constructor only for unit tests")] public VehiclesDictionaryUpdater() { - + } - public VehiclesDictionaryUpdater(IWargamingDictionariesApiClient wargamingDictionariesApiClient, IDictionariesDataAccessor dataAccessor, IMapper mapper) + public VehiclesDictionaryUpdater(IWargamingDictionariesApiClient wargamingDictionariesApiClient, + IDictionariesDataAccessor dataAccessor, IMapper mapper) { _wargamingDictionariesApiClient = wargamingDictionariesApiClient; _dataAccessor = dataAccessor; _mapper = mapper; } - public virtual Task Update() + public virtual async Task Update() { - throw new System.NotImplementedException(); + var vehiclesDictionary = await GetAndMapVehiclesDictionary(); + + await _dataAccessor.UpdateVehicles(vehiclesDictionary); + + return new UpdateDictionariesResponseItem + { + DictionaryType = DictionaryType.Vehicles, + Description = $"Got and saved {vehiclesDictionary.Count} vehicles dictionary items" + }; + } + + private async Task> GetAndMapVehiclesDictionary() + { + var defaultRealmType = RealmType.Eu; + + var vehicles = new List(); + + foreach (var requestLanguage in (RequestLanguage[]) Enum.GetValues(typeof(RequestLanguage))) + { + var wgVehicles = + await _wargamingDictionariesApiClient.GetVehicles(defaultRealmType, requestLanguage); + + foreach (var wgVehicle in wgVehicles) + { + var mappedVehicle = vehicles.FirstOrDefault(v => v.TankId == wgVehicle.TankId); + if (mappedVehicle == null) + { + mappedVehicle = _mapper.Map(wgVehicle); + vehicles.Add(mappedVehicle); + } + mappedVehicle.Name.Add(new LocalizableString {Language = requestLanguage, Value = wgVehicle.Name}); + mappedVehicle.Description.Add(new LocalizableString {Language = requestLanguage, Value = wgVehicle.Description}); + } + + } + + return vehicles; + } + } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Logic/Mappers/VehicleDictionaryProfile.cs b/WotBlitzStatisticsPro.Logic/Mappers/VehicleDictionaryProfile.cs new file mode 100644 index 0000000..4579462 --- /dev/null +++ b/WotBlitzStatisticsPro.Logic/Mappers/VehicleDictionaryProfile.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using AutoMapper; +using WotBlitzStatisticsPro.Common.Dictionaries; +using WotBlitzStatisticsPro.WgApiClient.Model; + +namespace WotBlitzStatisticsPro.Logic.Mappers +{ + public class VehicleDictionaryProfile : Profile + { + public VehicleDictionaryProfile() + { + CreateMap() + .ForMember(d => d.TankId, + o => o.MapFrom(s => s.TankId)) + .ForMember(d => d.Name, o => o.MapFrom(_ => new List())) + .ForMember(d => d.Description, o => o.MapFrom(_ => new List())) + .ForMember(d => d.TypeId, + o => o.MapFrom(s => s.Type)) + .ForMember(d => d.NationId, + o => o.MapFrom(s => s.Nation)) + .ForMember(d => d.Tier, + o => o.MapFrom(s => s.Tier)) + .ForMember(d => d.PreviewImage, + o => o.MapFrom(s => s.Images["preview"])) + .ForMember(d => d.NormalImage, + o => o.MapFrom(s => s.Images["normal"])) + .ForMember(d => d.PriceCredit, + o => o.MapFrom(s => s.Cost["price_credit"])) + .ForMember(d => d.PriceGold, + o => o.MapFrom(s => s.Cost["price_gold"])) + .ForMember(d => d.NexTanksInTree, + o => + o.MapFrom(s => s.NextTanks)) + .ForMember(d => d.PriceXp, + o => + o.MapFrom(s => s.PricesXp)); + } + } +} \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Logic/Mappers/VehiclePriceProfile.cs b/WotBlitzStatisticsPro.Logic/Mappers/VehiclePriceProfile.cs new file mode 100644 index 0000000..1602e82 --- /dev/null +++ b/WotBlitzStatisticsPro.Logic/Mappers/VehiclePriceProfile.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using AutoMapper; +using WotBlitzStatisticsPro.Common.Dictionaries; + +namespace WotBlitzStatisticsPro.Logic.Mappers +{ + public class VehiclePriceProfile : Profile + { + public VehiclePriceProfile() + { + CreateMap, VehiclePriceInXp>() + .ForMember(d => d.TankId, + o => o.MapFrom(s => Convert.ToInt64(s.Key))) + .ForMember(d => d.PricesXp, + o => o.MapFrom(s => Convert.ToInt64(s.Value))); + } + } +} \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Tests/DictionariesTests/VehiclesDictionaryUpdaterTests.cs b/WotBlitzStatisticsPro.Tests/DictionariesTests/VehiclesDictionaryUpdaterTests.cs new file mode 100644 index 0000000..f7eeda0 --- /dev/null +++ b/WotBlitzStatisticsPro.Tests/DictionariesTests/VehiclesDictionaryUpdaterTests.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.Configuration; +using Bogus; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using WotBlitzStatisticsPro.Common.Dictionaries; +using WotBlitzStatisticsPro.Common.Model; +using WotBlitzStatisticsPro.DataAccess; +using WotBlitzStatisticsPro.Logic.Dictionaries; +using WotBlitzStatisticsPro.Logic.Mappers; +using WotBlitzStatisticsPro.WgApiClient; +using WotBlitzStatisticsPro.WgApiClient.Model; + +namespace WotBlitzStatisticsPro.Tests.DictionariesTests +{ + [TestFixture] + public class VehiclesDictionaryUpdaterTests + { + private VehiclesDictionaryUpdater _vehiclesDictionariesUpdater; + private Mock _wargamingDictionariesApiClientMock; + private Mock _dataAccessorMock; + + private List _vehiclesInfoResponseEn; + private List _vehiclesInfoResponseRu; + private List _vehiclesInfoResponseDe; + + [SetUp] + public void Init() + { + // Generating fake data + _vehiclesInfoResponseEn = GetVehiclesInfoResponse("en"); + _vehiclesInfoResponseRu = GetVehiclesInfoResponse("ru", _vehiclesInfoResponseEn); + _vehiclesInfoResponseDe = GetVehiclesInfoResponse("de", _vehiclesInfoResponseEn); + + + // Real mapper instance + var configurationExp = new MapperConfigurationExpression(); + configurationExp.AddProfile(); + configurationExp.AddProfile(); + var mapperConfiguration = new MapperConfiguration(configurationExp); + IMapper mapper = new Mapper(mapperConfiguration); + + // Wargaming client mock + _wargamingDictionariesApiClientMock = new Mock(); + _wargamingDictionariesApiClientMock.Setup(w => + w.GetVehicles(It.IsAny(), RequestLanguage.En)) + .ReturnsAsync(_vehiclesInfoResponseEn); + _wargamingDictionariesApiClientMock.Setup(w => + w.GetVehicles(It.IsAny(), RequestLanguage.Ru)) + .ReturnsAsync(_vehiclesInfoResponseRu); + _wargamingDictionariesApiClientMock.Setup(w => + w.GetVehicles(It.IsAny(), RequestLanguage.De)) + .ReturnsAsync(_vehiclesInfoResponseDe); + + // MongoDb data accessor mock + _dataAccessorMock = new Mock(); + + _vehiclesDictionariesUpdater = new VehiclesDictionaryUpdater( + _wargamingDictionariesApiClientMock.Object, + _dataAccessorMock.Object, + mapper); + + } + + [Test] + public async Task ShouldCallWargamingApi3Times() + { + await _vehiclesDictionariesUpdater.Update(); + + // Should call wargaming API once for each language + _wargamingDictionariesApiClientMock.Verify(d => d.GetVehicles(It.IsAny(), RequestLanguage.En), Times.Once); + _wargamingDictionariesApiClientMock.Verify(d => d.GetVehicles(It.IsAny(), RequestLanguage.Ru), Times.Once); + _wargamingDictionariesApiClientMock.Verify(d => d.GetVehicles(It.IsAny(), RequestLanguage.De), Times.Once); + } + + [Test] + public async Task ShouldSaveAppropriateVehiclesDictionary() + { + List targetVehicleDictionary = null; + _dataAccessorMock + .Setup(d => d.UpdateVehicles(It.IsAny>())) + .Callback((List dictionary) => targetVehicleDictionary = dictionary); + + await _vehiclesDictionariesUpdater.Update(); + + targetVehicleDictionary.Should().NotBeNull(); + targetVehicleDictionary.Should().HaveCount(targetVehicleDictionary.Count); + + for (var i = 0; i < targetVehicleDictionary.Count; i++) + { + targetVehicleDictionary[i].TankId.Should() + .Be(_vehiclesInfoResponseEn[i].TankId); + targetVehicleDictionary[i].Name.First(n => n.Language == RequestLanguage.En).Value.Should() + .Be(_vehiclesInfoResponseEn[i].Name); + targetVehicleDictionary[i].Name.First(n => n.Language == RequestLanguage.Ru).Value.Should() + .Be(_vehiclesInfoResponseRu[i].Name); + targetVehicleDictionary[i].Name.First(n => n.Language == RequestLanguage.De).Value.Should() + .Be(_vehiclesInfoResponseDe[i].Name); + targetVehicleDictionary[i].Description.First(n => n.Language == RequestLanguage.En).Value.Should() + .Be(_vehiclesInfoResponseEn[i].Description); + targetVehicleDictionary[i].Description.First(n => n.Language == RequestLanguage.Ru).Value.Should() + .Be(_vehiclesInfoResponseRu[i].Description); + targetVehicleDictionary[i].Description.First(n => n.Language == RequestLanguage.De).Value.Should() + .Be(_vehiclesInfoResponseDe[i].Description); + } + + } + + + #region Fixtures + + private static List GetVehiclesInfoResponse(string language, List baseEncyclopedia = null) + { + var result = new List(); + var faker = new Faker(language); + + if (baseEncyclopedia == null) + { + var random = faker.Random; + for (int i = 0; i < random.Number(400); i++) + { + result.Add(FakeNewDictionaryItem(faker)); + } + } + else + { + foreach (var achievementsResponse in baseEncyclopedia) + { + result.Add(FakeDictionaryItem(faker, achievementsResponse)); + } + } + + return result; + } + + private static WotEncyclopediaVehiclesResponse FakeNewDictionaryItem(Faker faker) + { + return new WotEncyclopediaVehiclesResponse + { + Name = faker.Commerce.ProductName(), + TankId = faker.Random.Number(9999), + Description = faker.Lorem.Sentence(), + }; + } + + private static WotEncyclopediaVehiclesResponse FakeDictionaryItem(Faker faker, WotEncyclopediaVehiclesResponse baseResponse) + { + return new WotEncyclopediaVehiclesResponse + { + Name = faker.Commerce.ProductName(), + Description = faker.Lorem.Sentence(), + TankId = baseResponse.TankId + }; + } + + #endregion + + } +} \ No newline at end of file diff --git a/WotBlitzStatisticsPro.WgApiClient/IWargamingDictionariesApiClient.cs b/WotBlitzStatisticsPro.WgApiClient/IWargamingDictionariesApiClient.cs index af3260a..42a953b 100644 --- a/WotBlitzStatisticsPro.WgApiClient/IWargamingDictionariesApiClient.cs +++ b/WotBlitzStatisticsPro.WgApiClient/IWargamingDictionariesApiClient.cs @@ -16,5 +16,9 @@ namespace WotBlitzStatisticsPro.WgApiClient Task> GetAchievementsDictionary( RealmType realmType = RealmType.Ru, RequestLanguage language = RequestLanguage.En); + + Task> GetVehicles( + RealmType realmType = RealmType.Ru, + RequestLanguage language = RequestLanguage.En); } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.WgApiClient/Model/WotEncyclopediaVehiclesResponse.cs b/WotBlitzStatisticsPro.WgApiClient/Model/WotEncyclopediaVehiclesResponse.cs new file mode 100644 index 0000000..a1ab26e --- /dev/null +++ b/WotBlitzStatisticsPro.WgApiClient/Model/WotEncyclopediaVehiclesResponse.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace WotBlitzStatisticsPro.WgApiClient.Model +{ + public class WotEncyclopediaVehiclesResponse + { + /// + /// Vehicle description + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Engines id list + /// + [JsonProperty("engines")] + public int[] Engines { get; set; } + + /// + /// Guns id list + /// + [JsonProperty("guns")] + public int[] Guns { get; set; } + + /// + /// Is vehicle premium + /// + [JsonProperty("is_premium")] + public bool IsPremium { get; set; } + + /// + /// Vehicle name + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Vehicle nationId + /// + [JsonProperty("nation")] + public string Nation { get; set; } + + /// + /// List of next vehicles in tree: + /// + /// key - vehicle id, + /// value - amount of XP to open it + /// + [JsonProperty("next_tanks")] + public Dictionary NextTanks { get; set; } + + /// + /// Vehicle cost by pairs + /// with keys: price_credit, price_gold + /// + [JsonProperty("cost")] + public Dictionary Cost { get; set; } + + /// + /// Price in XP for parent vehicle as key + /// + [JsonProperty("prices_xp")] + public Dictionary PricesXp { get; set; } + + /// + /// List of compatible suspensions + /// + [JsonProperty("suspensions")] + public int[] Suspensions { get; set; } + + /// + /// VehicleId + /// + [JsonProperty("tank_id")] + public long? TankId { get; set; } + + /// + /// Vehicle tier + /// + [JsonProperty("tier")] + public long? Tier { get; set; } + + /// + /// List of compatible turrets ids + /// + [JsonProperty("turrets")] + public int[] Turrets { get; set; } + + /// + /// Vehicle type id + /// + [JsonProperty("type")] + public string Type { get; set; } + + ///// + ///// Default vehicle profile + ///// + //[JsonProperty("default_profile")] + //public WotEncyclopediaVehiclesDefaultProfile DefaultProfile { get; set; } + + /// + /// List of vehicle images. + /// Keys: preview, normal + /// + [JsonProperty("images")] + public Dictionary Images { get; set; } + + ///// + ///// Information about investigated modules + ///// + //[JsonProperty("modules_tree")] + //public WotEncyclopediaVehiclesModulesTree ModulesTree { get; set; } + } + + +} \ No newline at end of file diff --git a/WotBlitzStatisticsPro.WgApiClient/WargamingApiClient.cs b/WotBlitzStatisticsPro.WgApiClient/WargamingApiClient.cs index 9c825a6..32c1621 100644 --- a/WotBlitzStatisticsPro.WgApiClient/WargamingApiClient.cs +++ b/WotBlitzStatisticsPro.WgApiClient/WargamingApiClient.cs @@ -76,6 +76,16 @@ namespace WotBlitzStatisticsPro.WgApiClient return response.Values.ToList(); } + public async Task> GetVehicles(RealmType realmType = RealmType.Ru, RequestLanguage language = RequestLanguage.En) + { + var response = await GetFromBlitzApi>( + realmType, + language, + "encyclopedia/vehicles/").ConfigureAwait(false); + + return response.Values.ToList(); + } + #endregion