From 7b6f2fedf1080c0ea96f4975f82700db3e12d783 Mon Sep 17 00:00:00 2001 From: Serghei Cebotari Date: Tue, 14 Jan 2025 05:59:55 +0000 Subject: [PATCH] Add devcontainers --- .devcontainer/devcontainer.json | 35 + .dockerignore | 22 +- .gitignore | 968 +++++++++--------- Dockerfile | 48 +- README.md | 10 +- .../Controllers/ProductsController.cs | 94 +- RhSolutions.SkuParser.Api/Models/Product.cs | 128 +-- .../Models/ProductQuantity.cs | 60 +- RhSolutions.SkuParser.Api/Program.cs | 22 +- .../Properties/launchSettings.json | 58 +- .../RhSolutions.SkuParser.Api.csproj | 28 +- .../Services/CsvParser.cs | 48 +- .../Services/ExcelParser.cs | 152 +-- .../Services/ISkuParser.cs | 14 +- .../appsettings.Development.json | 16 +- RhSolutions.SkuParser.Api/appsettings.json | 18 +- .../ExcelParserTests.cs | 96 +- RhSolutions.SkuParser.Tests/FormFileUtil.cs | 32 +- RhSolutions.SkuParser.Tests/GlobalUsings.cs | 2 +- RhSolutions.SkuParser.Tests/ProductTests.cs | 148 +-- .../RhSolutions.SkuParser.Tests.csproj | 50 +- .../Workbooks/simple.csv | 20 +- RhSolutions.SkuParser.sln | 56 +- 23 files changed, 1080 insertions(+), 1045 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..0589e05 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0", + "customizations": { + "vscode": { + "extensions": [ + "Ironcutter24.cscurlyformatter", + "ms-dotnettools.csdevkit" + ] + } + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5001], + // "portsAttributes": { + // "5001": { + // "protocol": "https" + // } + // } + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "dotnet restore", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.dockerignore b/.dockerignore index 0aed759..8d35703 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,12 @@ -# directories -**/bin/ -**/obj/ -**/out/ - -# files -Dockerfile* -**/*.trx -**/*.md -**/*.ps1 -**/*.cmd +# directories +**/bin/ +**/obj/ +**/out/ + +# files +Dockerfile* +**/*.trx +**/*.md +**/*.ps1 +**/*.cmd **/*.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 104b544..5e57f18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,484 +1,484 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from `dotnet new gitignore` - -# dotenv files -.env - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml -.idea - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# Mac bundle stuff -*.dmg -*.app - -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# Vim temporary swap files -*.swp +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/Dockerfile b/Dockerfile index da6a91c..5070060 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,25 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build -ARG TARGETARCH -WORKDIR /source - -COPY RhSolutions.SkuParser.Api/*.csproj . -RUN dotnet restore -a $TARGETARCH - -COPY RhSolutions.SkuParser.Api/. . -RUN dotnet publish -a $TARGETARCH --no-restore -o /app - -FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine -EXPOSE 8080 - -ENV \ - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \ - LC_ALL=ru_RU.UTF-8 \ - LANG=ru_RU.UTF-8 -RUN apk add --no-cache \ - icu-data-full \ - icu-libs - -WORKDIR /app -COPY --from=build /app . -USER $APP_UID +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build +ARG TARGETARCH +WORKDIR /source + +COPY RhSolutions.SkuParser.Api/*.csproj . +RUN dotnet restore -a $TARGETARCH + +COPY RhSolutions.SkuParser.Api/. . +RUN dotnet publish -a $TARGETARCH --no-restore -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine +EXPOSE 8080 + +ENV \ + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \ + LC_ALL=ru_RU.UTF-8 \ + LANG=ru_RU.UTF-8 +RUN apk add --no-cache \ + icu-data-full \ + icu-libs + +WORKDIR /app +COPY --from=build /app . +USER $APP_UID ENTRYPOINT ["./RhSolutions.SkuParser.Api"] \ No newline at end of file diff --git a/README.md b/README.md index 8a0a1df..a77c23d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# RhSolutions.SkuParser.Api -Сервис для парсинга актикулов РЕХАУ и их количества из файлов `xlsx`, `xlsm` и `csv` - -Доступен через POST-метод `/api/Products/`. Отправлять файлы в `form-data`. - +# RhSolutions.SkuParser.Api +Сервис для парсинга актикулов РЕХАУ и их количества из файлов `xlsx`, `xlsm` и `csv` + +Доступен через POST-метод `/api/Products/`. Отправлять файлы в `form-data`. + Количества одноименных артикулов в одном или нескольких файлах суммируются. \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs index d85b01b..77b277b 100644 --- a/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs +++ b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs @@ -1,48 +1,48 @@ -using Microsoft.AspNetCore.Mvc; -using RhSolutions.SkuParser.Models; -using RhSolutions.SkuParser.Services; - -namespace RhSolutions.SkuParser.Controllers; - -[ApiController] -[Route("/api/[controller]")] -public class ProductsController : ControllerBase -{ - private IServiceProvider _provider; - private Dictionary _result; - public ProductsController(IServiceProvider provider) - { - _provider = provider; - _result = new(); - } - - [HttpPost] - public IActionResult PostFiles() - { - IFormFileCollection files = Request.Form.Files; - try - { - foreach (var file in files) - { - ISkuParser parser = _provider.GetRequiredKeyedService(file.ContentType); - IEnumerable productQuantities = parser.ParseProducts(file); - foreach (ProductQuantity pq in productQuantities) - { - if (_result.ContainsKey(pq.Product)) - { - _result[pq.Product] += pq.Quantity; - } - else - { - _result.Add(pq.Product, pq.Quantity); - } - } - } - } - catch (Exception ex) - { - return BadRequest(error: $"{ex.Message}\n\n{ex.Source}\n{ex.StackTrace}"); - } - return new JsonResult(_result.Select(x => new { Sku = x.Key.ToString(), Quantity = x.Value })); - } +using Microsoft.AspNetCore.Mvc; +using RhSolutions.SkuParser.Models; +using RhSolutions.SkuParser.Services; + +namespace RhSolutions.SkuParser.Controllers; + +[ApiController] +[Route("/api/[controller]")] +public class ProductsController : ControllerBase +{ + private IServiceProvider _provider; + private Dictionary _result; + public ProductsController(IServiceProvider provider) + { + _provider = provider; + _result = new(); + } + + [HttpPost] + public IActionResult PostFiles() + { + IFormFileCollection files = Request.Form.Files; + try + { + foreach (var file in files) + { + ISkuParser parser = _provider.GetRequiredKeyedService(file.ContentType); + IEnumerable productQuantities = parser.ParseProducts(file); + foreach (ProductQuantity pq in productQuantities) + { + if (_result.ContainsKey(pq.Product)) + { + _result[pq.Product] += pq.Quantity; + } + else + { + _result.Add(pq.Product, pq.Quantity); + } + } + } + } + catch (Exception ex) + { + return BadRequest(error: $"{ex.Message}\n\n{ex.Source}\n{ex.StackTrace}"); + } + return new JsonResult(_result.Select(x => new { Sku = x.Key.ToString(), Quantity = x.Value })); + } } \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Models/Product.cs b/RhSolutions.SkuParser.Api/Models/Product.cs index fd9ea45..26a7392 100644 --- a/RhSolutions.SkuParser.Api/Models/Product.cs +++ b/RhSolutions.SkuParser.Api/Models/Product.cs @@ -1,65 +1,65 @@ -using System.Text.RegularExpressions; - -namespace RhSolutions.SkuParser.Models; - -public record Product -{ - /// - /// Артикул РЕХАУ в заданном формате - /// - public required string Sku - { - get => _sku; - set - { - _sku = IsValudSku(value) - ? value - : throw new ArgumentException("$Неверный артикул: {value}"); - } - } - private string _sku = string.Empty; - private const string _parsePattern = @"(?[1\s]|^|\b)(?
\d{6})(?[\s13-])(?\d{3})(\b|$)"; - private const string _validnessPattern = @"^1\d{6}[1|3]\d{3}$"; - - private static bool IsValudSku(string value) - { - return Regex.IsMatch(value.Trim(), _validnessPattern); - } - private static string GetSku(Match match) - { - string lead = match.Groups["Lead"].Value; - string article = match.Groups["Article"].Value; - string delimiter = match.Groups["Delimiter"].Value; - string variant = match.Groups["Variant"].Value; - - if (lead != "1" && delimiter == "-") - { - return $"1{article}1{variant}"; - } - else - { - return $"{lead}{article}{delimiter}{variant}"; - } - } - - /// - /// Проверка строки на наличие в ней артикула РЕХАУ - /// - /// Входная строка для проверки - /// Артикул, если найден. null - если нет - /// Если артикул в строке есть возвращает true, Если нет - false - public static bool TryParse(string value, out Product? product) - { - product = null; - MatchCollection matches = Regex.Matches(value, _parsePattern); - if (matches.Count == 0) - { - return false; - } - string sku = GetSku(matches.First()); - product = new Product() { Sku = sku }; - return true; - } - public override int GetHashCode() => Sku.GetHashCode(); - public override string ToString() => Sku; +using System.Text.RegularExpressions; + +namespace RhSolutions.SkuParser.Models; + +public record Product +{ + /// + /// Артикул РЕХАУ в заданном формате + /// + public required string Sku + { + get => _sku; + set + { + _sku = IsValudSku(value) + ? value + : throw new ArgumentException("$Неверный артикул: {value}"); + } + } + private string _sku = string.Empty; + private const string _parsePattern = @"(?[1\s]|^|\b)(?
\d{6})(?[\s13-])(?\d{3})(\b|$)"; + private const string _validnessPattern = @"^1\d{6}[1|3]\d{3}$"; + + private static bool IsValudSku(string value) + { + return Regex.IsMatch(value.Trim(), _validnessPattern); + } + private static string GetSku(Match match) + { + string lead = match.Groups["Lead"].Value; + string article = match.Groups["Article"].Value; + string delimiter = match.Groups["Delimiter"].Value; + string variant = match.Groups["Variant"].Value; + + if (lead != "1" && delimiter == "-") + { + return $"1{article}1{variant}"; + } + else + { + return $"{lead}{article}{delimiter}{variant}"; + } + } + + /// + /// Проверка строки на наличие в ней артикула РЕХАУ + /// + /// Входная строка для проверки + /// Артикул, если найден. null - если нет + /// Если артикул в строке есть возвращает true, Если нет - false + public static bool TryParse(string value, out Product? product) + { + product = null; + MatchCollection matches = Regex.Matches(value, _parsePattern); + if (matches.Count == 0) + { + return false; + } + string sku = GetSku(matches.First()); + product = new Product() { Sku = sku }; + return true; + } + public override int GetHashCode() => Sku.GetHashCode(); + public override string ToString() => Sku; } \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs index b7b154d..d593f8b 100644 --- a/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs +++ b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs @@ -1,30 +1,30 @@ -using CsvHelper.Configuration.Attributes; - -namespace RhSolutions.SkuParser.Models; - -public class ProductQuantity -{ - [Index(0)] - public required Product Product { get; set; } - [Index(1)] - public required double Quantity { get; set; } - - public override bool Equals(object? obj) - { - if (obj == null || GetType() != obj.GetType()) - { - return false; - } - ProductQuantity other = (ProductQuantity)obj; - return Product == other.Product && - Quantity == other.Quantity; - } - - public override int GetHashCode() - { - HashCode hash = new(); - hash.Add(Product); - hash.Add(Quantity); - return hash.ToHashCode(); - } -} +using CsvHelper.Configuration.Attributes; + +namespace RhSolutions.SkuParser.Models; + +public class ProductQuantity +{ + [Index(0)] + public required Product Product { get; set; } + [Index(1)] + public required double Quantity { get; set; } + + public override bool Equals(object? obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + ProductQuantity other = (ProductQuantity)obj; + return Product == other.Product && + Quantity == other.Quantity; + } + + public override int GetHashCode() + { + HashCode hash = new(); + hash.Add(Product); + hash.Add(Quantity); + return hash.ToHashCode(); + } +} diff --git a/RhSolutions.SkuParser.Api/Program.cs b/RhSolutions.SkuParser.Api/Program.cs index 44c642a..e0acec8 100644 --- a/RhSolutions.SkuParser.Api/Program.cs +++ b/RhSolutions.SkuParser.Api/Program.cs @@ -1,12 +1,12 @@ -using RhSolutions.SkuParser.Services; - -var builder = WebApplication.CreateBuilder(args); -builder.Services - .AddKeyedScoped("text/csv") - .AddKeyedScoped("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - .AddKeyedScoped("application/vnd.ms-excel.sheet.macroenabled.12"); -builder.Services.AddControllers(); - -var app = builder.Build(); -app.MapControllers(); +using RhSolutions.SkuParser.Services; + +var builder = WebApplication.CreateBuilder(args); +builder.Services + .AddKeyedScoped("text/csv") + .AddKeyedScoped("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .AddKeyedScoped("application/vnd.ms-excel.sheet.macroenabled.12"); +builder.Services.AddControllers(); + +var app = builder.Build(); +app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Properties/launchSettings.json b/RhSolutions.SkuParser.Api/Properties/launchSettings.json index 09f4eae..002f6b7 100644 --- a/RhSolutions.SkuParser.Api/Properties/launchSettings.json +++ b/RhSolutions.SkuParser.Api/Properties/launchSettings.json @@ -1,29 +1,29 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:8080", - "sslPort": 44355 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:8080", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8080", + "sslPort": 44355 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:8080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj b/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj index d6e06a1..a6b1c5b 100644 --- a/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj +++ b/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj @@ -1,14 +1,14 @@ - - - - net8.0 - enable - enable - - - - - - - - + + + + net8.0 + enable + enable + + + + + + + + diff --git a/RhSolutions.SkuParser.Api/Services/CsvParser.cs b/RhSolutions.SkuParser.Api/Services/CsvParser.cs index 436a949..2776721 100644 --- a/RhSolutions.SkuParser.Api/Services/CsvParser.cs +++ b/RhSolutions.SkuParser.Api/Services/CsvParser.cs @@ -1,24 +1,24 @@ -using System.Globalization; -using CsvHelper; -using CsvHelper.Configuration; -using RhSolutions.SkuParser.Models; - -namespace RhSolutions.SkuParser.Services; - -/// -/// Парсер артикулов и их количества из файлов *.csv -/// -public class CsvParser : ISkuParser -{ - public IEnumerable ParseProducts(IFormFile file) - { - using StreamReader reader = new(file.OpenReadStream()); - var config = new CsvConfiguration(CultureInfo.GetCultureInfo("ru-RU")) - { - HasHeaderRecord = false, - }; - using CsvReader csvReader = new(reader, config); - - return csvReader.GetRecords().ToList(); - } -} +using System.Globalization; +using CsvHelper; +using CsvHelper.Configuration; +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; + +/// +/// Парсер артикулов и их количества из файлов *.csv +/// +public class CsvParser : ISkuParser +{ + public IEnumerable ParseProducts(IFormFile file) + { + using StreamReader reader = new(file.OpenReadStream()); + var config = new CsvConfiguration(CultureInfo.GetCultureInfo("ru-RU")) + { + HasHeaderRecord = false, + }; + using CsvReader csvReader = new(reader, config); + + return csvReader.GetRecords().ToList(); + } +} diff --git a/RhSolutions.SkuParser.Api/Services/ExcelParser.cs b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs index 27b10bd..fec3885 100644 --- a/RhSolutions.SkuParser.Api/Services/ExcelParser.cs +++ b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs @@ -1,76 +1,76 @@ -using ClosedXML.Excel; -using RhSolutions.SkuParser.Models; - -namespace RhSolutions.SkuParser.Services; - -public class ExcelParser : ISkuParser -{ - public IEnumerable ParseProducts(IFormFile file) - { - using XLWorkbook workbook = new(file.OpenReadStream()); - IXLWorksheet ws = workbook.Worksheet(1); - - var leftTop = ws.FirstCellUsed()?.Address; - var rightBottom = ws.LastCellUsed()?.Address; - if (new object?[] { leftTop, rightBottom }.Any(x => x == null)) - { - throw new ArgumentException($"Таблица пуста: {file.FileName}"); - } - - var lookupRange = ws.Range(leftTop, rightBottom).RangeUsed(); - var columns = lookupRange.Columns(); - - var skuColumnQuantity = columns - .Select(column => new - { - Column = column, - Products = column.CellsUsed() - .Select(cell => !cell.HasFormula && Product.TryParse(cell.Value.ToString(), out Product? p) ? p : null) - }) - .Select(c => new { c.Column, SkuCount = c.Products.Count(p => p != null) }) - .Aggregate((l, r) => l.SkuCount > r.SkuCount ? l : r); - var skuColumn = skuColumnQuantity.SkuCount > 0 ? skuColumnQuantity.Column : null; - - if (skuColumn == null) - { - throw new ArgumentException($"Столбец с артикулом не определен: {file.FileName}"); - } - - var quantityColumn = lookupRange.Columns().Skip(skuColumn.ColumnNumber()) - .Select(column => new - { - Column = column, - IsColumnWithNumbers = column.CellsUsed() - .Count(cell => cell.Value.IsNumber == true) > column.CellsUsed().Count() / 4 - }) - .First(x => x.IsColumnWithNumbers) - .Column; - - if (quantityColumn == null) - { - throw new ArgumentException($"Столбец с количеством не определен: {file.FileName}"); - } - - List result = new(); - var rows = quantityColumn.CellsUsed().Select(x => x.Address.RowNumber); - - foreach (var row in rows) - { - var quantity = quantityColumn.Cell(row).Value; - var sku = skuColumn.Cell(row).Value; - - if (quantity.IsNumber - && Product.TryParse(sku.ToString(), out Product? p)) - { - ProductQuantity pq = new() - { - Product = p!, - Quantity = quantity.GetNumber() - }; - result.Add(pq); - } - } - - return result; - } -} +using ClosedXML.Excel; +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; + +public class ExcelParser : ISkuParser +{ + public IEnumerable ParseProducts(IFormFile file) + { + using XLWorkbook workbook = new(file.OpenReadStream()); + IXLWorksheet ws = workbook.Worksheet(1); + + var leftTop = ws.FirstCellUsed()?.Address; + var rightBottom = ws.LastCellUsed()?.Address; + if (new object?[] { leftTop, rightBottom }.Any(x => x == null)) + { + throw new ArgumentException($"Таблица пуста: {file.FileName}"); + } + + var lookupRange = ws.Range(leftTop, rightBottom).RangeUsed(); + var columns = lookupRange.Columns(); + + var skuColumnQuantity = columns + .Select(column => new + { + Column = column, + Products = column.CellsUsed() + .Select(cell => !cell.HasFormula && Product.TryParse(cell.Value.ToString(), out Product? p) ? p : null) + }) + .Select(c => new { c.Column, SkuCount = c.Products.Count(p => p != null) }) + .Aggregate((l, r) => l.SkuCount > r.SkuCount ? l : r); + var skuColumn = skuColumnQuantity.SkuCount > 0 ? skuColumnQuantity.Column : null; + + if (skuColumn == null) + { + throw new ArgumentException($"Столбец с артикулом не определен: {file.FileName}"); + } + + var quantityColumn = lookupRange.Columns().Skip(skuColumn.ColumnNumber()) + .Select(column => new + { + Column = column, + IsColumnWithNumbers = column.CellsUsed() + .Count(cell => cell.Value.IsNumber == true) > column.CellsUsed().Count() / 4 + }) + .First(x => x.IsColumnWithNumbers) + .Column; + + if (quantityColumn == null) + { + throw new ArgumentException($"Столбец с количеством не определен: {file.FileName}"); + } + + List result = new(); + var rows = quantityColumn.CellsUsed().Select(x => x.Address.RowNumber); + + foreach (var row in rows) + { + var quantity = quantityColumn.Cell(row).Value; + var sku = skuColumn.Cell(row).Value; + + if (quantity.IsNumber + && Product.TryParse(sku.ToString(), out Product? p)) + { + ProductQuantity pq = new() + { + Product = p!, + Quantity = quantity.GetNumber() + }; + result.Add(pq); + } + } + + return result; + } +} diff --git a/RhSolutions.SkuParser.Api/Services/ISkuParser.cs b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs index 98b9d9c..4329135 100644 --- a/RhSolutions.SkuParser.Api/Services/ISkuParser.cs +++ b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs @@ -1,7 +1,7 @@ -using RhSolutions.SkuParser.Models; - -namespace RhSolutions.SkuParser.Services; -public interface ISkuParser -{ - public IEnumerable ParseProducts(IFormFile file); -} +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; +public interface ISkuParser +{ + public IEnumerable ParseProducts(IFormFile file); +} diff --git a/RhSolutions.SkuParser.Api/appsettings.Development.json b/RhSolutions.SkuParser.Api/appsettings.Development.json index 0c208ae..ff66ba6 100644 --- a/RhSolutions.SkuParser.Api/appsettings.Development.json +++ b/RhSolutions.SkuParser.Api/appsettings.Development.json @@ -1,8 +1,8 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RhSolutions.SkuParser.Api/appsettings.json b/RhSolutions.SkuParser.Api/appsettings.json index 10f68b8..4d56694 100644 --- a/RhSolutions.SkuParser.Api/appsettings.json +++ b/RhSolutions.SkuParser.Api/appsettings.json @@ -1,9 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/RhSolutions.SkuParser.Tests/ExcelParserTests.cs b/RhSolutions.SkuParser.Tests/ExcelParserTests.cs index 83e95c1..60e1e7b 100644 --- a/RhSolutions.SkuParser.Tests/ExcelParserTests.cs +++ b/RhSolutions.SkuParser.Tests/ExcelParserTests.cs @@ -1,48 +1,48 @@ -using RhSolutions.SkuParser.Services; - -namespace RhSolutions.SkuParser.Tests; - -public class ExcelParserTests -{ - private static readonly List _expected = new() - { - new ProductQuantity() {Product= new Product() {Sku = "11303703100"}, Quantity = 2129.5}, - new ProductQuantity() {Product= new Product() {Sku = "11303803100"}, Quantity = 503}, - new ProductQuantity() {Product= new Product() {Sku = "11303903050"}, Quantity = 52}, - new ProductQuantity() {Product= new Product() {Sku = "11080011001"}, Quantity = 2154}, - new ProductQuantity() {Product= new Product() {Sku = "11080021001"}, Quantity = 134}, - new ProductQuantity() {Product= new Product() {Sku = "11080031001"}, Quantity = 6}, - new ProductQuantity() {Product= new Product() {Sku = "11080311001"}, Quantity = 462}, - new ProductQuantity() {Product= new Product() {Sku = "11080611001"}, Quantity = 38}, - new ProductQuantity() {Product= new Product() {Sku = "11080811001"}, Quantity = 24}, - new ProductQuantity() {Product= new Product() {Sku = "11080831001"}, Quantity = 2}, - }; - - [TestCase("simple.xlsx")] - [TestCase("simpleWithNames.xlsx")] - [TestCase("withHeader.xlsx")] - [TestCase("withHeaderAndGarbage.xlsx")] - [TestCase("twoTables.xlsx")] - [TestCase("rhSolutionsBsTable.xlsx")] - [TestCase("simpleWithFormulas.xlsx")] - public void XlsxTests(string filename) - { - var mockFile = FormFileUtil.GetMockFormFile(filename); - var parser = new ExcelParser(); - var actual = parser.ParseProducts(mockFile.Object); - Assert.That(actual.Count, Is.EqualTo(_expected.Count())); - CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(ProductQuantity)); - CollectionAssert.AreEqual(_expected, actual); - } - - [TestCase("simple.csv")] - public void CsvTests(string filename) - { - var mockFile = FormFileUtil.GetMockFormFile(filename); - var parser = new CsvParser(); - var actual = parser.ParseProducts(mockFile.Object); - Assert.That(actual.Count, Is.EqualTo(_expected.Count())); - CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(ProductQuantity)); - CollectionAssert.AreEqual(_expected, actual); - } -} +using RhSolutions.SkuParser.Services; + +namespace RhSolutions.SkuParser.Tests; + +public class ExcelParserTests +{ + private static readonly List _expected = new() + { + new ProductQuantity() {Product= new Product() {Sku = "11303703100"}, Quantity = 2129.5}, + new ProductQuantity() {Product= new Product() {Sku = "11303803100"}, Quantity = 503}, + new ProductQuantity() {Product= new Product() {Sku = "11303903050"}, Quantity = 52}, + new ProductQuantity() {Product= new Product() {Sku = "11080011001"}, Quantity = 2154}, + new ProductQuantity() {Product= new Product() {Sku = "11080021001"}, Quantity = 134}, + new ProductQuantity() {Product= new Product() {Sku = "11080031001"}, Quantity = 6}, + new ProductQuantity() {Product= new Product() {Sku = "11080311001"}, Quantity = 462}, + new ProductQuantity() {Product= new Product() {Sku = "11080611001"}, Quantity = 38}, + new ProductQuantity() {Product= new Product() {Sku = "11080811001"}, Quantity = 24}, + new ProductQuantity() {Product= new Product() {Sku = "11080831001"}, Quantity = 2}, + }; + + [TestCase("simple.xlsx")] + [TestCase("simpleWithNames.xlsx")] + [TestCase("withHeader.xlsx")] + [TestCase("withHeaderAndGarbage.xlsx")] + [TestCase("twoTables.xlsx")] + [TestCase("rhSolutionsBsTable.xlsx")] + [TestCase("simpleWithFormulas.xlsx")] + public void XlsxTests(string filename) + { + var mockFile = FormFileUtil.GetMockFormFile(filename); + var parser = new ExcelParser(); + var actual = parser.ParseProducts(mockFile.Object); + Assert.That(actual.Count, Is.EqualTo(_expected.Count())); + CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(ProductQuantity)); + CollectionAssert.AreEqual(_expected, actual); + } + + [TestCase("simple.csv")] + public void CsvTests(string filename) + { + var mockFile = FormFileUtil.GetMockFormFile(filename); + var parser = new CsvParser(); + var actual = parser.ParseProducts(mockFile.Object); + Assert.That(actual.Count, Is.EqualTo(_expected.Count())); + CollectionAssert.AllItemsAreInstancesOfType(actual, typeof(ProductQuantity)); + CollectionAssert.AreEqual(_expected, actual); + } +} diff --git a/RhSolutions.SkuParser.Tests/FormFileUtil.cs b/RhSolutions.SkuParser.Tests/FormFileUtil.cs index aaee7ca..f100989 100644 --- a/RhSolutions.SkuParser.Tests/FormFileUtil.cs +++ b/RhSolutions.SkuParser.Tests/FormFileUtil.cs @@ -1,17 +1,17 @@ -using Microsoft.AspNetCore.Http; -using Moq; - -namespace RhSolutions.SkuParser.Tests; - -public static class FormFileUtil -{ - public static Mock GetMockFormFile(string workbookName) - { - string filepath = "./../../../Workbooks/" + workbookName; - var mockFile = new Mock(); - var memoryStream = new MemoryStream([.. File.ReadAllBytes(filepath)]); - mockFile.Setup(x => x.OpenReadStream()) - .Returns(memoryStream); - return mockFile; - } +using Microsoft.AspNetCore.Http; +using Moq; + +namespace RhSolutions.SkuParser.Tests; + +public static class FormFileUtil +{ + public static Mock GetMockFormFile(string workbookName) + { + string filepath = "./../../../Workbooks/" + workbookName; + var mockFile = new Mock(); + var memoryStream = new MemoryStream([.. File.ReadAllBytes(filepath)]); + mockFile.Setup(x => x.OpenReadStream()) + .Returns(memoryStream); + return mockFile; + } } \ No newline at end of file diff --git a/RhSolutions.SkuParser.Tests/GlobalUsings.cs b/RhSolutions.SkuParser.Tests/GlobalUsings.cs index 139a90f..f1d70cc 100644 --- a/RhSolutions.SkuParser.Tests/GlobalUsings.cs +++ b/RhSolutions.SkuParser.Tests/GlobalUsings.cs @@ -1,2 +1,2 @@ -global using NUnit.Framework; +global using NUnit.Framework; global using RhSolutions.SkuParser.Models; \ No newline at end of file diff --git a/RhSolutions.SkuParser.Tests/ProductTests.cs b/RhSolutions.SkuParser.Tests/ProductTests.cs index 12f0944..ee7396d 100644 --- a/RhSolutions.SkuParser.Tests/ProductTests.cs +++ b/RhSolutions.SkuParser.Tests/ProductTests.cs @@ -1,75 +1,75 @@ -namespace RhSolutions.SkuParser.Tests; - -public class ProductTests -{ - [TestCase("12222221001")] - [TestCase("12222223001")] - [TestCase("160001-001")] - public void SimpleParse(string value) - { - Assert.True(Product.TryParse(value, out _)); - } - - [TestCase("string 12222221001")] - [TestCase("12222223001 string")] - [TestCase("string 160001-001")] - [TestCase("160001-001 string ")] - [TestCase("11096641001 Трубка РЕХАУ из. нерж. стали для подкл. радиатора, Г-образная 16/250")] - public void AdvancedParse(string value) - { - Assert.True(Product.TryParse(value, out _)); - } - - [TestCase("11600011001")] - [TestCase("160001-001")] - public void ProductIsCorrect(string value) - { - if (Product.TryParse(value, out Product? product)) - { - Assert.That(product!.Sku, Is.EqualTo("11600011001")); - } - else - { - Assert.Fail($"Parsing failed on {value}"); - } - } - - [TestCase("1222222001")] - [TestCase("12222225001")] - public void NotParses(string value) - { - Assert.False(Product.TryParse(value, out _)); - } - - [Test] - public void ProductEquality() - { - string value = "12222223001"; - Product.TryParse(value, out Product? first); - Product.TryParse(value, out Product? second); - if (first == null || second == null) - { - Assert.Fail($"Parsing failed on {value}"); - } - else - { - Assert.True(first.Equals(second)); - } - } - - [Test] - public void HashTest() - { - string value = "12222223001"; - HashSet set = new(); - if (Product.TryParse(value, out var product)) - { - set.Add(product!); - } - else - { - Assert.Fail($"Parsing failed on {value}"); - } - Assert.True(set.Contains(product!)); - } +namespace RhSolutions.SkuParser.Tests; + +public class ProductTests +{ + [TestCase("12222221001")] + [TestCase("12222223001")] + [TestCase("160001-001")] + public void SimpleParse(string value) + { + Assert.True(Product.TryParse(value, out _)); + } + + [TestCase("string 12222221001")] + [TestCase("12222223001 string")] + [TestCase("string 160001-001")] + [TestCase("160001-001 string ")] + [TestCase("11096641001 Трубка РЕХАУ из. нерж. стали для подкл. радиатора, Г-образная 16/250")] + public void AdvancedParse(string value) + { + Assert.True(Product.TryParse(value, out _)); + } + + [TestCase("11600011001")] + [TestCase("160001-001")] + public void ProductIsCorrect(string value) + { + if (Product.TryParse(value, out Product? product)) + { + Assert.That(product!.Sku, Is.EqualTo("11600011001")); + } + else + { + Assert.Fail($"Parsing failed on {value}"); + } + } + + [TestCase("1222222001")] + [TestCase("12222225001")] + public void NotParses(string value) + { + Assert.False(Product.TryParse(value, out _)); + } + + [Test] + public void ProductEquality() + { + string value = "12222223001"; + Product.TryParse(value, out Product? first); + Product.TryParse(value, out Product? second); + if (first == null || second == null) + { + Assert.Fail($"Parsing failed on {value}"); + } + else + { + Assert.True(first.Equals(second)); + } + } + + [Test] + public void HashTest() + { + string value = "12222223001"; + HashSet set = new(); + if (Product.TryParse(value, out var product)) + { + set.Add(product!); + } + else + { + Assert.Fail($"Parsing failed on {value}"); + } + Assert.True(set.Contains(product!)); + } } \ No newline at end of file diff --git a/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj b/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj index 069fa02..ba8a0cb 100644 --- a/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj +++ b/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj @@ -1,25 +1,25 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - - + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/RhSolutions.SkuParser.Tests/Workbooks/simple.csv b/RhSolutions.SkuParser.Tests/Workbooks/simple.csv index 51d2c85..948bcf9 100644 --- a/RhSolutions.SkuParser.Tests/Workbooks/simple.csv +++ b/RhSolutions.SkuParser.Tests/Workbooks/simple.csv @@ -1,10 +1,10 @@ -11303703100;2129,5 -11303803100;503 -11303903050;52 -11080011001;2154 -11080021001;134 -11080031001;6 -11080311001;462 -11080611001;38 -11080811001;24 -11080831001;2 +11303703100;2129,5 +11303803100;503 +11303903050;52 +11080011001;2154 +11080021001;134 +11080031001;6 +11080311001;462 +11080611001;38 +11080811001;24 +11080831001;2 diff --git a/RhSolutions.SkuParser.sln b/RhSolutions.SkuParser.sln index 05f155d..f1ff419 100644 --- a/RhSolutions.SkuParser.sln +++ b/RhSolutions.SkuParser.sln @@ -1,28 +1,28 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Api", "RhSolutions.SkuParser.Api\RhSolutions.SkuParser.Api.csproj", "{5178E712-F984-48F4-9C68-6DC8CCAA4053}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Tests", "RhSolutions.SkuParser.Tests\RhSolutions.SkuParser.Tests.csproj", "{8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.Build.0 = Release|Any CPU - {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Api", "RhSolutions.SkuParser.Api\RhSolutions.SkuParser.Api.csproj", "{5178E712-F984-48F4-9C68-6DC8CCAA4053}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Tests", "RhSolutions.SkuParser.Tests\RhSolutions.SkuParser.Tests.csproj", "{8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.Build.0 = Release|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal