├── packaging ├── linux │ └── CMakeLists.txt ├── windows │ └── CMakeLists.txt ├── macos │ ├── icons │ │ └── getit.icns │ ├── package.sh │ └── CMakeLists.txt └── CMakeLists.txt ├── tests ├── res │ └── test_file.txt ├── main.cpp ├── service │ ├── factories │ │ └── tst_RequestServiceFactory.cpp │ └── implementations │ │ └── tst_CppRestRequestService.cpp ├── data │ ├── factories │ │ └── tst_RequestRepositoryFactory.cpp │ └── repositories │ │ ├── tst_RawRequestRepository.cpp │ │ ├── tst_RequestRepository.cpp │ │ └── tst_FormDataRequestRepository.cpp ├── presentation │ ├── fragments │ │ ├── tst_HeadersFragmentController.cpp │ │ ├── tst_BodyFragmentController.cpp │ │ └── tst_ResponseFragmentController.cpp │ └── highlighters │ │ └── tst_JsonSyntaxHighlighterRule.cpp ├── domain │ ├── implementations │ │ ├── tst_RawRequestBody.cpp │ │ └── tst_FormDataRequestBody.cpp │ └── factories │ │ └── tst_RequestFactory.cpp └── CMakeLists.txt ├── docs ├── uml │ ├── class_diagram.png │ └── class_diagram.puml └── design.md ├── resources ├── images │ └── getit.png ├── resources.qrc └── icons │ └── main.svg ├── src ├── presentation │ ├── fragments │ │ ├── BodyFragment │ │ │ ├── BodyType.hpp │ │ │ ├── BodyFragmentController.hpp │ │ │ ├── IBodyFragmentView.hpp │ │ │ ├── BodyFragmentController.cpp │ │ │ ├── BodyFragmentView.hpp │ │ │ └── BodyFragmentView.cpp │ │ ├── ResponseFragment │ │ │ ├── ContentType.hpp │ │ │ ├── IResponseFragmentView.hpp │ │ │ ├── ResponseFragmentController.hpp │ │ │ ├── ResponseFragmentController.cpp │ │ │ ├── ResponseFragmentView.hpp │ │ │ ├── ResponseFragmentView.cpp │ │ │ └── ResponseFragmentView.ui │ │ ├── FragmentController.hpp │ │ └── HeadersFragment │ │ │ ├── IHeadersFragmentView.hpp │ │ │ ├── HeadersFragmentController.cpp │ │ │ ├── HeadersFragmentController.hpp │ │ │ ├── HeadersFragmentView.hpp │ │ │ ├── HeadersFragmentView.cpp │ │ │ └── HeadersFragmentView.ui │ ├── states │ │ ├── RequestState.hpp │ │ ├── Sending.hpp │ │ ├── Error.hpp │ │ ├── Sent.hpp │ │ └── FileOpened.hpp │ ├── highlighters │ │ ├── SyntaxHighlighterRule.hpp │ │ ├── JsonSyntaxHighlighterRule.hpp │ │ ├── SyntaxHighlighter.hpp │ │ ├── SyntaxHighlighter.cpp │ │ └── XmlSyntaxHighlighterRule.hpp │ └── windows │ │ ├── IMainWindow.hpp │ │ ├── IMainWindowViewModel.hpp │ │ ├── MainWindowViewModel.hpp │ │ ├── MainWindowViewModel.cpp │ │ ├── MainWindow.hpp │ │ ├── MainWindow.cpp │ │ └── MainWindow.ui ├── domain │ ├── models │ │ ├── Response.hpp │ │ ├── RequestBody.hpp │ │ ├── Request.hpp │ │ └── Request.cpp │ ├── implementations │ │ ├── RawRequestBody.cpp │ │ ├── RawRequestBody.hpp │ │ ├── FormDataRequestBody.hpp │ │ └── FormDataRequestBody.cpp │ ├── contracts │ │ └── RequestFactory.hpp │ └── factories │ │ ├── RequestFactory.hpp │ │ └── RequestFactory.cpp ├── service │ ├── factories │ │ ├── RequestServiceFactory.cpp │ │ └── RequestServiceFactory.hpp │ ├── contracts │ │ ├── RequestServiceFactory.hpp │ │ └── RequestService.hpp │ └── implementations │ │ ├── CppRestRequestService.hpp │ │ └── CppRestRequestService.cpp ├── data │ ├── contracts │ │ ├── RequestRepositoryFactory.hpp │ │ └── RequestRepository.hpp │ ├── exceptions │ │ └── NoAvailableRepositoryException.hpp │ ├── factories │ │ ├── RequestRepositoryFactory.cpp │ │ └── RequestRepositoryFactory.hpp │ └── repositories │ │ ├── RawRequestRepository.hpp │ │ ├── RequestRepository.hpp │ │ ├── FormDataRequestRepository.hpp │ │ ├── RequestRepository.cpp │ │ ├── RawRequestRepository.cpp │ │ └── FormDataRequestRepository.cpp ├── main.cpp └── CMakeLists.txt ├── .gitignore ├── .github ├── workflows │ ├── create_release.yml │ ├── codeql-analysis.yml │ ├── build_and_test.yml │ ├── macos_bundle.yml │ ├── linux_flatpak.yml │ ├── windows_executable.yml │ └── package_tst.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── CMakeLists.txt ├── LICENSE ├── third_party └── CMakeLists.txt └── readme.md /packaging/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packaging/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/res/test_file.txt: -------------------------------------------------------------------------------- 1 | content -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include -------------------------------------------------------------------------------- /docs/uml/class_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkessels/GetIt/HEAD/docs/uml/class_diagram.png -------------------------------------------------------------------------------- /resources/images/getit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkessels/GetIt/HEAD/resources/images/getit.png -------------------------------------------------------------------------------- /packaging/macos/icons/getit.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkessels/GetIt/HEAD/packaging/macos/icons/getit.icns -------------------------------------------------------------------------------- /resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/main.svg 4 | 5 | -------------------------------------------------------------------------------- /src/presentation/fragments/BodyFragment/BodyType.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace getit::presentation::fragments 4 | { 5 | enum BodyType 6 | { 7 | FORM_DATA, 8 | RAW 9 | }; 10 | } -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/ContentType.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace getit::presentation::fragments 4 | { 5 | enum ContentType 6 | { 7 | PLAIN, 8 | JSON, 9 | XML 10 | }; 11 | } -------------------------------------------------------------------------------- /src/presentation/states/RequestState.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace getit::presentation::states 4 | { 5 | struct RequestState 6 | { 7 | public: 8 | virtual ~RequestState() = default; 9 | }; 10 | } -------------------------------------------------------------------------------- /src/presentation/states/Sending.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "presentation/states/RequestState.hpp" 4 | 5 | namespace getit::presentation::states 6 | { 7 | struct Sending: public RequestState 8 | { 9 | public: 10 | ~Sending() override = default; 11 | }; 12 | } -------------------------------------------------------------------------------- /packaging/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}) 4 | 5 | IF (APPLE) 6 | add_subdirectory(macos) 7 | ENDIF() 8 | 9 | IF (WINDOWS) 10 | add_subdirectory(windows) 11 | ENDIF() 12 | 13 | IF (LINUX) 14 | add_subdirectory(linux) 15 | ENDIF() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _deps/ 2 | bin/ 3 | build/ 4 | CMakeFiles/ 5 | lib/ 6 | Testing/ 7 | *_autogen/ 8 | .idea/ 9 | .vscode/ 10 | cmake-build-debug/ 11 | CMakeScripts/ 12 | *.xcodeproj 13 | *.build 14 | .ninja* 15 | *.ninja 16 | *.zip 17 | 18 | .DS_Store 19 | *.cbp 20 | *.cmake 21 | *.tcl 22 | *.user 23 | MakeFile 24 | *Cache* 25 | getit 26 | -------------------------------------------------------------------------------- /src/domain/models/Response.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace getit::domain::models 7 | { 8 | struct Response 9 | { 10 | public: 11 | ~Response() = default; 12 | 13 | std::map headers; 14 | std::string body; 15 | }; 16 | } -------------------------------------------------------------------------------- /src/service/factories/RequestServiceFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "service/factories/RequestServiceFactory.hpp" 2 | 3 | using namespace getit::service::factories; 4 | 5 | std::shared_ptr RequestServiceFactory::getRequestService() 6 | { 7 | return std::make_shared(); 8 | } -------------------------------------------------------------------------------- /src/domain/models/RequestBody.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace getit::domain::models 6 | { 7 | struct RequestBody 8 | { 9 | public: 10 | virtual ~RequestBody() = default; 11 | 12 | virtual std::string getBody() = 0; 13 | virtual std::string getContentType() = 0; 14 | }; 15 | } -------------------------------------------------------------------------------- /src/presentation/fragments/FragmentController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace getit::presentation::fragments 6 | { 7 | template 8 | struct FragmentController 9 | { 10 | public: 11 | virtual ~FragmentController() = default; 12 | 13 | virtual M getContent() = 0; 14 | virtual void setContent(M content) = 0; 15 | }; 16 | } -------------------------------------------------------------------------------- /src/service/contracts/RequestServiceFactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "service/contracts/RequestService.hpp" 6 | 7 | namespace getit::service::contracts 8 | { 9 | struct RequestServiceFactory 10 | { 11 | public: 12 | virtual ~RequestServiceFactory() = default; 13 | 14 | virtual std::shared_ptr getRequestService() = 0; 15 | }; 16 | } -------------------------------------------------------------------------------- /src/presentation/states/Error.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "presentation/states/RequestState.hpp" 6 | 7 | namespace getit::presentation::states 8 | { 9 | struct Error: public RequestState 10 | { 11 | public: 12 | explicit Error(std::string message): message(std::move(message)) {} 13 | ~Error() override = default; 14 | 15 | std::string message; 16 | }; 17 | } -------------------------------------------------------------------------------- /src/presentation/highlighters/SyntaxHighlighterRule.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace getit::presentation::highlighters 7 | { 8 | struct SyntaxHighlighterRule 9 | { 10 | public: 11 | std::string regex; 12 | int color; 13 | 14 | SyntaxHighlighterRule(std::string regex, int color): 15 | regex(std::move(regex)), color(color) {} 16 | }; 17 | } -------------------------------------------------------------------------------- /src/data/contracts/RequestRepositoryFactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "data/contracts/RequestRepository.hpp" 6 | #include "domain/models/Request.hpp" 7 | 8 | namespace getit::data::contracts 9 | { 10 | struct RequestRepositoryFactory 11 | { 12 | public: 13 | virtual ~RequestRepositoryFactory() = default; 14 | 15 | virtual std::shared_ptr getRepository() = 0; 16 | }; 17 | } -------------------------------------------------------------------------------- /packaging/macos/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/env zsh 2 | 3 | # Compile GetIt 4 | echo 'Compiling GetIt...' 5 | # Disable warnings as error 6 | cmake . -Dpackaging=true -G Ninja -D CMAKE_CXX_FLAGS="-w" 7 | ninja GetIt 8 | 9 | # Link Qt 10 | echo 'Linking Qt to the app bundle' 11 | macdeployqt ./bin/GetIt.app -timestamp -libpath=./_deps 12 | 13 | # Copy icon file 14 | cp ./packaging/macos/icons/getit.icns ./bin/GetIt.app/Contents/Resources 15 | 16 | # Zip file 17 | zip -rT getit.zip ./bin/GetIt.app >> /dev/null -------------------------------------------------------------------------------- /src/presentation/fragments/HeadersFragment/IHeadersFragmentView.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace getit::presentation::fragments 7 | { 8 | struct IHeadersFragmentView 9 | { 10 | public: 11 | virtual ~IHeadersFragmentView() = default; 12 | 13 | virtual std::map getHeaders() = 0; 14 | virtual void setHeaders(const std::map& headers) = 0; 15 | }; 16 | } -------------------------------------------------------------------------------- /src/data/exceptions/NoAvailableRepositoryException.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace getit::data::exceptions 6 | { 7 | struct NoAvailableRepositoryException: public std::exception 8 | { 9 | public: 10 | [[nodiscard]] const char* what() const noexcept override 11 | { 12 | return message; 13 | } 14 | 15 | inline static const char* message = "Cannot find a repository for the specified class"; 16 | }; 17 | } -------------------------------------------------------------------------------- /src/service/contracts/RequestService.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "domain/models/Request.hpp" 7 | #include "domain/models/Response.hpp" 8 | 9 | namespace getit::service::contracts 10 | { 11 | struct RequestService 12 | { 13 | public: 14 | virtual ~RequestService() = default; 15 | 16 | virtual std::future> send(std::shared_ptr request) = 0; 17 | }; 18 | } -------------------------------------------------------------------------------- /src/presentation/windows/IMainWindow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "presentation/states/RequestState.hpp" 6 | #include "presentation/windows/IMainWindowViewModel.hpp" 7 | 8 | namespace getit::presentation::windows 9 | { 10 | struct IMainWindow 11 | { 12 | public: 13 | virtual ~IMainWindow() = default; 14 | 15 | virtual void setViewModel(std::shared_ptr viewModel) = 0; 16 | virtual void updateState(std::shared_ptr state) = 0; 17 | }; 18 | } -------------------------------------------------------------------------------- /src/presentation/states/Sent.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "domain/models/Response.hpp" 7 | #include "presentation/states/RequestState.hpp" 8 | 9 | namespace getit::presentation::states 10 | { 11 | struct Sent: public RequestState 12 | { 13 | public: 14 | explicit Sent(std::shared_ptr response): response(std::move(response)) {} 15 | ~Sent() override = default; 16 | 17 | std::shared_ptr response; 18 | }; 19 | } -------------------------------------------------------------------------------- /.github/workflows/create_release.yml: -------------------------------------------------------------------------------- 1 | name: Create new release 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | name: Create new release based on latest tag 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Create release 15 | uses: actions/create-release@v1 16 | id: create_release 17 | with: 18 | draft: false 19 | prerelease: false 20 | release_name: ${{ github.ref }} 21 | tag_name: ${{ github.ref }} 22 | env: 23 | GITHUB_TOKEN: ${{ github.token }} 24 | -------------------------------------------------------------------------------- /src/service/factories/RequestServiceFactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "service/contracts/RequestService.hpp" 6 | #include "service/contracts/RequestServiceFactory.hpp" 7 | #include "service/implementations/CppRestRequestService.hpp" 8 | 9 | namespace getit::service::factories 10 | { 11 | class RequestServiceFactory : public contracts::RequestServiceFactory 12 | { 13 | public: 14 | ~RequestServiceFactory() override = default; 15 | 16 | std::shared_ptr getRequestService() override; 17 | }; 18 | } -------------------------------------------------------------------------------- /src/presentation/states/FileOpened.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "domain/models/Request.hpp" 7 | #include "presentation/states/RequestState.hpp" 8 | 9 | namespace getit::presentation::states 10 | { 11 | struct FileOpened: public RequestState 12 | { 13 | public: 14 | explicit FileOpened(std::shared_ptr request): 15 | request(std::move(request)) {} 16 | ~FileOpened() override = default; 17 | 18 | std::shared_ptr request; 19 | }; 20 | } -------------------------------------------------------------------------------- /src/service/implementations/CppRestRequestService.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "domain/models/Request.hpp" 8 | #include "domain/models/Response.hpp" 9 | #include "service/contracts/RequestService.hpp" 10 | 11 | namespace getit::service::implementations 12 | { 13 | class CppRestRequestService: public contracts::RequestService 14 | { 15 | public: 16 | ~CppRestRequestService() override = default; 17 | 18 | std::future> send(std::shared_ptr request) override; 19 | }; 20 | } -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/IResponseFragmentView.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace getit::presentation::fragments 7 | { 8 | struct IResponseFragmentView 9 | { 10 | public: 11 | virtual ~IResponseFragmentView() = default; 12 | 13 | virtual std::map getHeaders() = 0; 14 | virtual std::string getBody() = 0; 15 | 16 | virtual void setHeaders(const std::map& headers) = 0; 17 | virtual void setBody(const std::string& body) = 0; 18 | 19 | virtual void clearHeaders() = 0; 20 | }; 21 | } -------------------------------------------------------------------------------- /src/presentation/fragments/HeadersFragment/HeadersFragmentController.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/fragments/HeadersFragment/HeadersFragmentController.hpp" 2 | 3 | using namespace getit::presentation::fragments; 4 | 5 | HeadersFragmentController::HeadersFragmentController(IHeadersFragmentView* view): 6 | view(view) 7 | { 8 | 9 | } 10 | 11 | HeadersFragmentController::~HeadersFragmentController() 12 | { 13 | delete view; 14 | } 15 | 16 | std::map HeadersFragmentController::getContent() 17 | { 18 | return view->getHeaders(); 19 | } 20 | 21 | void HeadersFragmentController::setContent(std::map content) 22 | { 23 | view->setHeaders(content); 24 | } -------------------------------------------------------------------------------- /tests/service/factories/tst_RequestServiceFactory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "service/factories/RequestServiceFactory.hpp" 5 | #include "service/implementations/CppRestRequestService.hpp" 6 | 7 | using namespace getit::service::factories; 8 | 9 | TEST_CASE("RequestServiceFactory.getRequestService") 10 | { 11 | const auto& factory = std::make_shared(); 12 | 13 | SECTION("Returns an instance of CppRestRequestService") 14 | { 15 | // Act 16 | const auto& result = factory->getRequestService(); 17 | 18 | // Assert 19 | REQUIRE(std::dynamic_pointer_cast(result)); 20 | } 21 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | project(GetIt 4 | VERSION 5.0.0 5 | DESCRIPTION "Open source REST client application" 6 | HOMEPAGE_URL "https://getit.bartkessels.net" 7 | ) 8 | 9 | # General project setup 10 | set(CMAKE_CXX_STANDARD 17) 11 | set(CMAKE_XX_STANDARD 17) 12 | set(CMAKE_XX_STANDARD_REQUIRED ON) 13 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 14 | set(CMAKE_EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/build) 15 | 16 | # Subdirectories 17 | add_subdirectory(third_party) 18 | add_subdirectory(src) 19 | add_subdirectory(tests) 20 | 21 | # Only build packages when the -Dpackaging=true flag is set 22 | IF (packaging) 23 | add_subdirectory(packaging) 24 | ENDIF() 25 | -------------------------------------------------------------------------------- /src/data/factories/RequestRepositoryFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "data/factories/RequestRepositoryFactory.hpp" 2 | 3 | using namespace getit::data::factories; 4 | 5 | RequestRepositoryFactory::RequestRepositoryFactory(std::shared_ptr factory): 6 | factory(std::move(factory)) 7 | { 8 | 9 | } 10 | 11 | std::shared_ptr RequestRepositoryFactory::getRepository() 12 | { 13 | const auto& formDataRequestRepository = std::make_shared(factory); 14 | const auto& rawRequestRepository = std::make_shared(factory); 15 | 16 | return std::make_shared(formDataRequestRepository, rawRequestRepository); 17 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Short description** 11 | A clear and concise description of what you'd like to see added to GetIt. 12 | 13 | I.e.: Automated response validation 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | I.e.: I'd like to have a screen in GetIt where I can specify the expected request body and some header. When the request is sent, I'd like GetIt to validate my expected body and headers against the actual response of the request. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /src/domain/implementations/RawRequestBody.cpp: -------------------------------------------------------------------------------- 1 | #include "domain/implementations/RawRequestBody.hpp" 2 | 3 | using namespace getit::domain::implementations; 4 | 5 | RawRequestBody::RawRequestBody(std::string contentType): 6 | contentType(std::move(contentType)) 7 | { 8 | 9 | } 10 | 11 | RawRequestBody::RawRequestBody(): 12 | RawRequestBody(DEFAULT_CONTENT_TYPE) 13 | { 14 | 15 | } 16 | 17 | void RawRequestBody::setContentType(std::string contentType) 18 | { 19 | this->contentType = contentType; 20 | } 21 | 22 | void RawRequestBody::setBody(std::string body) 23 | { 24 | this->body = body; 25 | } 26 | 27 | std::string RawRequestBody::getBody() 28 | { 29 | return this->body; 30 | } 31 | 32 | std::string RawRequestBody::getContentType() 33 | { 34 | return this->contentType; 35 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Help us improve GetIt 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. MacOS, Windows, Linux] 28 | - Platform: [e.g. x64, ARM] 29 | - Version [e.g. v5.0.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /src/presentation/fragments/HeadersFragment/HeadersFragmentController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "presentation/fragments/HeadersFragment/IHeadersFragmentView.hpp" 8 | #include "presentation/fragments/FragmentController.hpp" 9 | 10 | namespace getit::presentation::fragments 11 | { 12 | class HeadersFragmentController : public FragmentController> 13 | { 14 | public: 15 | explicit HeadersFragmentController(IHeadersFragmentView* view); 16 | ~HeadersFragmentController() override; 17 | 18 | std::map getContent() override; 19 | void setContent(std::map content) override; 20 | 21 | private: 22 | IHeadersFragmentView* view; 23 | }; 24 | } -------------------------------------------------------------------------------- /src/domain/implementations/RawRequestBody.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "domain/models/RequestBody.hpp" 6 | 7 | namespace getit::domain::implementations 8 | { 9 | class RawRequestBody: public models::RequestBody 10 | { 11 | public: 12 | explicit RawRequestBody(std::string contentType); 13 | RawRequestBody(); 14 | ~RawRequestBody() override = default; 15 | 16 | void setContentType(std::string contentType); 17 | void setBody(std::string body); 18 | 19 | std::string getBody() override; 20 | std::string getContentType() override; 21 | 22 | private: 23 | std::string body; 24 | std::string contentType; 25 | 26 | inline static 27 | const std::string DEFAULT_CONTENT_TYPE = "text/plain"; 28 | }; 29 | } -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/ResponseFragmentController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "domain/models/Response.hpp" 6 | #include "presentation/fragments/ResponseFragment/IResponseFragmentView.hpp" 7 | #include "presentation/fragments/FragmentController.hpp" 8 | 9 | namespace getit::presentation::fragments 10 | { 11 | class ResponseFragmentController : public FragmentController> 12 | { 13 | public: 14 | explicit ResponseFragmentController(IResponseFragmentView* view); 15 | ~ResponseFragmentController() override; 16 | 17 | std::shared_ptr getContent() override; 18 | void setContent(std::shared_ptr content) override; 19 | 20 | private: 21 | IResponseFragmentView* view; 22 | }; 23 | } -------------------------------------------------------------------------------- /src/presentation/highlighters/JsonSyntaxHighlighterRule.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "presentation/highlighters/SyntaxHighlighterRule.hpp" 7 | 8 | namespace getit::presentation::highlighters 9 | { 10 | struct JsonSyntaxHighlighterRule 11 | { 12 | public: 13 | inline static const auto& integerRule = std::make_shared( 14 | "[+-]?([0-9]*[.])?[0-9]+", 15 | 0x0000ff 16 | ); 17 | inline static const auto& stringRule = std::make_shared( 18 | "\"(.*?)\"", 19 | 0x800080 20 | ); 21 | 22 | inline static std::list> rules = { 23 | integerRule, 24 | stringRule 25 | }; 26 | }; 27 | } -------------------------------------------------------------------------------- /src/presentation/windows/IMainWindowViewModel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "domain/models/RequestBody.hpp" 8 | 9 | using namespace getit::domain::models; 10 | 11 | namespace getit::presentation::windows 12 | { 13 | struct IMainWindowViewModel 14 | { 15 | public: 16 | virtual ~IMainWindowViewModel() = default; 17 | 18 | virtual void sendRequest(std::string method, std::string uri, std::map headers, 19 | std::shared_ptr body) = 0; 20 | virtual void saveRequest(std::string method, std::string uri, std::map headers, 21 | std::shared_ptr body, const std::string& location) = 0; 22 | virtual void openRequest(const std::string& location) = 0; 23 | }; 24 | } -------------------------------------------------------------------------------- /packaging/macos/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE TRUE) 4 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME ${CMAKE_PROJECT_NAME}) 5 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_COPYRIGHT "Bart Kessels") 6 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_ICON_FILE getit) 7 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "net.bartkessels.${CMAKE_PROJECT_NAME}") 8 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_INFO_STRING ${CMAKE_PROJECT_DESCRIPTION}) 9 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING ${CMAKE_PROJECT_VERSION}) 10 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING ${CMAKE_PROJECT_VERSION_MAJOR}) 11 | -------------------------------------------------------------------------------- /src/presentation/fragments/BodyFragment/BodyFragmentController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "domain/models/RequestBody.hpp" 6 | #include "presentation/fragments/BodyFragment/BodyType.hpp" 7 | #include "presentation/fragments/BodyFragment/IBodyFragmentView.hpp" 8 | #include "presentation/fragments/FragmentController.hpp" 9 | 10 | namespace getit::presentation::fragments 11 | { 12 | class BodyFragmentController : public FragmentController> 13 | { 14 | public: 15 | explicit BodyFragmentController(IBodyFragmentView* view); 16 | ~BodyFragmentController() override; 17 | 18 | std::shared_ptr getContent() override; 19 | void setContent(std::shared_ptr content) override; 20 | 21 | private: 22 | IBodyFragmentView* view; 23 | }; 24 | } -------------------------------------------------------------------------------- /src/presentation/highlighters/SyntaxHighlighter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "presentation/highlighters/SyntaxHighlighterRule.hpp" 12 | 13 | namespace getit::presentation::highlighters 14 | { 15 | class SyntaxHighlighter: public QSyntaxHighlighter 16 | { 17 | public: 18 | explicit SyntaxHighlighter(QTextDocument* document); 19 | ~SyntaxHighlighter() override = default; 20 | 21 | void startHighlighting(std::list> rules); 22 | void stopHighlighting(); 23 | 24 | protected: 25 | void highlightBlock(const QString& text) override; 26 | 27 | private: 28 | std::list> rules; 29 | }; 30 | } -------------------------------------------------------------------------------- /tests/data/factories/tst_RequestRepositoryFactory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "data/factories/RequestRepositoryFactory.hpp" 6 | #include "data/repositories/RequestRepository.hpp" 7 | #include "domain/factories/RequestFactory.hpp" 8 | #include "domain/implementations/RawRequestBody.hpp" 9 | 10 | using namespace getit::data::factories; 11 | 12 | TEST_CASE("RequestRepositoryFactory.getRepository") 13 | { 14 | const auto& requestFactory = std::make_shared(); 15 | const auto& factory = std::make_shared(requestFactory); 16 | 17 | SECTION("returns an instance of RequestRepository") 18 | { 19 | // Arrange 20 | // Act 21 | const auto& result = factory->getRepository(); 22 | 23 | // Assert 24 | REQUIRE(std::dynamic_pointer_cast(result)); 25 | } 26 | } -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/ResponseFragmentController.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/fragments/ResponseFragment/ResponseFragmentController.hpp" 2 | 3 | using namespace getit; 4 | using namespace getit::presentation::fragments; 5 | 6 | ResponseFragmentController::ResponseFragmentController(IResponseFragmentView* view): 7 | view(view) 8 | { 9 | 10 | }; 11 | 12 | ResponseFragmentController::~ResponseFragmentController() 13 | { 14 | delete view; 15 | } 16 | 17 | std::shared_ptr ResponseFragmentController::getContent() 18 | { 19 | const auto& content = std::make_shared(); 20 | 21 | content->headers = view->getHeaders(); 22 | content->body = view->getBody(); 23 | 24 | return content; 25 | } 26 | 27 | void ResponseFragmentController::setContent(std::shared_ptr content) 28 | { 29 | view->clearHeaders(); 30 | view->setHeaders(content->headers); 31 | view->setBody(content->body); 32 | } -------------------------------------------------------------------------------- /src/data/contracts/RequestRepository.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "domain/models/Request.hpp" 7 | 8 | namespace getit::data::contracts 9 | { 10 | struct RequestRepository 11 | { 12 | public: 13 | virtual ~RequestRepository() = default; 14 | 15 | virtual void saveRequest(const std::string& filePath, std::shared_ptr request) = 0; 16 | virtual std::shared_ptr loadRequest(const std::string& filePath) = 0; 17 | 18 | protected: 19 | const std::string METHOD_NAME = "method"; 20 | const std::string URI_NAME = "uri"; 21 | const std::string HEADERS_NAME = "headers"; 22 | const std::string HEADER_NAME = "header"; 23 | const std::string HEADER_VALUE = "value"; 24 | const std::string FORM_DATA_BODY_TYPE_NAME = "formdata"; 25 | const std::string RAW_BODY_TYPE_NAME = "raw"; 26 | }; 27 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # Checklist: 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] I have added tests that prove my fix is effective or that my feature works 23 | - [ ] New and existing unit tests pass locally with my changes 24 | -------------------------------------------------------------------------------- /src/presentation/fragments/BodyFragment/IBodyFragmentView.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "domain/implementations/FormDataRequestBody.hpp" 7 | #include "domain/implementations/RawRequestBody.hpp" 8 | #include "presentation/fragments/BodyFragment/BodyType.hpp" 9 | 10 | namespace getit::presentation::fragments 11 | { 12 | struct IBodyFragmentView 13 | { 14 | public: 15 | virtual ~IBodyFragmentView() = default; 16 | 17 | virtual std::shared_ptr getRawBody() = 0; 18 | virtual std::shared_ptr getFormDataBody() = 0; 19 | virtual BodyType getBodyType() = 0; 20 | 21 | virtual void setFormDataBody(const std::shared_ptr& body) = 0; 22 | virtual void setRawBody(const std::shared_ptr& body) = 0; 23 | virtual void setBodyType(const BodyType& bodyType) = 0; 24 | }; 25 | } -------------------------------------------------------------------------------- /src/domain/models/Request.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "domain/models/RequestBody.hpp" 8 | 9 | namespace getit::domain::models 10 | { 11 | class Request 12 | { 13 | public: 14 | Request() = default; 15 | ~Request() = default; 16 | 17 | void setMethod(const std::string& method); 18 | void setUri(const std::string& uri); 19 | void addHeader(const std::string& header, const std::string& value); 20 | 21 | void setHeaders(const std::map& headers); 22 | void setBody(std::shared_ptr body); 23 | 24 | std::string getMethod(); 25 | std::string getUri(); 26 | std::map getHeaders(); 27 | std::shared_ptr getBody(); 28 | 29 | private: 30 | std::string method; 31 | std::string uri; 32 | std::map headers; 33 | std::shared_ptr body; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/domain/models/Request.cpp: -------------------------------------------------------------------------------- 1 | #include "domain/models/Request.hpp" 2 | 3 | using namespace getit::domain::models; 4 | 5 | void Request::setMethod(const std::string& method) 6 | { 7 | this->method = method; 8 | } 9 | 10 | void Request::setUri(const std::string& uri) 11 | { 12 | this->uri = uri; 13 | } 14 | 15 | void Request::addHeader(const std::string& header, const std::string& value) 16 | { 17 | this->headers.emplace(header, value); 18 | } 19 | 20 | void Request::setHeaders(const std::map& headers) 21 | { 22 | for (const auto& [header, value] : headers) { 23 | this->addHeader(header, value); 24 | } 25 | } 26 | 27 | void Request::setBody(std::shared_ptr body) 28 | { 29 | this->body = body; 30 | } 31 | 32 | std::string Request::getMethod() 33 | { 34 | return this->method; 35 | } 36 | 37 | std::string Request::getUri() 38 | { 39 | return this->uri; 40 | } 41 | 42 | std::map Request::getHeaders() 43 | { 44 | return this->headers; 45 | } 46 | 47 | std::shared_ptr Request::getBody() 48 | { 49 | return this->body; 50 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bart Kessels 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/presentation/highlighters/SyntaxHighlighter.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/highlighters/SyntaxHighlighter.hpp" 2 | 3 | using namespace getit::presentation::highlighters; 4 | 5 | SyntaxHighlighter::SyntaxHighlighter(QTextDocument* document): 6 | QSyntaxHighlighter(document) 7 | { 8 | 9 | } 10 | 11 | void SyntaxHighlighter::startHighlighting(std::list> rules) 12 | { 13 | this->rules = rules; 14 | this->rehighlight(); 15 | } 16 | 17 | void SyntaxHighlighter::stopHighlighting() 18 | { 19 | this->rules.clear(); 20 | this->rehighlight(); 21 | } 22 | 23 | void SyntaxHighlighter::highlightBlock(const QString& text) 24 | { 25 | for (const auto& rule : this->rules) { 26 | QRegularExpression pattern(QString::fromStdString(rule->regex)); 27 | QTextCharFormat format; 28 | format.setForeground(QColor(rule->color)); 29 | 30 | QRegularExpressionMatchIterator matchIterator = pattern.globalMatch(text); 31 | while (matchIterator.hasNext()) { 32 | const auto& match = matchIterator.next(); 33 | setFormat(match.capturedStart(), match.capturedLength(), format); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/domain/contracts/RequestFactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "domain/models/Request.hpp" 8 | 9 | namespace getit::domain::contracts 10 | { 11 | struct RequestFactory 12 | { 13 | public: 14 | virtual ~RequestFactory() = default; 15 | 16 | virtual std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers) = 0; 17 | virtual std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers, const std::string& body) = 0; 18 | virtual std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers, const std::string& body, const std::string& contentType) = 0; 19 | virtual std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers, std::map elements, std::map files, const std::string& boundary) = 0; 20 | }; 21 | } -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Vulnerability scan" 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | schedule: 7 | - cron: '24 18 * * 3' 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'cpp' ] 22 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 23 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | - name: Install dependencies 29 | run: sudo apt update && sudo apt install -yq libboost-all-dev build-essential libgl1-mesa-dev qt6-base-dev cmake 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v2 32 | with: 33 | languages: ${{ matrix.language }} 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v2 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v2 38 | -------------------------------------------------------------------------------- /src/data/repositories/RawRequestRepository.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "data/contracts/RequestRepository.hpp" 11 | #include "domain/contracts/RequestFactory.hpp" 12 | #include "domain/models/Request.hpp" 13 | #include "domain/implementations/RawRequestBody.hpp" 14 | 15 | namespace getit::data::repositories 16 | { 17 | class RawRequestRepository: public contracts::RequestRepository 18 | { 19 | public: 20 | explicit RawRequestRepository(std::shared_ptr factory); 21 | ~RawRequestRepository() override = default; 22 | 23 | void saveRequest(const std::string& filePath, std::shared_ptr request) override; 24 | std::shared_ptr loadRequest(const std::string& filePath) override; 25 | 26 | private: 27 | std::shared_ptr factory; 28 | 29 | const std::string BODY_NAME = "body"; 30 | const std::string CONTENT_TYPE_NAME = "contentType"; 31 | }; 32 | } -------------------------------------------------------------------------------- /src/data/factories/RequestRepositoryFactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "data/contracts/RequestRepository.hpp" 7 | #include "data/contracts/RequestRepositoryFactory.hpp" 8 | #include "data/exceptions/NoAvailableRepositoryException.hpp" 9 | #include "data/repositories/FormDataRequestRepository.hpp" 10 | #include "data/repositories/RawRequestRepository.hpp" 11 | #include "data/repositories/RequestRepository.hpp" 12 | #include "domain/contracts/RequestFactory.hpp" 13 | #include "domain/models/Request.hpp" 14 | #include "domain/implementations/FormDataRequestBody.hpp" 15 | #include "domain/implementations/RawRequestBody.hpp" 16 | 17 | namespace getit::data::factories 18 | { 19 | class RequestRepositoryFactory: public contracts::RequestRepositoryFactory 20 | { 21 | public: 22 | explicit RequestRepositoryFactory(std::shared_ptr factory); 23 | ~RequestRepositoryFactory() override = default; 24 | 25 | std::shared_ptr getRepository() override; 26 | 27 | private: 28 | std::shared_ptr factory; 29 | }; 30 | } -------------------------------------------------------------------------------- /src/presentation/fragments/BodyFragment/BodyFragmentController.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/fragments/BodyFragment/BodyFragmentController.hpp" 2 | 3 | using namespace getit; 4 | using namespace getit::presentation::fragments; 5 | 6 | BodyFragmentController::BodyFragmentController(IBodyFragmentView* view): 7 | view(view) 8 | { 9 | 10 | } 11 | 12 | BodyFragmentController::~BodyFragmentController() 13 | { 14 | delete this->view; 15 | } 16 | 17 | std::shared_ptr BodyFragmentController::getContent() 18 | { 19 | switch(view->getBodyType()) 20 | { 21 | case BodyType::FORM_DATA: 22 | return view->getFormDataBody(); 23 | case BodyType::RAW: 24 | return view->getRawBody(); 25 | } 26 | 27 | return nullptr; 28 | } 29 | 30 | void BodyFragmentController::setContent(std::shared_ptr content) 31 | { 32 | if (const auto& formData = std::dynamic_pointer_cast(content)) { 33 | view->setFormDataBody(formData); 34 | } else if (const auto& raw = std::dynamic_pointer_cast(content)) { 35 | view->setRawBody(raw); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | To make sure everyone can contribute to GetIt we value the quality and understandability of our code. When you're building 2 | a feature, or patching a bug and you've made a decision. Please document this decision in the [`docs/design.md`](docs/design.md) document. 3 | This will help future contributors understand your rationale. If you've updated some logic in the code, please always update related 4 | test cases or add test cases to test your logic. 5 | 6 | ## Branching strategy 7 | 8 | This project does not use a development branch but uses short lived feature branches which are directly merged into `main`. 9 | We've chosen this strategy 'cause we believe in _release often, release fast_, there's no need for your feature to gather dust in 10 | a stale development branch. However we do have some guidelines: 11 | 12 | - If you're contributing a __feature__ use the `feature/` naming convention 13 | - If you're contributing a __bug fix__ use the `bugfix/` naming convention 14 | - If you're contributing to __documentation__ use the `documentation/` naming convention 15 | - If you're contributing a __small fix__, like a version bump to a dependency, use the `hotfix/-` naming convention 16 | -------------------------------------------------------------------------------- /src/presentation/fragments/HeadersFragment/HeadersFragmentView.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "presentation/fragments/HeadersFragment/IHeadersFragmentView.hpp" 10 | 11 | QT_BEGIN_NAMESPACE 12 | namespace Ui { class HeadersFragmentView; } 13 | QT_END_NAMESPACE 14 | 15 | namespace getit::presentation::fragments 16 | { 17 | class HeadersFragmentView : public IHeadersFragmentView, public QWidget 18 | { 19 | public: 20 | explicit HeadersFragmentView(QWidget* parent = nullptr); 21 | ~HeadersFragmentView() override; 22 | 23 | std::map getHeaders() override; 24 | void setHeaders(const std::map& headers) override; 25 | 26 | private: 27 | Ui::HeadersFragmentView* ui; 28 | 29 | const int keyIndex = 0; 30 | const int valueIndex = 1; 31 | const Qt::ItemFlags treeItemFlag = Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled; 32 | 33 | void addDefaultHeader(); 34 | void addHeader(const std::string& header, const std::string& value); 35 | void removeSelectedHeader(); 36 | }; 37 | } -------------------------------------------------------------------------------- /src/presentation/highlighters/XmlSyntaxHighlighterRule.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "presentation/highlighters/SyntaxHighlighterRule.hpp" 7 | 8 | namespace getit::presentation::highlighters 9 | { 10 | struct XmlSyntaxHighlighterRule 11 | { 12 | public: 13 | inline static const auto& tagRule = std::make_shared( 14 | "<[^>]*>|<[^>]*/[^>]*\\?>", 15 | 0x72bcd4 16 | ); 17 | inline static const auto& attributeValueRule = std::make_shared( 18 | "\"(.*?)\"", 19 | 0x800080 20 | ); 21 | inline static const auto& commentRule = std::make_shared( 22 | "", 23 | 0x00ff00 24 | ); 25 | inline static const auto& entityRule = std::make_shared( 26 | "&[^-\\s][\\s\\S]*?[^-\\s];", 27 | 0x0000ff 28 | ); 29 | 30 | inline static std::list> rules = { 31 | tagRule, 32 | entityRule, 33 | attributeValueRule, 34 | commentRule 35 | }; 36 | }; 37 | } -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "data/factories/RequestRepositoryFactory.hpp" 5 | #include "domain/factories/RequestFactory.hpp" 6 | #include "presentation/windows/MainWindow.hpp" 7 | #include "presentation/windows/MainWindowViewModel.hpp" 8 | #include "service/factories/RequestServiceFactory.hpp" 9 | 10 | using namespace getit; 11 | 12 | int main(int argc, char** args) 13 | { 14 | const auto& appName = "GetIt"; 15 | QIcon icon(":icons/main"); 16 | 17 | const auto& requestFactory = std::make_shared(); 18 | const auto& requestServiceFactory = std::make_shared(); 19 | const auto& requestRepositoryFactory = std::make_shared(requestFactory); 20 | 21 | QApplication app(argc, args); 22 | QApplication::setWindowIcon(icon); 23 | QApplication::setApplicationDisplayName(appName); 24 | QApplication::setApplicationName(appName); 25 | 26 | const auto& window = std::make_shared(); 27 | const auto& viewModel = std::make_shared( 28 | requestFactory, requestServiceFactory, requestRepositoryFactory, window); 29 | window->setViewModel(viewModel); 30 | window->show(); 31 | 32 | return QApplication::exec(); 33 | } -------------------------------------------------------------------------------- /src/service/implementations/CppRestRequestService.cpp: -------------------------------------------------------------------------------- 1 | #include "service/implementations/CppRestRequestService.hpp" 2 | 3 | using namespace getit::service::implementations; 4 | 5 | std::future> CppRestRequestService::send(std::shared_ptr request) 6 | { 7 | const auto& cppRequest = std::make_shared(request->getMethod()); 8 | const auto& client = std::make_shared(request->getUri()); 9 | 10 | for (const auto& [header, value] : request->getHeaders()) { 11 | cppRequest->headers().add(header, value); 12 | } 13 | 14 | if (request->getBody()) 15 | cppRequest->set_body( 16 | request->getBody()->getBody(), 17 | request->getBody()->getContentType() 18 | ); 19 | 20 | return std::async(std::launch::async, [client, cppRequest]() { 21 | const auto& rawResponse = client->request(*cppRequest).get(); 22 | const auto& response = std::make_shared(); 23 | bool ignoreContentType = true; 24 | 25 | response->body = rawResponse.extract_string(ignoreContentType).get(); 26 | 27 | for (auto [header, value] : rawResponse.headers()) { 28 | response->headers.insert({header, value}); 29 | } 30 | 31 | return response; 32 | }); 33 | } -------------------------------------------------------------------------------- /src/domain/factories/RequestFactory.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "domain/contracts/RequestFactory.hpp" 9 | #include "domain/implementations/FormDataRequestBody.hpp" 10 | #include "domain/implementations/RawRequestBody.hpp" 11 | #include "domain/models/Request.hpp" 12 | 13 | namespace getit::domain::factories 14 | { 15 | class RequestFactory: public contracts::RequestFactory 16 | { 17 | public: 18 | ~RequestFactory() override = default; 19 | 20 | std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers) override; 21 | std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers, const std::string& body) override; 22 | std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers, const std::string& body, const std::string& contentType) override; 23 | std::shared_ptr getRequest(const std::string& method, const std::string& uri, std::map headers, std::map elements, std::map files, const std::string& boundary) override; 24 | }; 25 | } -------------------------------------------------------------------------------- /src/data/repositories/RequestRepository.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "data/contracts/RequestRepository.hpp" 11 | #include "data/exceptions/NoAvailableRepositoryException.hpp" 12 | #include "data/repositories/FormDataRequestRepository.hpp" 13 | #include "data/repositories/RawRequestRepository.hpp" 14 | 15 | #include "domain/contracts/RequestFactory.hpp" 16 | #include "domain/implementations/FormDataRequestBody.hpp" 17 | #include "domain/implementations/RawRequestBody.hpp" 18 | #include "domain/models/Request.hpp" 19 | 20 | namespace getit::data::repositories 21 | { 22 | class RequestRepository : public contracts::RequestRepository 23 | { 24 | public: 25 | explicit RequestRepository(std::shared_ptr formDataRepository, std::shared_ptr rawRepository); 26 | ~RequestRepository() override = default; 27 | 28 | void saveRequest(const std::string& filePath, std::shared_ptr request) override; 29 | std::shared_ptr loadRequest(const std::string& filePath) override; 30 | 31 | private: 32 | std::shared_ptr formDataRepository; 33 | std::shared_ptr rawRepository; 34 | }; 35 | } -------------------------------------------------------------------------------- /src/data/repositories/FormDataRequestRepository.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "data/contracts/RequestRepository.hpp" 11 | #include "domain/contracts/RequestFactory.hpp" 12 | #include "domain/models/Request.hpp" 13 | #include "domain/implementations/FormDataRequestBody.hpp" 14 | 15 | namespace getit::data::repositories 16 | { 17 | class FormDataRequestRepository: public contracts::RequestRepository 18 | { 19 | public: 20 | explicit FormDataRequestRepository(std::shared_ptr factory); 21 | ~FormDataRequestRepository() override = default; 22 | 23 | void saveRequest(const std::string& filePath, std::shared_ptr request) override; 24 | std::shared_ptr loadRequest(const std::string& filePath) override; 25 | 26 | private: 27 | std::shared_ptr factory; 28 | 29 | const std::string BOUNDARY_NAME = "boundary"; 30 | const std::string ELEMENTS_ARRAY_NAME = "elements"; 31 | const std::string FILES_ARRAY_NAME = "files"; 32 | const std::string KEY_NAME = "key"; 33 | const std::string ELEMENT_VALUE_NAME = "value"; 34 | const std::string FILE_PATH_NAME = "filePath"; 35 | }; 36 | } -------------------------------------------------------------------------------- /third_party/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | include(FetchContent) 4 | 5 | # ------------------------------------------------------------------------------ 6 | # Trompeloeil 7 | FetchContent_Declare( 8 | extern_trompeloeil 9 | 10 | GIT_REPOSITORY https://github.com/rollbear/trompeloeil.git 11 | GIT_TAG v42) 12 | FetchContent_MakeAvailable(extern_trompeloeil) 13 | 14 | # ------------------------------------------------------------------------------ 15 | # Catch2 16 | FetchContent_Declare( 17 | extern_catch2 18 | 19 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 20 | GIT_TAG v2.13.9) 21 | FetchContent_MakeAvailable(extern_catch2) 22 | 23 | # ------------------------------------------------------------------------------ 24 | # Cpp rest SDK 25 | SET(BUILD_TESTS OFF) 26 | SET(BUILD_SAMPLES OFF) 27 | 28 | FetchContent_Declare( 29 | extern_restsdk 30 | 31 | GIT_REPOSITORY https://github.com/microsoft/cpprestsdk.git 32 | GIT_TAG 2.10.18) 33 | FetchContent_MakeAvailable(extern_restsdk) 34 | 35 | SET(BUILD_TESTS ON) 36 | SET(BUILD_SAMPLES ON) 37 | 38 | # ------------------------------------------------------------------------------ 39 | # JSON 40 | SET(JSON_BuildTests OFF) 41 | 42 | FetchContent_Declare( 43 | extern_json 44 | 45 | GIT_REPOSITORY https://github.com/nlohmann/json.git 46 | GIT_TAG v3.10.5) 47 | FetchContent_MakeAvailable(extern_json) -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test GetIt 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - development 8 | pull_request: 9 | branches: 10 | - main 11 | - development 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install boost 19 | run: sudo apt update && sudo apt install -yq libboost-all-dev 20 | - name: Install Qt6 21 | run: sudo apt update && sudo apt install -yq build-essential libgl1-mesa-dev qt6-base-dev 22 | - name: Install CMake 23 | run: sudo apt update && sudo apt install -yq cmake 24 | - name: Run CMake 25 | run: cmake . 26 | - name: Build GetIt 27 | run: make GetIt 28 | test: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Install boost 33 | run: sudo apt update && sudo apt install -yq libboost-all-dev 34 | - name: Install Qt6 35 | run: sudo apt update && sudo apt install -yq build-essential libgl1-mesa-dev qt6-base-dev 36 | - name: Install CMake 37 | run: sudo apt update && sudo apt install -yq cmake 38 | - name: Run CMake 39 | run: cmake . -DCMAKE_CXX_FLAGS="--coverage" 40 | - name: Build GetIt tests 41 | run: make getit_tests 42 | - name: Run tests 43 | run: ./bin/getit_tests 44 | - uses: codecov/codecov-action@v2.1.0 45 | with: 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/macos_bundle.yml: -------------------------------------------------------------------------------- 1 | name: Build MacOS package 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | BUILD_TYPE: Release 10 | 11 | jobs: 12 | build: 13 | name: Build MacOS package 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install dependencies 19 | run: brew install boost qt ninja cmake 20 | - name: Create GetIt project 21 | run: cmake . -G Ninja -Dpackaging=true -DUseQt6=true -DCMAKE_CXX_FLAGS="-w" 22 | - name: Create GetIt Bundle 23 | run: ninja GetIt 24 | - name: Link Qt to app bundle 25 | run: macdeployqt ./bin/GetIt.app -timestamp -libpath=./_deps 26 | - name: Copy icon to MacOS Bundle 27 | run: cp ./packaging/macos/icons/getit.icns ./bin/GetIt.app/Contents/Resources 28 | - name: Zip MacOS Bundle 29 | run: zip -rT getit.zip ./bin/GetIt.app >> /dev/null 30 | - name: Upload MacOS bundle as build artifact 31 | uses: actions/upload-artifact@v3.0.0 32 | with: 33 | name: GetIt-MacOS-x64.zip 34 | path: ./getit.zip 35 | - name: Upload MacOS bundle as release artifact 36 | uses: actions/upload-release-asset@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | upload_url: https://github.com/bartkessels/GetIt/releases/latest 41 | asset_path: ./getit.zip 42 | asset_name: GetIt-MacOS-x64.zip 43 | asset_content_type: application/zip 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/data/repositories/RequestRepository.cpp: -------------------------------------------------------------------------------- 1 | #include "data/repositories/RequestRepository.hpp" 2 | 3 | using namespace getit::data::repositories; 4 | 5 | RequestRepository::RequestRepository(std::shared_ptr formDataRepository, std::shared_ptr rawRepository): 6 | formDataRepository(std::move(formDataRepository)), 7 | rawRepository(std::move(rawRepository)) 8 | { 9 | 10 | } 11 | 12 | void RequestRepository::saveRequest(const std::string& filePath, std::shared_ptr request) 13 | { 14 | if (std::dynamic_pointer_cast(request->getBody())) { 15 | formDataRepository->saveRequest(filePath, request); 16 | } else if (std::dynamic_pointer_cast(request->getBody())) { 17 | rawRepository->saveRequest(filePath, request); 18 | } else { 19 | throw exceptions::NoAvailableRepositoryException(); 20 | } 21 | } 22 | 23 | std::shared_ptr RequestRepository::loadRequest(const std::string& filePath) 24 | { 25 | auto jsonObject = nlohmann::json::object(); 26 | 27 | std::ifstream input(filePath); 28 | input >> jsonObject; 29 | input.close(); 30 | 31 | if (jsonObject.contains(RAW_BODY_TYPE_NAME)) { 32 | return rawRepository->loadRequest(filePath); 33 | } else if (jsonObject.contains(FORM_DATA_BODY_TYPE_NAME)) { 34 | return formDataRepository->loadRequest(filePath); 35 | } 36 | 37 | throw exceptions::NoAvailableRepositoryException(); 38 | } -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/ResponseFragmentView.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "presentation/fragments/ResponseFragment/ContentType.hpp" 11 | #include "presentation/fragments/ResponseFragment/IResponseFragmentView.hpp" 12 | #include "presentation/highlighters/JsonSyntaxHighlighterRule.hpp" 13 | #include "presentation/highlighters/SyntaxHighlighter.hpp" 14 | #include "presentation/highlighters/XmlSyntaxHighlighterRule.hpp" 15 | 16 | QT_BEGIN_NAMESPACE 17 | namespace Ui { class ResponseFragmentView; } 18 | QT_END_NAMESPACE 19 | 20 | namespace getit::presentation::fragments 21 | { 22 | class ResponseFragmentView : public IResponseFragmentView, public QWidget 23 | { 24 | public: 25 | explicit ResponseFragmentView(QWidget* parent = nullptr); 26 | ~ResponseFragmentView() override; 27 | 28 | std::map getHeaders() override; 29 | std::string getBody() override; 30 | 31 | void setHeaders(const std::map& headers) override; 32 | void setBody(const std::string& body) override; 33 | 34 | void clearHeaders() override; 35 | 36 | private: 37 | Ui::ResponseFragmentView* ui; 38 | highlighters::SyntaxHighlighter* syntaxHighlighter; 39 | 40 | const int keyIndex = 0; 41 | const int valueIndex = 1; 42 | const Qt::ItemFlags treeItemFlag = Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled; 43 | 44 | void addHeader(const std::string& header, const std::string& value); 45 | void setContentType(int selectedIndex); 46 | }; 47 | } -------------------------------------------------------------------------------- /src/domain/implementations/FormDataRequestBody.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "domain/models/RequestBody.hpp" 9 | 10 | namespace getit::domain::implementations 11 | { 12 | class FormDataRequestBody: public models::RequestBody 13 | { 14 | public: 15 | explicit FormDataRequestBody(std::string boundary); 16 | FormDataRequestBody() = default; 17 | ~FormDataRequestBody() = default; 18 | 19 | void addElement(std::string key, std::string value); 20 | void addFile(std::string key, std::string filePath); 21 | 22 | void setBoundary(std::string boundary); 23 | void setElements(std::map elements); 24 | void setFiles(std::map files); 25 | 26 | std::string getBoundary(); 27 | std::map getElements(); 28 | std::map getFiles(); 29 | 30 | std::string getBody() override; 31 | std::string getContentType() override; 32 | 33 | private: 34 | std::string boundary; 35 | std::map elements; 36 | std::map files; 37 | 38 | std::string getElementsBody(); 39 | std::string getFilesBody(); 40 | 41 | // Boost templates 42 | const std::string CONTENT_TYPE_TEMPLATE = R"(multipart/form-data; boundary="%1%")"; 43 | const std::string BODY_WITH_BOUNDARY_TEMPLATE = "%1%%2%\r\n--%3%--\r\n"; 44 | const std::string ELEMENT_TEMPLATE = "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"\r\n\r\n%3%\r\n"; 45 | const std::string FILE_TEMPLATE = "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"; filename=\"%3%\"\r\n\r\n%4%\r\n"; 46 | }; 47 | } -------------------------------------------------------------------------------- /src/presentation/fragments/HeadersFragment/HeadersFragmentView.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/fragments/HeadersFragment/HeadersFragmentView.hpp" 2 | #include "./ui_HeadersFragmentView.h" 3 | 4 | using namespace getit::presentation::fragments; 5 | 6 | HeadersFragmentView::HeadersFragmentView(QWidget* parent): 7 | QWidget(parent), 8 | ui(new Ui::HeadersFragmentView()) 9 | { 10 | ui->setupUi(this); 11 | 12 | connect(ui->addHeader, &QPushButton::pressed, this, &HeadersFragmentView::addDefaultHeader); 13 | connect(ui->removeHeader, &QPushButton::pressed, this, &HeadersFragmentView::removeSelectedHeader); 14 | } 15 | 16 | HeadersFragmentView::~HeadersFragmentView() 17 | { 18 | delete ui; 19 | } 20 | 21 | std::map HeadersFragmentView::getHeaders() 22 | { 23 | std::map headers; 24 | 25 | for (int i = 0; i < ui->headers->topLevelItemCount(); ++i) { 26 | QTreeWidgetItem* itm = ui->headers->topLevelItem(i); 27 | 28 | headers.insert_or_assign( 29 | itm->text(keyIndex).toStdString(), 30 | itm->text(valueIndex).toStdString() 31 | ); 32 | } 33 | 34 | return headers; 35 | } 36 | 37 | void HeadersFragmentView::setHeaders(const std::map& headers) 38 | { 39 | for (const auto& [header, value] : headers) { 40 | addHeader(header, value); 41 | } 42 | } 43 | 44 | void HeadersFragmentView::addDefaultHeader() 45 | { 46 | this->addHeader("Header", "Value"); 47 | } 48 | 49 | void HeadersFragmentView::addHeader(const std::string& header, const std::string& value) 50 | { 51 | const auto& row = new QTreeWidgetItem(ui->headers); 52 | 53 | row->setText(keyIndex, QString::fromStdString(header)); 54 | row->setText(valueIndex, QString::fromStdString(value)); 55 | row->setFlags(treeItemFlag); 56 | } 57 | 58 | void HeadersFragmentView::removeSelectedHeader() 59 | { 60 | delete ui->headers->currentItem(); 61 | } -------------------------------------------------------------------------------- /.github/workflows/linux_flatpak.yml: -------------------------------------------------------------------------------- 1 | name: Build Linux Flatpak 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | BUILD_TYPE: Release 10 | SDK_VERSION: 6.2 11 | ARCHITECTURE: x86_64 12 | 13 | jobs: 14 | build: 15 | name: Build Linux Flatpak 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Update apt 21 | run: sudo apt update && sudo apt upgrade -y 22 | - name: Install dependencies 23 | run: sudo apt install -y libboost-all-dev qt5-default ninja-build cmake flatpak-builder flatpak 24 | - name: Prepare flatpak template 25 | run: cmake . -G Ninja -Dpackaging=true 26 | - name: Add Flathub to flatpak refs 27 | run: flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 28 | - name: Install Qt/Kde runtime 29 | run: flatpak install --user -y flathub runtime/org.kde.Platform/${{env.ARCHITECTURE}}/${{env.SDK_VERSION}} 30 | - name: Install Qt/Kde SDK 31 | run: flatpak install --user -y flathub runtime/org.kde.Sdk/${{env.ARCHITECTURE}}/${{env.SDK_VERSION}} 32 | - name: Create flatpak repository for GetIt 33 | run: flatpak-builder --repo=getit_repo getit ./packaging/linux/getit.yml 34 | - name: Create flatpak bundle 35 | run: flatpak build-bundle getit_repo getit.flatpak net.bartkessels.getit 36 | - name: Zip flatpak bundle 37 | run: zip -rT getit.zip ./bin/getit.flatpak >> /dev/null 38 | - name: Upload GetIt flatpak as build artifact 39 | uses: actions/upload-artifact@v3.0.0 40 | with: 41 | name: GetIt-Linux-Flatpak.zip 42 | path: ./getit.zip 43 | - name: Upload binaries to release 44 | uses: svenstaro/upload-release-action@v2 45 | with: 46 | repo_token: ${{ secrets.GITHUB_TOKEN }} 47 | file: ./getit.zip 48 | asset_name: GetIt-Linux-x64.zip 49 | tag: ${{ github.ref }} 50 | -------------------------------------------------------------------------------- /tests/presentation/fragments/tst_HeadersFragmentController.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "presentation/fragments/HeadersFragment/HeadersFragmentController.hpp" 8 | #include "presentation/fragments/HeadersFragment/IHeadersFragmentView.hpp" 9 | 10 | using namespace getit; 11 | using namespace getit::presentation::fragments; 12 | using trompeloeil::_; 13 | 14 | class HeadersFragmentViewMock: public IHeadersFragmentView 15 | { 16 | public: 17 | MAKE_MOCK0(getHeaders, (std::map()), override); 18 | MAKE_MOCK1(setHeaders, void(const std::map&), override); 19 | }; 20 | 21 | TEST_CASE("HeadersFragmentController.getContent") 22 | { 23 | // Before 24 | auto view = new HeadersFragmentViewMock(); 25 | const auto& sut = std::make_shared(view); 26 | 27 | SECTION("returns the headers from the view") 28 | { 29 | // Arrange 30 | std::map expectedHeaders = { 31 | { "Content-Type", "application/json" }, 32 | { "Set-Cookies", "we-track-you=always" } 33 | }; 34 | 35 | ALLOW_CALL(*view, getHeaders()).RETURN(expectedHeaders); 36 | 37 | // Act 38 | const auto& actual = sut->getContent(); 39 | 40 | // Assert 41 | REQUIRE(actual == expectedHeaders); 42 | } 43 | } 44 | 45 | 46 | TEST_CASE("HeadersFragmentController.setContent") 47 | { 48 | // Before 49 | auto view = new HeadersFragmentViewMock(); 50 | const auto& sut = std::make_shared(view); 51 | 52 | SECTION("calls setHeaders on the view") 53 | { 54 | // Arrange 55 | std::map expectedHeaders = { 56 | { "Content-Type", "application/json" }, 57 | { "Set-Cookies", "we-track-you=always" } 58 | }; 59 | 60 | // Assert 61 | REQUIRE_CALL(*view, setHeaders(expectedHeaders)); 62 | 63 | // Act 64 | sut->setContent(expectedHeaders); 65 | } 66 | } -------------------------------------------------------------------------------- /src/domain/factories/RequestFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "domain/factories/RequestFactory.hpp" 2 | 3 | using namespace getit::domain::factories; 4 | 5 | std::shared_ptr RequestFactory::getRequest(const std::string& method, const std::string& uri, std::map headers, const std::string& body) 6 | { 7 | const auto& requestBody = std::make_shared(); 8 | const auto& request = RequestFactory::getRequest(method, uri, headers); 9 | 10 | requestBody->setBody(body); 11 | 12 | request->setBody(requestBody); 13 | 14 | return request; 15 | } 16 | 17 | std::shared_ptr RequestFactory::getRequest(const std::string& method, const std::string& uri, std::map headers, const std::string& body, const std::string& contentType) 18 | { 19 | const auto& requestBody = std::make_shared(contentType); 20 | const auto& request = RequestFactory::getRequest(method, uri, headers); 21 | 22 | requestBody->setBody(body); 23 | 24 | request->setBody(requestBody); 25 | 26 | return request; 27 | } 28 | 29 | std::shared_ptr RequestFactory::getRequest(const std::string& method, const std::string& uri, std::map headers, std::map elements, std::map files, const std::string& boundary) 30 | { 31 | const auto& requestBody = std::make_shared(boundary); 32 | const auto& request = RequestFactory::getRequest(method, uri, headers); 33 | 34 | requestBody->setElements(elements); 35 | requestBody->setFiles(files); 36 | 37 | request->setBody(requestBody); 38 | 39 | return request; 40 | } 41 | 42 | std::shared_ptr RequestFactory::getRequest(const std::string& method, const std::string& uri, std::map headers) 43 | { 44 | const auto& request = std::make_shared(); 45 | 46 | request->setMethod(method); 47 | request->setUri(uri); 48 | request->setHeaders(headers); 49 | 50 | return request; 51 | } -------------------------------------------------------------------------------- /src/data/repositories/RawRequestRepository.cpp: -------------------------------------------------------------------------------- 1 | #include "data/repositories/RawRequestRepository.hpp" 2 | 3 | using namespace getit::data::repositories; 4 | 5 | RawRequestRepository::RawRequestRepository(std::shared_ptr factory): 6 | factory(std::move(factory)) 7 | { 8 | 9 | } 10 | 11 | void RawRequestRepository::saveRequest(const std::string& filePath, std::shared_ptr request) 12 | { 13 | const auto& body = std::dynamic_pointer_cast(request->getBody()); 14 | auto jsonObject = nlohmann::json::object(); 15 | 16 | jsonObject[METHOD_NAME] = request->getMethod(); 17 | jsonObject[URI_NAME] = request->getUri(); 18 | jsonObject[HEADERS_NAME] = nlohmann::json::array(); 19 | 20 | jsonObject[RAW_BODY_TYPE_NAME] = nlohmann::json::object(); 21 | jsonObject[RAW_BODY_TYPE_NAME][CONTENT_TYPE_NAME] = body->getContentType(); 22 | jsonObject[RAW_BODY_TYPE_NAME][BODY_NAME] = body->getBody(); 23 | 24 | for (const auto& [key, value] : request->getHeaders()) { 25 | auto header = nlohmann::json::object(); 26 | header[HEADER_NAME] = key; 27 | header[HEADER_VALUE] = value; 28 | jsonObject[HEADERS_NAME].push_back(header); 29 | } 30 | 31 | std::ofstream output(filePath); 32 | output << std::setw(4) << jsonObject << std::endl; 33 | output.close(); 34 | } 35 | 36 | std::shared_ptr RawRequestRepository::loadRequest(const std::string& filePath) 37 | { 38 | auto jsonObject = nlohmann::json::object(); 39 | 40 | std::ifstream input(filePath); 41 | input >> jsonObject; 42 | input.close(); 43 | 44 | const auto& method = jsonObject[METHOD_NAME]; 45 | const auto& uri = jsonObject[URI_NAME]; 46 | const auto& contentType = jsonObject[RAW_BODY_TYPE_NAME][CONTENT_TYPE_NAME]; 47 | const auto& body = jsonObject[RAW_BODY_TYPE_NAME][BODY_NAME]; 48 | 49 | std::map headers; 50 | 51 | for (auto obj : jsonObject[HEADERS_NAME]) { 52 | headers.emplace(obj[HEADER_NAME], obj[HEADER_VALUE]); 53 | } 54 | 55 | return this->factory->getRequest(method, uri, headers, body, contentType); 56 | } -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 4 | set(CMAKE_AUTOUIC ON) 5 | set(CMAKE_AUTOMOC ON) 6 | set(CMAKE_AUTORCC ON) 7 | 8 | find_package(Boost COMPONENTS system REQUIRED) 9 | 10 | # Try to use Qt6 if it's available, otherwise 11 | # fall back to Qt5 12 | find_package(Qt6 COMPONENTS Widgets) 13 | if (NOT Qt6_FOUND) 14 | find_package(Qt5 5 COMPONENTS Widgets REQUIRED) 15 | endif() 16 | 17 | set(SOURCEFILES 18 | main.cpp 19 | 20 | data/factories/RequestRepositoryFactory.cpp 21 | data/repositories/FormDataRequestRepository.cpp 22 | data/repositories/RawRequestRepository.cpp 23 | data/repositories/RequestRepository.cpp 24 | 25 | domain/factories/RequestFactory.cpp 26 | domain/implementations/FormDataRequestBody.cpp 27 | domain/implementations/RawRequestBody.cpp 28 | domain/models/Request.cpp 29 | 30 | presentation/fragments/BodyFragment/BodyFragmentController.cpp 31 | presentation/fragments/BodyFragment/BodyFragmentView.cpp 32 | presentation/fragments/HeadersFragment/HeadersFragmentView.cpp 33 | presentation/fragments/HeadersFragment/HeadersFragmentController.cpp 34 | presentation/fragments/ResponseFragment/ResponseFragmentController.cpp 35 | presentation/fragments/ResponseFragment/ResponseFragmentView.cpp 36 | 37 | presentation/highlighters/SyntaxHighlighter.cpp 38 | 39 | presentation/windows/MainWindow.cpp 40 | presentation/windows/MainWindowViewModel.cpp 41 | 42 | service/factories/RequestServiceFactory.cpp 43 | service/implementations/CppRestRequestService.cpp 44 | ) 45 | 46 | add_executable( 47 | ${CMAKE_PROJECT_NAME} 48 | ${SOURCEFILES} 49 | ../resources/resources.qrc 50 | ) 51 | 52 | 53 | install(TARGETS ${CMAKE_PROJECT_NAME} 54 | RUNTIME 55 | DESTINATION bin 56 | ) 57 | 58 | # Link libraries 59 | target_link_libraries(${CMAKE_PROJECT_NAME} Boost::system) 60 | target_link_libraries(${CMAKE_PROJECT_NAME} cpprest) 61 | target_link_libraries(${CMAKE_PROJECT_NAME} nlohmann_json) 62 | 63 | if (NOT Qt6_FOUND) 64 | target_link_libraries(${CMAKE_PROJECT_NAME} Qt5::Widgets) 65 | else() 66 | target_link_libraries(${CMAKE_PROJECT_NAME} Qt::Widgets) 67 | endif() -------------------------------------------------------------------------------- /src/presentation/windows/MainWindowViewModel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "data/contracts/RequestRepositoryFactory.hpp" 8 | 9 | #include "domain/contracts/RequestFactory.hpp" 10 | 11 | #include "domain/models/Request.hpp" 12 | #include "domain/models/RequestBody.hpp" 13 | 14 | #include "presentation/states/Error.hpp" 15 | #include "presentation/states/FileOpened.hpp" 16 | #include "presentation/states/RequestState.hpp" 17 | #include "presentation/states/Sending.hpp" 18 | #include "presentation/states/Sent.hpp" 19 | 20 | #include "presentation/windows/IMainWindow.hpp" 21 | #include "presentation/windows/IMainWindowViewModel.hpp" 22 | 23 | #include "service/contracts/RequestServiceFactory.hpp" 24 | 25 | using namespace getit::data::contracts; 26 | using namespace getit::domain::contracts; 27 | using namespace getit::domain::models; 28 | using namespace getit::service::contracts; 29 | 30 | namespace getit::presentation::windows 31 | { 32 | class MainWindowViewModel: public IMainWindowViewModel 33 | { 34 | public: 35 | explicit MainWindowViewModel( 36 | std::shared_ptr requestFactory, 37 | std::shared_ptr requestServiceFactory, 38 | std::shared_ptr requestRepositoryFactory, 39 | std::shared_ptr view); 40 | ~MainWindowViewModel() override = default; 41 | 42 | void sendRequest(std::string method, std::string uri, std::map headers, 43 | std::shared_ptr body) override; 44 | void saveRequest(std::string method, std::string uri, std::map headers, 45 | std::shared_ptr body, const std::string& location) override; 46 | void openRequest(const std::string& location) override; 47 | 48 | private: 49 | std::shared_ptr requestFactory; 50 | std::shared_ptr requestServiceFactory; 51 | std::shared_ptr requestRepositoryFactory; 52 | std::shared_ptr view; 53 | }; 54 | } -------------------------------------------------------------------------------- /src/presentation/fragments/BodyFragment/BodyFragmentView.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "domain/implementations/FormDataRequestBody.hpp" 13 | #include "domain/implementations/RawRequestBody.hpp" 14 | #include "domain/models/RequestBody.hpp" 15 | #include "presentation/fragments/BodyFragment/BodyType.hpp" 16 | #include "presentation/fragments/BodyFragment/IBodyFragmentView.hpp" 17 | 18 | QT_BEGIN_NAMESPACE 19 | namespace Ui { class BodyFragmentView; } 20 | QT_END_NAMESPACE 21 | 22 | namespace getit::presentation::fragments 23 | { 24 | class BodyFragmentView : public QWidget, public IBodyFragmentView 25 | { 26 | Q_OBJECT 27 | 28 | public: 29 | explicit BodyFragmentView(QWidget* parent = nullptr); 30 | ~BodyFragmentView() override; 31 | 32 | std::shared_ptr getRawBody() override; 33 | std::shared_ptr getFormDataBody() override; 34 | BodyType getBodyType() override; 35 | 36 | void setFormDataBody(const std::shared_ptr& body) override; 37 | void setRawBody(const std::shared_ptr& body) override; 38 | void setBodyType(const BodyType& bodyType) override; 39 | 40 | private slots: 41 | void toggleBody(int changedIndex); 42 | void addDefaultElement(); 43 | void addDefaultFile(); 44 | void removeSelectedElement(); 45 | void removeSelectedFile(); 46 | 47 | private: 48 | Ui::BodyFragmentView* ui; 49 | 50 | const int keyIndex = 0; 51 | const int valueIndex = 1; 52 | const Qt::ItemFlags treeItemFlag = Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled; 53 | 54 | void addRowToTreeWidget(const std::string& key, const std::string& value, QTreeWidget* widget); 55 | std::map getRowsFromTreeWidget(QTreeWidget* widget) const; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/windows_executable.yml: -------------------------------------------------------------------------------- 1 | name: Build Windows executable 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | BUILD_TYPE: Release 10 | VCPKG_CMAKE_TOOLCHAIN: C:\vcpkg\scripts\buildsystems\vcpkg.cmake 11 | VCPKG_INCLUDE_DIR: C:\vcpkg\installed\x64-windows-static\include 12 | MSBUILD_PATH: C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\devenv.com 13 | 14 | jobs: 15 | build: 16 | name: Build Windows executable 17 | runs-on: windows-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Install Qt 22 | uses: jurplel/install-qt-action@v2.14.0 23 | - name: Install dependencies (Choco) 24 | run: choco install -y ninja cmake 25 | - name: Install dependencies (VCPKG) 26 | run: vcpkg install boost-system:x64-windows-static boost-format:x64-windows-static cpprestsdk:x64-windows-static nlohmann-json:x64-windows-static 27 | - name: System wide VCPKG integrations 28 | run: vcpkg integrate install 29 | - name: Add VCPKG and Qt installation path to PATH variable 30 | run: $Env:PATH += ';C:\vcpkg;${{env.VCPKG_INCLUDE_DIR}};${{env.Qt5_Dir}}' 31 | - name: Create GetIt solution 32 | run: cmake . -G 'Visual Studio 17 2022' -Dpackaging=true -Duse_installed_dependencies=true -DCMAKE_CXX_FLAGS='/w' -DCMAKE_PREFIX_PATH=${{env.Qt5_Dir}} -DCMAKE_TOOLCHAIN_FILE=${{env.VCPKG_CMAKE_TOOLCHAIN}} -DVCPKG_TARGET_TRIPLET='x64-windows-static' -DBoost_INCLUDE_DIR=${{env.VCPKG_INCLUDE_DIR}} 33 | - name: Build GetIt 34 | shell: cmd 35 | run: "%MSBUILD_PATH% GetIt.sln /Build" 36 | - name: Add Qt dependencies to built executable 37 | run: | 38 | $windeployqt = '${{env.Qt5_Dir}}\bin\windeployqt.exe' 39 | & $windeployqt ./bin/GetIt.exe 40 | - name: Zip GetIt executable 41 | run: 7z a -tzip getit.zip ./bin/GetIt.exe 42 | - name: Upload Windows executable as build artifact 43 | uses: actions/upload-artifact@v3.0.0 44 | with: 45 | name: GetIt-Windows-x64.zip 46 | path: ./getit.zip 47 | - name: Upload binaries to release 48 | uses: svenstaro/upload-release-action@v2 49 | with: 50 | repo_token: ${{ secrets.GITHUB_TOKEN }} 51 | file: ./getit.zip 52 | asset_name: GetIt-Windows-x64.zip 53 | tag: ${{ github.ref }} 54 | -------------------------------------------------------------------------------- /tests/domain/implementations/tst_RawRequestBody.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "domain/implementations/RawRequestBody.hpp" 6 | 7 | using namespace getit::domain::implementations; 8 | 9 | TEST_CASE("RawRequestBody.getContentType") 10 | { 11 | SECTION("returns the Content-Type set through the constructor") 12 | { 13 | // Arrange 14 | const auto& contentType = "application/json"; 15 | const auto& request = std::make_shared(contentType); 16 | 17 | // Act 18 | const auto& result = request->getContentType(); 19 | 20 | // Assert 21 | REQUIRE(contentType == result); 22 | } 23 | 24 | SECTION("returns the default Content-Type when the empty constructor is called") 25 | { 26 | // Arrange 27 | const auto& defaultContentType = "text/plain"; 28 | const auto& request = std::make_shared(); 29 | 30 | // Act 31 | const auto& result = request->getContentType(); 32 | 33 | // Assert 34 | REQUIRE(defaultContentType == result); 35 | } 36 | } 37 | 38 | TEST_CASE("RawRequestBody.getBody") 39 | { 40 | SECTION("returns the direct string given through setBody") 41 | { 42 | // Arrange 43 | const auto& body = "This is my exact body"; 44 | const auto& request = std::make_shared(); 45 | 46 | // Act 47 | request->setBody(body); 48 | const auto& result = request->getBody(); 49 | 50 | // Assert 51 | REQUIRE(body == result); 52 | } 53 | 54 | SECTION("returns the string including linebreaks and tabs exactly as given through setBody") 55 | { 56 | // Arrange 57 | const auto& body = "This is my body\nWith multiple lines\tand a tab for some\treason..."; 58 | const auto& request = std::make_shared(); 59 | 60 | // Act 61 | request->setBody(body); 62 | const auto& result = request->getBody(); 63 | 64 | // Assert 65 | REQUIRE(body == result); 66 | } 67 | 68 | SECTION("returns an empty string when the setBody is never called") 69 | { 70 | // Arrange 71 | const auto& request = std::make_shared(); 72 | 73 | // Act 74 | const auto& result = request->getBody(); 75 | 76 | // Assert 77 | REQUIRE(result.empty()); 78 | } 79 | } -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | find_package(Boost COMPONENTS thread system chrono REQUIRED) 4 | 5 | # Copy test_file.txt to current working directory 6 | file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/res/test_file.txt 7 | DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) 8 | 9 | set(SOURCEFILES 10 | ../src/data/factories/RequestRepositoryFactory.cpp 11 | ../src/data/repositories/FormDataRequestRepository.cpp 12 | ../src/data/repositories/RawRequestRepository.cpp 13 | ../src/data/repositories/RequestRepository.cpp 14 | 15 | ../src/domain/factories/RequestFactory.cpp 16 | ../src/domain/implementations/FormDataRequestBody.cpp 17 | ../src/domain/implementations/RawRequestBody.cpp 18 | ../src/domain/models/Request.cpp 19 | 20 | ../src/presentation/fragments/BodyFragment/BodyFragmentController.cpp 21 | ../src/presentation/fragments/HeadersFragment/HeadersFragmentController.cpp 22 | ../src/presentation/fragments/ResponseFragment/ResponseFragmentController.cpp 23 | 24 | ../src/presentation/windows/MainWindowViewModel.cpp 25 | 26 | ../src/service/factories/RequestServiceFactory.cpp 27 | ../src/service/implementations/CppRestRequestService.cpp 28 | ) 29 | 30 | set(TESTFILES 31 | main.cpp 32 | 33 | data/factories/tst_RequestRepositoryFactory.cpp 34 | data/repositories/tst_FormDataRequestRepository.cpp 35 | data/repositories/tst_RawRequestRepository.cpp 36 | data/repositories/tst_RequestRepository.cpp 37 | 38 | domain/factories/tst_RequestFactory.cpp 39 | domain/implementations/tst_FormDataRequestBody.cpp 40 | domain/implementations/tst_RawRequestBody.cpp 41 | 42 | presentation/fragments/tst_BodyFragmentController.cpp 43 | presentation/fragments/tst_HeadersFragmentController.cpp 44 | presentation/fragments/tst_ResponseFragmentController.cpp 45 | 46 | presentation/windows/tst_MainWindowViewModel.cpp 47 | 48 | presentation/highlighters/tst_JsonSyntaxHighlighterRule.cpp 49 | 50 | service/factories/tst_RequestServiceFactory.cpp 51 | service/implementations/tst_CppRestRequestService.cpp 52 | ) 53 | 54 | add_executable(getit_tests ${SOURCEFILES} ${TESTFILES}) 55 | 56 | include_directories(../src) 57 | 58 | target_link_libraries(getit_tests Boost::system) 59 | target_link_libraries(getit_tests cpprest) 60 | target_link_libraries(getit_tests nlohmann_json::nlohmann_json) 61 | target_link_libraries(getit_tests trompeloeil Catch2::Catch2) 62 | 63 | enable_testing() 64 | add_test(NAME getit_tests COMMAND getit_tests) -------------------------------------------------------------------------------- /src/presentation/windows/MainWindowViewModel.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/windows/MainWindowViewModel.hpp" 2 | 3 | using namespace getit; 4 | using namespace getit::presentation::windows; 5 | 6 | MainWindowViewModel::MainWindowViewModel( 7 | std::shared_ptr requestFactory, 8 | std::shared_ptr requestServiceFactory, 9 | std::shared_ptr requestRepositoryFactory, 10 | std::shared_ptr view): 11 | requestFactory(std::move(requestFactory)), 12 | requestServiceFactory(std::move(requestServiceFactory)), 13 | requestRepositoryFactory(std::move(requestRepositoryFactory)), 14 | view(std::move(view)) 15 | { 16 | 17 | } 18 | 19 | void MainWindowViewModel::sendRequest(std::string method, std::string uri, std::map headers, 20 | std::shared_ptr body) 21 | { 22 | this->view->updateState(std::make_shared()); 23 | const auto& requestService = requestServiceFactory->getRequestService(); 24 | const auto& request = requestFactory->getRequest(method, uri, headers); 25 | request->setBody(body); 26 | 27 | try 28 | { 29 | auto response = requestService->send(request).get(); 30 | this->view->updateState(std::make_shared(response)); 31 | } 32 | catch(const std::exception& e) 33 | { 34 | this->view->updateState(std::make_shared(e.what())); 35 | } 36 | } 37 | 38 | void MainWindowViewModel::saveRequest(std::string method, std::string uri, std::map headers, 39 | std::shared_ptr body, const std::string& location) 40 | { 41 | const auto& repository = requestRepositoryFactory->getRepository(); 42 | const auto& request = requestFactory->getRequest(method, uri, headers); 43 | request->setBody(body); 44 | 45 | try 46 | { 47 | repository->saveRequest(location, request); 48 | } 49 | catch(const std::exception& e) 50 | { 51 | this->view->updateState(std::make_shared(e.what())); 52 | } 53 | } 54 | 55 | void MainWindowViewModel::openRequest(const std::string& location) 56 | { 57 | const auto& repository = requestRepositoryFactory->getRepository(); 58 | 59 | try 60 | { 61 | const auto& request = repository->loadRequest(location); 62 | view->updateState(std::make_shared(request)); 63 | } 64 | catch(const std::exception& e) { 65 | this->view->updateState(std::make_shared(e.what())); 66 | } 67 | } -------------------------------------------------------------------------------- /.github/workflows/package_tst.yml: -------------------------------------------------------------------------------- 1 | name: Package GetIt for all platforms 2 | on: 3 | create: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | package: 9 | strategy: 10 | matrix: 11 | include: 12 | - os: ubuntu-latest 13 | generator: Ninja 14 | openssl_root: /usr 15 | artifact_name: GetIt_x64-Linux.tar.gz 16 | 17 | - os: macos-latest 18 | generator: Ninja 19 | openssl_root: /usr/local/opt/openssl@3 20 | artifact_name: GetIt_x64-MacOS.zip 21 | 22 | - os: windows-latest 23 | generator: MinGW Makefiles 24 | openssl_root: D:\msys2\msys64\mingw64 25 | artifact_name: GetIt_x64-Windows.zip 26 | 27 | runs-on: ${{matrix.os}} 28 | steps: 29 | - name: Install Qt 30 | uses: jurplel/install-qt-action@v2 31 | with: 32 | arch: win64_mingw81 33 | 34 | - name: Setup msys 35 | if: matrix.os == 'windows-latest' 36 | uses: msys2/setup-msys2@v2 37 | with: 38 | location: ${{env.msys}} 39 | msystem: mingw64 40 | install: mingw-w64-x86_64-openssl openssl-dev mingw-w64-x86_64-boost 41 | 42 | - name: Brew install openssl 43 | if: matrix.os == 'macos-latest' 44 | run: brew install openssl boost 45 | 46 | - name: Checkout repository 47 | uses: actions/checkout@v2 48 | with: 49 | fetch-depth: 0 50 | 51 | - name: Build GetIt 52 | run: cmake . -G ${{matrix.generator}} -Dpackaging=true -DCMAKE_CXX_FLAGS='-w' 53 | 54 | - name: Add Qt dependencies to Windows executable and zip the executable 55 | if: matrix.os == 'windows-latest' 56 | run: | 57 | $windeployqt = '${{env.Qt5_Dir}}\bin\windeployqt.exe' 58 | & $windeployqt ./bin/GetIt.exe 59 | 7z a -tzip ${{matrix.artifact_name}} ./bin/GetIt.exe 60 | 61 | - name: Add Qt dependencies to MacOS bundle and zip the bundle 62 | if: matrix.os == 'macos-latest' 63 | run: | 64 | macdeployqt ./bin/GetIt.app -timestamp -libpath=./_deps 65 | cp ./packaging/macos/icons/getit.icns ./bin/GetIt.app/Contents/Resources 66 | zip -rT ${{matrix.artifact_name}} ./bin/GetIt.app >> /dev/null 67 | 68 | - name: Upload zip-file as build artifact 69 | uses: ctions/upload-release-asset@1 70 | env: 71 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 72 | with: 73 | upload:url: https://github.com/bartkessels/GetIt/releases/latest 74 | asset_path: ${{matrix.artifact_name}} 75 | asset_name: ${{matrix.artifact_name}} 76 | asset_content_type: application/zip 77 | -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/ResponseFragmentView.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/fragments/ResponseFragment/ResponseFragmentView.hpp" 2 | #include "./ui_ResponseFragmentView.h" 3 | 4 | using namespace getit::presentation::fragments; 5 | 6 | ResponseFragmentView::ResponseFragmentView(QWidget* parent): 7 | QWidget(parent), 8 | ui(new Ui::ResponseFragmentView()) 9 | { 10 | ui->setupUi(this); 11 | syntaxHighlighter = new highlighters::SyntaxHighlighter(ui->body->document()); 12 | connect(ui->contentType, QOverload::of(&QComboBox::currentIndexChanged), this, &ResponseFragmentView::setContentType); 13 | } 14 | 15 | ResponseFragmentView::~ResponseFragmentView() 16 | { 17 | delete ui; 18 | delete syntaxHighlighter; 19 | } 20 | 21 | std::map ResponseFragmentView::getHeaders() 22 | { 23 | std::map headers; 24 | 25 | for (int i = 0; i < ui->headers->topLevelItemCount(); ++i) { 26 | QTreeWidgetItem* itm = ui->headers->topLevelItem(i); 27 | 28 | headers.insert_or_assign( 29 | itm->text(keyIndex).toStdString(), 30 | itm->text(valueIndex).toStdString() 31 | ); 32 | } 33 | 34 | return headers; 35 | } 36 | 37 | std::string ResponseFragmentView::getBody() 38 | { 39 | return ui->body->document()->toPlainText().toStdString(); 40 | } 41 | 42 | void ResponseFragmentView::setHeaders(const std::map& headers) 43 | { 44 | for (const auto& [header, value] : headers) { 45 | addHeader(header, value); 46 | } 47 | } 48 | 49 | void ResponseFragmentView::clearHeaders() 50 | { 51 | ui->headers->clear(); 52 | } 53 | 54 | void ResponseFragmentView::setBody(const std::string& body) 55 | { 56 | ui->body->document()->setPlainText(QString::fromStdString(body)); 57 | } 58 | 59 | void ResponseFragmentView::addHeader(const std::string& header, const std::string& value) 60 | { 61 | const auto& row = new QTreeWidgetItem(ui->headers); 62 | 63 | row->setText(keyIndex, QString::fromStdString(header)); 64 | row->setText(valueIndex, QString::fromStdString(value)); 65 | row->setFlags(treeItemFlag); 66 | } 67 | 68 | void ResponseFragmentView::setContentType(int selectedIndex) 69 | { 70 | switch(selectedIndex) 71 | { 72 | case ContentType::JSON: 73 | syntaxHighlighter->startHighlighting(highlighters::JsonSyntaxHighlighterRule::rules); 74 | break; 75 | case ContentType::XML: 76 | syntaxHighlighter->startHighlighting(highlighters::XmlSyntaxHighlighterRule::rules); 77 | break; 78 | default: 79 | syntaxHighlighter->stopHighlighting(); 80 | break; 81 | } 82 | } -------------------------------------------------------------------------------- /src/presentation/windows/MainWindow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "domain/models/Request.hpp" 14 | #include "domain/models/Response.hpp" 15 | 16 | #include "presentation/fragments/BodyFragment/BodyFragmentController.hpp" 17 | #include "presentation/fragments/BodyFragment/BodyFragmentView.hpp" 18 | #include "presentation/fragments/HeadersFragment//HeadersFragmentController.hpp" 19 | #include "presentation/fragments/HeadersFragment/HeadersFragmentView.hpp" 20 | #include "presentation/fragments/ResponseFragment/ResponseFragmentController.hpp" 21 | #include "presentation/fragments/ResponseFragment/ResponseFragmentView.hpp" 22 | 23 | #include "presentation/states/Error.hpp" 24 | #include "presentation/states/FileOpened.hpp" 25 | #include "presentation/states/RequestState.hpp" 26 | #include "presentation/states/Sending.hpp" 27 | #include "presentation/states/Sent.hpp" 28 | 29 | #include "presentation/windows/IMainWindow.hpp" 30 | #include "presentation/windows/IMainWindowViewModel.hpp" 31 | 32 | QT_BEGIN_NAMESPACE 33 | namespace Ui { class MainWindow; } 34 | QT_END_NAMESPACE 35 | 36 | namespace getit::presentation::windows 37 | { 38 | class MainWindow: public QMainWindow, public IMainWindow 39 | { 40 | Q_OBJECT 41 | 42 | public: 43 | explicit MainWindow(QWidget* parent = nullptr); 44 | ~MainWindow() override; 45 | 46 | void setViewModel(std::shared_ptr viewModel) override; 47 | void updateState(std::shared_ptr state) override; 48 | 49 | signals: 50 | void responseReceived(std::shared_ptr response); 51 | void errorOccurred(const std::string& errorMessage); 52 | 53 | private: 54 | Ui::MainWindow* ui; 55 | std::shared_ptr viewModel; 56 | std::string saveLocation; 57 | 58 | std::shared_ptr bodyController; 59 | std::shared_ptr headersController; 60 | std::shared_ptr responseController; 61 | 62 | void setRequest(const std::shared_ptr& request); 63 | void setResponse(const std::shared_ptr& response); 64 | 65 | void sendRequest(); 66 | void registerControllers(); 67 | void saveRequest(); 68 | void saveRequestAs(); 69 | void openRequest(); 70 | void displayErrorMessage(const std::string& errorMessage); 71 | }; 72 | } -------------------------------------------------------------------------------- /src/domain/implementations/FormDataRequestBody.cpp: -------------------------------------------------------------------------------- 1 | #include "domain/implementations/FormDataRequestBody.hpp" 2 | 3 | using namespace getit::domain::implementations; 4 | 5 | FormDataRequestBody::FormDataRequestBody(std::string boundary): 6 | boundary(std::move(boundary)) 7 | { 8 | 9 | } 10 | 11 | void FormDataRequestBody::addElement(std::string key, std::string value) 12 | { 13 | this->elements.insert({key, value}); 14 | } 15 | 16 | void FormDataRequestBody::addFile(std::string key, std::string filePath) 17 | { 18 | this->files.insert({key, filePath}); 19 | } 20 | 21 | void FormDataRequestBody::setBoundary(std::string boundary) 22 | { 23 | this->boundary = boundary; 24 | } 25 | 26 | void FormDataRequestBody::setElements(std::map elements) 27 | { 28 | this->elements = elements; 29 | } 30 | 31 | void FormDataRequestBody::setFiles(std::map files) 32 | { 33 | this->files = files; 34 | } 35 | 36 | std::string FormDataRequestBody::getBoundary() 37 | { 38 | return this->boundary; 39 | } 40 | 41 | std::map FormDataRequestBody::getElements() 42 | { 43 | return this->elements; 44 | } 45 | 46 | std::map FormDataRequestBody::getFiles() 47 | { 48 | return this->files; 49 | } 50 | 51 | std::string FormDataRequestBody::getBody() 52 | { 53 | const auto& elementsBody = this->getElementsBody(); 54 | const auto& filesBody = this->getFilesBody(); 55 | 56 | boost::format frmt = boost::format( 57 | BODY_WITH_BOUNDARY_TEMPLATE 58 | ) % elementsBody % filesBody % this->boundary; 59 | 60 | return frmt.str(); 61 | } 62 | 63 | std::string FormDataRequestBody::getContentType() 64 | { 65 | boost::format frmt = boost::format( 66 | CONTENT_TYPE_TEMPLATE 67 | ) % this->boundary; 68 | 69 | return frmt.str(); 70 | } 71 | 72 | std::string FormDataRequestBody::getElementsBody() 73 | { 74 | std::string body; 75 | 76 | for (const auto& [key, value] : this->elements) { 77 | boost::format frmt = boost::format( 78 | ELEMENT_TEMPLATE 79 | ) % this->boundary % key % value; 80 | 81 | body += frmt.str(); 82 | } 83 | 84 | return body; 85 | } 86 | 87 | std::string FormDataRequestBody::getFilesBody() 88 | { 89 | std::string body; 90 | 91 | for (const auto& [key, filePath] : this->files) { 92 | 93 | std::ifstream inputFileStream(filePath); 94 | std::string fileContents( 95 | (std::istreambuf_iterator(inputFileStream)), 96 | (std::istreambuf_iterator()) 97 | ); 98 | 99 | boost::format frmt = boost::format( 100 | FILE_TEMPLATE 101 | ) % this->boundary % key % filePath % fileContents; 102 | 103 | body += frmt.str(); 104 | } 105 | 106 | return body; 107 | } -------------------------------------------------------------------------------- /tests/presentation/fragments/tst_BodyFragmentController.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "presentation/fragments/BodyFragment/BodyFragmentController.hpp" 6 | #include "presentation/fragments/BodyFragment/IBodyFragmentView.hpp" 7 | 8 | using namespace getit; 9 | using namespace getit::domain::implementations; 10 | using namespace getit::presentation::fragments; 11 | 12 | class BodyFragmentViewMock: public IBodyFragmentView 13 | { 14 | public: 15 | MAKE_MOCK0(getRawBody, std::shared_ptr(), override); 16 | MAKE_MOCK0(getFormDataBody, std::shared_ptr(), override); 17 | MAKE_MOCK0(getBodyType, BodyType(), override); 18 | MAKE_MOCK1(setFormDataBody, void(const std::shared_ptr&), override); 19 | MAKE_MOCK1(setRawBody, void(const std::shared_ptr&), override); 20 | MAKE_MOCK1(setBodyType, void(const BodyType&), override); 21 | }; 22 | 23 | TEST_CASE("BodyFragmentController.getContent") 24 | { 25 | // Before 26 | auto view = new BodyFragmentViewMock(); 27 | const auto& sut = std::make_shared(view); 28 | 29 | SECTION("returns the formdata body from the view when the view returns FORM_DATA as the body type") 30 | { 31 | // Arrange 32 | const auto& body = std::make_shared(); 33 | const auto& bodyType = BodyType::FORM_DATA; 34 | 35 | ALLOW_CALL(*view, getBodyType()).RETURN(bodyType); 36 | ALLOW_CALL(*view, getFormDataBody()).RETURN(body); 37 | 38 | // Act 39 | const auto& actual = sut->getContent(); 40 | 41 | // Assert 42 | REQUIRE(std::dynamic_pointer_cast(actual)); 43 | } 44 | 45 | SECTION("returns the raw body from the view when the view returns RAW as the body type") 46 | { 47 | // Arrange 48 | const auto& body = std::make_shared(); 49 | const auto& bodyType = BodyType::RAW; 50 | 51 | ALLOW_CALL(*view, getBodyType()).RETURN(bodyType); 52 | ALLOW_CALL(*view, getRawBody()).RETURN(body); 53 | 54 | // Act 55 | const auto& actual = sut->getContent(); 56 | 57 | // Assert 58 | REQUIRE(std::dynamic_pointer_cast(actual)); 59 | } 60 | } 61 | 62 | TEST_CASE("BodyFragmentController.setContent") 63 | { 64 | // Before 65 | auto view = new BodyFragmentViewMock(); 66 | const auto& sut = std::make_shared(view); 67 | 68 | SECTION("calls setBody with the formData body when the implementation is FormDataRequestBody") 69 | { 70 | // Arrange 71 | const auto& body = std::make_shared(); 72 | 73 | // Assert 74 | REQUIRE_CALL(*view, setFormDataBody(body)); 75 | 76 | // Act 77 | sut->setContent(body); 78 | } 79 | 80 | SECTION("calls setBody with the raw body when the implementation is RawRequestBody") 81 | { 82 | // Arrange 83 | const auto& body = std::make_shared(); 84 | 85 | // Assert 86 | REQUIRE_CALL(*view, setRawBody(body)); 87 | 88 | // Act 89 | sut->setContent(body); 90 | } 91 | } -------------------------------------------------------------------------------- /src/data/repositories/FormDataRequestRepository.cpp: -------------------------------------------------------------------------------- 1 | #include "data/repositories/FormDataRequestRepository.hpp" 2 | 3 | using namespace getit::data::repositories; 4 | #include 5 | FormDataRequestRepository::FormDataRequestRepository(std::shared_ptr factory): 6 | factory(std::move(factory)) 7 | { 8 | 9 | } 10 | 11 | void FormDataRequestRepository::saveRequest(const std::string& filePath, std::shared_ptr request) 12 | { 13 | const auto& requestBody = std::dynamic_pointer_cast(request->getBody()); 14 | auto jsonObject = nlohmann::json::object(); 15 | 16 | jsonObject[METHOD_NAME] = request->getMethod(); 17 | jsonObject[URI_NAME] = request->getUri(); 18 | jsonObject[HEADERS_NAME] = nlohmann::json::array(); 19 | 20 | jsonObject[FORM_DATA_BODY_TYPE_NAME] = nlohmann::json::object(); 21 | jsonObject[FORM_DATA_BODY_TYPE_NAME][BOUNDARY_NAME] = requestBody->getBoundary(); 22 | jsonObject[FORM_DATA_BODY_TYPE_NAME][ELEMENTS_ARRAY_NAME] = nlohmann::json::array(); 23 | jsonObject[FORM_DATA_BODY_TYPE_NAME][FILES_ARRAY_NAME] = nlohmann::json::array(); 24 | 25 | for (const auto& [key, value] : request->getHeaders()) { 26 | auto header = nlohmann::json::object(); 27 | header[HEADER_NAME] = key; 28 | header[HEADER_VALUE] = value; 29 | jsonObject[HEADERS_NAME].push_back(header); 30 | } 31 | 32 | for (const auto& [key, value] : requestBody->getElements()) { 33 | auto element = nlohmann::json::object(); 34 | element[KEY_NAME] = key; 35 | element[ELEMENT_VALUE_NAME] = value; 36 | jsonObject[FORM_DATA_BODY_TYPE_NAME][ELEMENTS_ARRAY_NAME].push_back(element); 37 | } 38 | 39 | for (const auto& [key, path] : requestBody->getFiles()) { 40 | auto file = nlohmann::json::object(); 41 | file[KEY_NAME] = key; 42 | file[FILE_PATH_NAME] = path; 43 | jsonObject[FORM_DATA_BODY_TYPE_NAME][FILES_ARRAY_NAME].push_back(file); 44 | } 45 | 46 | std::ofstream output(filePath); 47 | output << std::setw(4) << jsonObject << std::endl; 48 | output.close(); 49 | } 50 | 51 | std::shared_ptr FormDataRequestRepository::loadRequest(const std::string& filePath) 52 | { 53 | auto jsonObject = nlohmann::json::object(); 54 | 55 | std::ifstream input(filePath); 56 | input >> jsonObject; 57 | input.close(); 58 | 59 | const auto& method = jsonObject[METHOD_NAME]; 60 | const auto& uri = jsonObject[URI_NAME]; 61 | const auto& boundary = jsonObject[FORM_DATA_BODY_TYPE_NAME][BOUNDARY_NAME]; 62 | 63 | std::map headers; 64 | std::map elements; 65 | std::map files; 66 | 67 | for (auto obj : jsonObject[HEADERS_NAME]) { 68 | headers.emplace(obj[HEADER_NAME], obj[HEADER_VALUE]); 69 | } 70 | 71 | for (auto obj : jsonObject[FORM_DATA_BODY_TYPE_NAME][ELEMENTS_ARRAY_NAME]) { 72 | elements.emplace(obj[KEY_NAME], obj[ELEMENT_VALUE_NAME]); 73 | } 74 | 75 | for (auto obj : jsonObject[FORM_DATA_BODY_TYPE_NAME][FILES_ARRAY_NAME]) { 76 | files.emplace(obj[KEY_NAME], obj[FILE_PATH_NAME]); 77 | } 78 | 79 | return this->factory->getRequest(method, uri, headers, elements, files, boundary); 80 | } -------------------------------------------------------------------------------- /src/presentation/fragments/HeadersFragment/HeadersFragmentView.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | HeadersFragmentView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 617 10 | 426 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | true 21 | 22 | 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | true 30 | 31 | 32 | 33 | Header 34 | 35 | 36 | 37 | 38 | Value 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Qt::Horizontal 49 | 50 | 51 | 52 | 40 53 | 20 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 0 63 | 0 64 | 65 | 66 | 67 | 68 | 30 69 | 16777215 70 | 71 | 72 | 73 | - 74 | 75 | 76 | Backspace 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 0 85 | 0 86 | 87 | 88 | 89 | 90 | 30 91 | 16777215 92 | 93 | 94 | 95 | + 96 | 97 | 98 | A 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/presentation/fragments/ResponseFragment/ResponseFragmentView.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ResponseFragmentView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 741 10 | 465 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Qt::Vertical 21 | 22 | 23 | 24 | Qt::ScrollBarAsNeeded 25 | 26 | 27 | QAbstractScrollArea::AdjustToContents 28 | 29 | 30 | QAbstractItemView::NoEditTriggers 31 | 32 | 33 | false 34 | 35 | 36 | true 37 | 38 | 39 | true 40 | 41 | 42 | true 43 | 44 | 45 | 46 | Header 47 | 48 | 49 | 50 | 51 | Value 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Content Type 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Plain 71 | 72 | 73 | 74 | 75 | JSON 76 | 77 | 78 | 79 | 80 | XML 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | QTextEdit::NoWrap 91 | 92 | 93 | true 94 | 95 | 96 | Send a request to view the response body 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/presentation/fragments/BodyFragment/BodyFragmentView.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/fragments/BodyFragment/BodyFragmentView.hpp" 2 | #include "./ui_BodyFragmentView.h" 3 | 4 | using namespace getit; 5 | using namespace getit::presentation::fragments; 6 | 7 | BodyFragmentView::BodyFragmentView(QWidget* parent): 8 | QWidget(parent), 9 | ui(new Ui::BodyFragmentView()) 10 | { 11 | ui->setupUi(this); 12 | toggleBody(BodyType::FORM_DATA); 13 | 14 | connect(ui->bodyType, QOverload::of(&QComboBox::currentIndexChanged), this, &BodyFragmentView::toggleBody); 15 | connect(ui->addElement, &QPushButton::pressed, this, &BodyFragmentView::addDefaultElement); 16 | connect(ui->removeElement, &QPushButton::pressed, this, &BodyFragmentView::removeSelectedElement); 17 | connect(ui->addFile, &QPushButton::pressed, this, &BodyFragmentView::addDefaultFile); 18 | connect(ui->removeFile, &QPushButton::pressed, this, &BodyFragmentView::removeSelectedFile); 19 | } 20 | 21 | BodyFragmentView::~BodyFragmentView() 22 | { 23 | delete ui; 24 | } 25 | 26 | std::shared_ptr BodyFragmentView::getFormDataBody() 27 | { 28 | const auto& body = std::make_shared(); 29 | 30 | body->setElements(getRowsFromTreeWidget(ui->elements)); 31 | body->setFiles(getRowsFromTreeWidget(ui->files)); 32 | 33 | return body; 34 | } 35 | 36 | std::shared_ptr BodyFragmentView::getRawBody() 37 | { 38 | const auto& body = std::make_shared(); 39 | 40 | body->setContentType(ui->contentType->text().toStdString()); 41 | body->setBody(ui->raw->document()->toPlainText().toStdString()); 42 | 43 | return body; 44 | } 45 | 46 | void BodyFragmentView::setFormDataBody(const std::shared_ptr& body) 47 | { 48 | for (const auto& [element, value] : body->getElements()) { 49 | addRowToTreeWidget(element, value, ui->elements); 50 | } 51 | 52 | for (const auto& [file, filePath] : body->getFiles()) { 53 | addRowToTreeWidget(file, filePath, ui->files); 54 | } 55 | } 56 | 57 | void BodyFragmentView::setRawBody(const std::shared_ptr& body) 58 | { 59 | ui->contentType->setText(QString::fromStdString(body->getContentType())); 60 | ui->raw->setPlainText(QString::fromStdString(body->getBody())); 61 | } 62 | 63 | BodyType BodyFragmentView::getBodyType() 64 | { 65 | if (ui->bodyType->currentIndex() == BodyType::RAW) 66 | return BodyType::RAW; 67 | return BodyType::FORM_DATA; 68 | } 69 | 70 | void BodyFragmentView::setBodyType(const BodyType& bodyType) 71 | { 72 | ui->bodyType->setCurrentIndex(bodyType); 73 | } 74 | 75 | void BodyFragmentView::toggleBody(int changedIndex) 76 | { 77 | // Show form data view by default 78 | ui->formdataWidget->show(); 79 | ui->rawWidget->hide(); 80 | 81 | if (changedIndex == BodyType::RAW) { 82 | ui->formdataWidget->hide(); 83 | ui->rawWidget->show(); 84 | } 85 | } 86 | 87 | void BodyFragmentView::addDefaultElement() 88 | { 89 | addRowToTreeWidget("Key", "Value", ui->elements); 90 | } 91 | 92 | void BodyFragmentView::removeSelectedElement() 93 | { 94 | delete ui->elements->currentItem(); 95 | } 96 | 97 | void BodyFragmentView::addDefaultFile() 98 | { 99 | const auto& filePath = QFileDialog::getOpenFileUrl(this); 100 | 101 | if (!filePath.isEmpty()) { 102 | addRowToTreeWidget("File", filePath.toLocalFile().toStdString(), ui->files); 103 | } 104 | } 105 | 106 | void BodyFragmentView::removeSelectedFile() 107 | { 108 | delete ui->files->currentItem(); 109 | } 110 | 111 | void BodyFragmentView::addRowToTreeWidget(const std::string &key, const std::string &value, QTreeWidget* widget) 112 | { 113 | const auto& row = new QTreeWidgetItem(widget); 114 | 115 | row->setText(keyIndex, QString::fromStdString(key)); 116 | row->setText(valueIndex, QString::fromStdString(value)); 117 | row->setFlags(treeItemFlag); 118 | } 119 | 120 | std::map BodyFragmentView::getRowsFromTreeWidget(QTreeWidget* widget) const 121 | { 122 | std::map rows; 123 | 124 | for (int i = 0; i < widget->topLevelItemCount(); ++i) { 125 | QTreeWidgetItem* itm = widget->topLevelItem(i); 126 | 127 | rows.insert_or_assign( 128 | itm->text(keyIndex).toStdString(), 129 | itm->text(valueIndex).toStdString() 130 | ); 131 | } 132 | 133 | return rows; 134 | } 135 | -------------------------------------------------------------------------------- /src/presentation/windows/MainWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "presentation/windows/MainWindow.hpp" 2 | #include "./ui_MainWindow.h" 3 | 4 | using namespace getit::presentation; 5 | using namespace getit::presentation::windows; 6 | 7 | MainWindow::MainWindow(QWidget* parent): 8 | QMainWindow(parent), 9 | ui(new Ui::MainWindow()) 10 | { 11 | this->ui->setupUi(this); 12 | this->registerControllers(); 13 | 14 | connect(ui->send, &QPushButton::pressed, this, &MainWindow::sendRequest); 15 | connect(ui->menuItemSave, &QAction::triggered, this, &MainWindow::saveRequest); 16 | connect(ui->menuItemSaveAs, &QAction::triggered, this, &MainWindow::saveRequest); 17 | connect(ui->menuItemOpen, &QAction::triggered, this, &MainWindow::openRequest); 18 | connect(this, &MainWindow::responseReceived, this, [this](auto response) { 19 | responseController->setContent(response); 20 | ui->tabs->setCurrentIndex(ui->tabs->count() - 1); 21 | }); 22 | connect(this, &MainWindow::errorOccurred, this, &MainWindow::displayErrorMessage); 23 | } 24 | 25 | MainWindow::~MainWindow() 26 | { 27 | delete this->ui; 28 | } 29 | 30 | void MainWindow::setViewModel(std::shared_ptr viewModel) 31 | { 32 | this->viewModel = viewModel; 33 | } 34 | 35 | void MainWindow::updateState(std::shared_ptr state) 36 | { 37 | if (const auto& error = std::dynamic_pointer_cast(state)) 38 | { 39 | emit errorOccurred(error->message); 40 | } 41 | else if (const auto& fileOpened = std::dynamic_pointer_cast(state)) 42 | { 43 | emit setRequest(fileOpened->request); 44 | } 45 | else if (std::dynamic_pointer_cast(state)) 46 | { 47 | // Loading 48 | } 49 | else if (const auto& sent = std::dynamic_pointer_cast(state)) 50 | { 51 | emit setResponse(sent->response); 52 | } 53 | } 54 | 55 | void MainWindow::registerControllers() 56 | { 57 | auto bodyView = new fragments::BodyFragmentView(); 58 | auto headersView = new fragments::HeadersFragmentView(); 59 | auto responseView = new fragments::ResponseFragmentView(); 60 | 61 | bodyController = std::make_shared(bodyView); 62 | headersController = std::make_shared(headersView); 63 | responseController = std::make_shared(responseView); 64 | 65 | ui->tabs->addTab(bodyView, "Body"); 66 | ui->tabs->addTab(headersView, "Headers"); 67 | ui->tabs->addTab(responseView, "Response"); 68 | } 69 | 70 | void MainWindow::sendRequest() 71 | { 72 | const auto& method = ui->method->currentText().toStdString(); 73 | const auto& uri = ui->uri->text().toStdString(); 74 | const auto& headers = headersController->getContent(); 75 | const auto& body = bodyController->getContent(); 76 | 77 | this->viewModel->sendRequest(method, uri, headers, body); 78 | } 79 | 80 | void MainWindow::setRequest(const std::shared_ptr& request) 81 | { 82 | ui->method->setCurrentText(QString::fromStdString(request->getMethod())); 83 | ui->uri->setText(QString::fromStdString(request->getUri())); 84 | headersController->setContent(request->getHeaders()); 85 | bodyController->setContent(request->getBody()); 86 | } 87 | 88 | void MainWindow::setResponse(const std::shared_ptr& response) 89 | { 90 | responseController->setContent(response); 91 | } 92 | 93 | void MainWindow::saveRequest() 94 | { 95 | if (saveLocation.empty()) { 96 | const auto& filePath = QFileDialog::getSaveFileUrl(this, "Save GetIt Request", QUrl(), "*.getit"); 97 | 98 | if (filePath.isEmpty()) 99 | return; 100 | 101 | saveLocation = filePath.toLocalFile().toStdString(); 102 | } 103 | 104 | const auto& method = ui->method->currentText().toStdString(); 105 | const auto& uri = ui->uri->text().toStdString(); 106 | const auto& headers = headersController->getContent(); 107 | const auto& body = bodyController->getContent(); 108 | 109 | this->viewModel->saveRequest(method, uri, headers, body, saveLocation); 110 | } 111 | 112 | void MainWindow::openRequest() 113 | { 114 | const auto& filePath = QFileDialog::getOpenFileUrl(this, "Open GetIt Request", QUrl(), "*.getit"); 115 | 116 | if (filePath.isEmpty()) 117 | return; 118 | 119 | saveLocation = filePath.toLocalFile().toStdString(); 120 | this->viewModel->openRequest(saveLocation); 121 | } 122 | 123 | void MainWindow::displayErrorMessage(const std::string& errorMessage) 124 | { 125 | QMessageBox::warning(this, windowTitle(), QString::fromStdString(errorMessage)); 126 | } -------------------------------------------------------------------------------- /tests/domain/factories/tst_RequestFactory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "domain/factories/RequestFactory.hpp" 7 | #include "domain/implementations/FormDataRequestBody.hpp" 8 | #include "domain/implementations/RawRequestBody.hpp" 9 | 10 | using namespace getit::domain::factories; 11 | 12 | TEST_CASE("RequestFactory.getRequest [RawRequestBody - body overload]") 13 | { 14 | const auto& factory = std::make_shared(); 15 | const auto& method = "POST"; 16 | const auto& uri = "https://github.com/bartkessels"; 17 | const auto& headers = std::map(); 18 | 19 | SECTION("returns an instance of RawRequestBody when using the overload with body") 20 | { 21 | // Arrange 22 | const auto& body = "This is my raw body"; 23 | const auto& contentType = "text/plain"; 24 | 25 | // Act 26 | const auto& result = factory->getRequest(method, uri, headers, body, contentType); 27 | 28 | // Assert 29 | REQUIRE(std::dynamic_pointer_cast(result->getBody())); 30 | } 31 | 32 | SECTION("returns an instance of RawRequestBody with the correct values set for just the body") 33 | { 34 | // Arrange 35 | const auto& body = "This is my raw body"; 36 | 37 | // Act 38 | const auto& result = factory->getRequest(method, uri, headers, body); 39 | const auto& requestBody = std::dynamic_pointer_cast(result->getBody()); 40 | 41 | // Assert 42 | REQUIRE(body == requestBody->getBody()); 43 | } 44 | } 45 | 46 | TEST_CASE("RequestFactory.getRequest [RawRequestBody - body/contentType overload") 47 | { 48 | const auto& factory = std::make_shared(); 49 | const auto& method = "POST"; 50 | const auto& uri = "https://github.com/bartkessels"; 51 | const auto& headers = std::map(); 52 | 53 | SECTION("returns an instance of RawRequestBody when using the overload with body and contentType") 54 | { 55 | // Arrange 56 | const auto& body = "This is my raw body"; 57 | const auto& contentType = "text/plain"; 58 | 59 | // Act 60 | const auto& result = factory->getRequest(method, uri, headers, body, contentType); 61 | 62 | // Assert 63 | REQUIRE(std::dynamic_pointer_cast(result->getBody())); 64 | } 65 | 66 | SECTION("returns an instance of RawRequestBody with the correct values set") 67 | { 68 | // Arrange 69 | const auto& body = "This is my raw body"; 70 | const auto& contentType = "text/plain"; 71 | 72 | // Act 73 | const auto& result = factory->getRequest(method, uri, headers, body, contentType); 74 | const auto& requestBody = std::dynamic_pointer_cast(result->getBody()); 75 | 76 | // Assert 77 | REQUIRE(body == requestBody->getBody()); 78 | REQUIRE(contentType == requestBody->getContentType()); 79 | } 80 | } 81 | 82 | TEST_CASE("RequestFactory.getRequest [FormDataRequestBody]") 83 | { 84 | const auto& factory = std::make_shared(); 85 | const auto& method = "POST"; 86 | const auto& uri = "https://github.com/bartkessels"; 87 | const auto& headers = std::map(); 88 | 89 | SECTION("returns an instance of FormDataRequestBody when using the overload with elements, files and boundary") 90 | { 91 | // Arrange 92 | std::map elements = { { "username", "bartkessels" } }; 93 | std::map files = { { "image", "./butterfly.png" } }; 94 | const auto& boundary = "--boundary"; 95 | 96 | // Act 97 | const auto& result = factory->getRequest(method, uri, headers, elements, files, boundary); 98 | 99 | // Assert 100 | REQUIRE(std::dynamic_pointer_cast(result->getBody())); 101 | } 102 | 103 | SECTION("returns an instance of FormDataRequestBody with the correct values set") 104 | { 105 | // Arrange 106 | std::map elements = { { "username", "bartkessels" } }; 107 | std::map files = { { "image", "./butterfly.png" } }; 108 | const auto& boundary = "--boundary"; 109 | 110 | // Act 111 | const auto& result = factory->getRequest(method, uri, headers, elements, files, boundary); 112 | const auto& requestBody = std::dynamic_pointer_cast(result->getBody()); 113 | 114 | // Assert 115 | REQUIRE(elements == requestBody->getElements()); 116 | REQUIRE(files == requestBody->getFiles()); 117 | } 118 | } -------------------------------------------------------------------------------- /tests/presentation/fragments/tst_ResponseFragmentController.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "presentation/fragments/ResponseFragment/ResponseFragmentController.hpp" 8 | #include "presentation/fragments/ResponseFragment/IResponseFragmentView.hpp" 9 | 10 | using namespace getit; 11 | using namespace getit::presentation::fragments; 12 | 13 | class ResponseFragmentViewMock: public IResponseFragmentView 14 | { 15 | public: 16 | MAKE_MOCK0(getHeaders, (std::map()), override); 17 | MAKE_MOCK0(getBody, std::string(), override); 18 | MAKE_MOCK1(setHeaders, void(const std::map&), override); 19 | MAKE_MOCK1(setBody, void(const std::string&), override); 20 | MAKE_MOCK0(clearHeaders, void(), override); 21 | }; 22 | 23 | TEST_CASE("ResponseFragmentController.getContent") 24 | { 25 | // Before 26 | auto view = new ResponseFragmentViewMock(); 27 | const auto &sut = std::make_shared(view); 28 | 29 | SECTION("returns the headers from the view") 30 | { 31 | // Arrange 32 | std::map expectedHeaders = { 33 | { "Content-Type", "application/json" }, 34 | { "Set-Cookies", "we-track-you=always" } 35 | }; 36 | 37 | ALLOW_CALL(*view, getHeaders()).RETURN(expectedHeaders); 38 | ALLOW_CALL(*view, getBody()).RETURN(""); 39 | 40 | // Act 41 | const auto& actual = sut->getContent(); 42 | 43 | // Assert 44 | REQUIRE(actual->headers == expectedHeaders); 45 | } 46 | 47 | SECTION("returns the body from the view") 48 | { 49 | // Arrange 50 | const auto& expectedBody = "This is my response body"; 51 | 52 | ALLOW_CALL(*view, getHeaders()).RETURN(std::map()); 53 | ALLOW_CALL(*view, getBody()).RETURN(expectedBody); 54 | 55 | // Act 56 | const auto& actual = sut->getContent(); 57 | 58 | // Assert 59 | REQUIRE(actual->body == expectedBody); 60 | } 61 | } 62 | 63 | 64 | TEST_CASE("ResponseFragmentController.setContent") 65 | { 66 | // Before 67 | auto view = new ResponseFragmentViewMock(); 68 | const auto &sut = std::make_shared(view); 69 | 70 | SECTION("calls clearHeaders on the view") 71 | { 72 | // Arrange 73 | ALLOW_CALL(*view, clearHeaders()); 74 | ALLOW_CALL(*view, setHeaders(trompeloeil::_)); 75 | ALLOW_CALL(*view, setBody(trompeloeil::_)); 76 | 77 | const auto& response = std::make_shared(); 78 | 79 | // Assert 80 | REQUIRE_CALL(*view, clearHeaders()); 81 | 82 | // Act 83 | sut->setContent(response); 84 | } 85 | 86 | SECTION("calls setHeaders on the view") 87 | { 88 | // Arrange 89 | ALLOW_CALL(*view, clearHeaders()); 90 | ALLOW_CALL(*view, setBody(trompeloeil::_)); 91 | 92 | std::map expectedHeaders = { 93 | { "Content-Type", "application/json" }, 94 | { "Set-Cookies", "we-track-you=always" } 95 | }; 96 | 97 | const auto& response = std::make_shared(); 98 | response->headers = expectedHeaders; 99 | 100 | // Assert 101 | REQUIRE_CALL(*view, setHeaders(expectedHeaders)); 102 | 103 | // Act 104 | sut->setContent(response); 105 | } 106 | 107 | SECTION("calls clearHeaders before setHeaders on the view") 108 | { 109 | // Arrange 110 | trompeloeil::sequence seq; 111 | ALLOW_CALL(*view, setBody(trompeloeil::_)); 112 | 113 | std::map expectedHeaders = { 114 | { "Content-Type", "application/json" }, 115 | { "Set-Cookies", "we-track-you=always" } 116 | }; 117 | 118 | const auto& response = std::make_shared(); 119 | response->headers = expectedHeaders; 120 | 121 | // Assert 122 | REQUIRE_CALL(*view, clearHeaders()).IN_SEQUENCE(seq); 123 | REQUIRE_CALL(*view, setHeaders(expectedHeaders)).IN_SEQUENCE(seq); 124 | 125 | // Act 126 | sut->setContent(response); 127 | } 128 | 129 | SECTION("calls setBody on the view") 130 | { 131 | // Arrange 132 | ALLOW_CALL(*view, clearHeaders()); 133 | ALLOW_CALL(*view, setHeaders(trompeloeil::_)); 134 | ALLOW_CALL(*view, setBody(trompeloeil::_)); 135 | 136 | const auto& expectedBody = "This is my response body"; 137 | 138 | const auto& response = std::make_shared(); 139 | response->body = expectedBody; 140 | 141 | // Assert 142 | REQUIRE_CALL(*view, setBody(expectedBody)); 143 | 144 | // Act 145 | sut->setContent(response); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /resources/icons/main.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | GetIt Icon 24 | 26 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | GetIt Icon 54 | 06-11-2017 55 | 56 | 57 | Bart Kessels 58 | 59 | 60 | 62 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 78 | 79 | 80 | 85 | 103 | 110 | 115 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /tests/data/repositories/tst_RawRequestRepository.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "data/repositories/RawRequestRepository.hpp" 6 | #include "domain/factories/RequestFactory.hpp" 7 | #include "domain/implementations/RawRequestBody.hpp" 8 | 9 | using namespace getit::data::repositories; 10 | 11 | void writeRawFile(const std::string& path, const std::string& content) 12 | { 13 | std::ofstream output(path); 14 | output << std::setw(4) << content << std::endl; 15 | output.close(); 16 | } 17 | 18 | 19 | TEST_CASE("RawRequestRepository.saveRequest") 20 | { 21 | const auto& factory = std::make_shared(); 22 | const auto& repository = std::make_shared(factory); 23 | const auto& filePath = "./output.json"; 24 | 25 | SECTION("saves all the fields to the specified file") 26 | { 27 | // Arrange 28 | const auto& expectedFileContents = R"({"headers":[{"header":"Content-Type","value":"application/json"}],"method":"GET","raw":{"body":"PlainContent","contentType":"text/plain"},"uri":"https://github.com/bartkessels/getit"})"; 29 | 30 | const auto& request = std::make_shared(); 31 | const auto& body = std::make_shared(); 32 | 33 | request->setMethod("GET"); 34 | request->setUri("https://github.com/bartkessels/getit"); 35 | request->addHeader("Content-Type", "application/json"); 36 | request->setBody(body); 37 | 38 | body->setContentType("text/plain"); 39 | body->setBody("PlainContent"); 40 | 41 | // Act 42 | repository->saveRequest(filePath, request); 43 | 44 | // Assert 45 | std::ifstream input(filePath); 46 | std::string fileContents( 47 | (std::istreambuf_iterator(input)), 48 | (std::istreambuf_iterator()) 49 | ); 50 | 51 | fileContents.erase(std::remove_if( 52 | fileContents.begin(), fileContents.end(), ::isspace 53 | ), fileContents.end()); 54 | 55 | REQUIRE(expectedFileContents == fileContents); 56 | 57 | std::remove(filePath); 58 | } 59 | 60 | SECTION("saves all the headers") 61 | { 62 | // Arrange 63 | const auto& expectedFileContents = R"({"headers":[{"header":"Accept-Language","value":"nl-NL"},{"header":"Content-Type","value":"application/json"}],"method":"GET","raw":{"body":"","contentType":"text/plain"},"uri":"https://github.com/bartkessels/getit"})"; 64 | 65 | const auto& request = std::make_shared(); 66 | const auto& body = std::make_shared(); 67 | request->setMethod("GET"); 68 | request->setUri("https://github.com/bartkessels/getit"); 69 | request->setBody(body); 70 | request->addHeader("Content-Type", "application/json"); 71 | request->addHeader("Accept-Language", "nl-NL"); 72 | 73 | // Act 74 | repository->saveRequest(filePath, request); 75 | 76 | // Assert 77 | std::ifstream input(filePath); 78 | std::string fileContents( 79 | (std::istreambuf_iterator(input)), 80 | (std::istreambuf_iterator()) 81 | ); 82 | 83 | fileContents.erase(std::remove_if( 84 | fileContents.begin(), fileContents.end(), ::isspace 85 | ), fileContents.end()); 86 | 87 | REQUIRE(expectedFileContents == fileContents); 88 | 89 | std::remove(filePath); 90 | } 91 | } 92 | 93 | TEST_CASE("RawRequestRepository.loadRequest") 94 | { 95 | const auto& factory = std::make_shared(); 96 | const auto& repository = std::make_shared(factory); 97 | const auto& filePath = "./Request.getit"; 98 | 99 | SECTION("reads all the fields into the request when it's a raw request") 100 | { 101 | // Arrange 102 | const auto& expectedMethod = "GET"; 103 | const auto& expectedUri = "https://github.com/bartkessels/getit"; 104 | const auto& expectedHeader = "Content-Type"; 105 | const auto& expectedHeaderValue = "application/json"; 106 | const auto& expectedBody = "This is my raw body"; 107 | const auto& expectedContentType = "text/plain"; 108 | 109 | boost::format requestFormat = boost::format( 110 | R"({"headers":[{"header":"%1%","value":"%2%"}],"raw":{"body": "%3%", "contentType":"%4%"},"method":"%5%","uri":"%6%"})" 111 | ) % expectedHeader % expectedHeaderValue % expectedBody % expectedContentType % expectedMethod % expectedUri; 112 | 113 | writeRawFile(filePath, requestFormat.str()); 114 | 115 | // Act 116 | const auto& actual = repository->loadRequest(filePath); 117 | const auto& actualBody = std::dynamic_pointer_cast(actual->getBody()); 118 | 119 | // Assert 120 | REQUIRE(actual->getMethod() == expectedMethod); 121 | REQUIRE(actual->getUri() == expectedUri); 122 | REQUIRE(actual->getHeaders().find(expectedHeader)->first == expectedHeader); 123 | REQUIRE(actual->getHeaders().find(expectedHeader)->second == expectedHeaderValue); 124 | REQUIRE(actualBody->getContentType() == expectedContentType); 125 | REQUIRE(actualBody->getBody() == expectedBody); 126 | 127 | std::remove(filePath); 128 | } 129 | } -------------------------------------------------------------------------------- /src/presentation/windows/MainWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | GetIt 15 | 16 | 17 | QTabWidget::Rounded 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 9 26 | 27 | 28 | 0 29 | 30 | 31 | 0 32 | 33 | 34 | 0 35 | 36 | 37 | 0 38 | 39 | 40 | 41 | 42 | 9 43 | 44 | 45 | 9 46 | 47 | 48 | 9 49 | 50 | 51 | 9 52 | 53 | 54 | 55 | 56 | true 57 | 58 | 59 | 60 | GET 61 | 62 | 63 | 64 | 65 | POST 66 | 67 | 68 | 69 | 70 | PUT 71 | 72 | 73 | 74 | 75 | PATCH 76 | 77 | 78 | 79 | 80 | DELETE 81 | 82 | 83 | 84 | 85 | HEAD 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | http(s)://... 94 | 95 | 96 | true 97 | 98 | 99 | 100 | 101 | 102 | 103 | Send 104 | 105 | 106 | Ctrl+Return 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | QTabWidget::North 116 | 117 | 118 | true 119 | 120 | 121 | false 122 | 123 | 124 | false 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 0 134 | 0 135 | 800 136 | 21 137 | 138 | 139 | 140 | 141 | File 142 | 143 | 144 | 145 | New 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | Raw Request 161 | 162 | 163 | 164 | 165 | Formdata Request 166 | 167 | 168 | 169 | 170 | Save 171 | 172 | 173 | Ctrl+S 174 | 175 | 176 | 177 | 178 | Save As... 179 | 180 | 181 | 182 | 183 | Open 184 | 185 | 186 | Ctrl+O 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | development _AT_ bk-mail _DOT_ com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /tests/data/repositories/tst_RequestRepository.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "data/contracts/RequestRepository.hpp" 7 | #include "data/exceptions/NoAvailableRepositoryException.hpp" 8 | #include "data/repositories/RequestRepository.hpp" 9 | #include "domain/implementations/RawRequestBody.hpp" 10 | 11 | using namespace getit::data::repositories; 12 | 13 | class RequestRepositoryMock: public getit::data::contracts::RequestRepository 14 | { 15 | public: 16 | MAKE_MOCK2(saveRequest, void(const std::string&, std::shared_ptr), override); 17 | MAKE_MOCK1(loadRequest, (std::shared_ptr(const std::string&)), override); 18 | }; 19 | 20 | void writeFile(const std::string& path, const std::string& content) 21 | { 22 | std::ofstream output(path); 23 | output << std::setw(4) << content << std::endl; 24 | output.close(); 25 | } 26 | 27 | TEST_CASE("RequestRepository.saveRequest") 28 | { 29 | // Before 30 | const auto& formDataRepository = std::make_shared(); 31 | const auto& rawRepository = std::make_shared(); 32 | const auto& filePath = "./Request.getit"; 33 | 34 | const auto& sut = std::make_shared(formDataRepository, rawRepository); 35 | 36 | SECTION("calls saveRequest on FormDataRequestRepository when body is instance of FormDataRequestBody") 37 | { 38 | // Arrange 39 | const auto& request = std::make_shared(); 40 | const auto& body = std::make_shared(); 41 | request->setBody(body); 42 | 43 | // Assert 44 | REQUIRE_CALL(*formDataRepository, saveRequest(filePath, request)); 45 | 46 | // Act 47 | sut->saveRequest(filePath, request); 48 | } 49 | 50 | SECTION("calls saveRequest on RawRequestRepository when body is instance of RawRequestBody") 51 | { 52 | // Arrange 53 | const auto& request = std::make_shared(); 54 | const auto& body = std::make_shared(); 55 | request->setBody(body); 56 | 57 | // Assert 58 | REQUIRE_CALL(*rawRepository, saveRequest(filePath, request)); 59 | 60 | // Act 61 | sut->saveRequest(filePath, request); 62 | } 63 | 64 | SECTION("throws a NoAvailableRepositoryException when the body isn't set") 65 | { 66 | // Arrange 67 | const auto& request = std::make_shared(); 68 | 69 | // Act & Assert 70 | REQUIRE_THROWS_AS( 71 | sut->saveRequest(filePath, request), 72 | getit::data::exceptions::NoAvailableRepositoryException 73 | ); 74 | } 75 | 76 | SECTION("throws a NoAvailableRepositoryException when the body is a nullptr") 77 | { 78 | // Arrange 79 | const auto& request = std::make_shared(); 80 | request->setBody(nullptr); 81 | 82 | // Act & Assert 83 | REQUIRE_THROWS_AS( 84 | sut->saveRequest(filePath, request), 85 | getit::data::exceptions::NoAvailableRepositoryException 86 | ); 87 | } 88 | } 89 | 90 | TEST_CASE("RequestRepository.loadRequest") 91 | { 92 | // Before 93 | const auto& formDataRepository = std::make_shared(); 94 | const auto& rawRepository = std::make_shared(); 95 | const auto& filePath = "./Request.getit"; 96 | 97 | const auto& sut = std::make_shared(formDataRepository, rawRepository); 98 | 99 | SECTION("calls loadRequest on FormDataRequestRepository when the json contains a form data body") 100 | { 101 | // Arrange 102 | std::string json = R"({"formdata": {}})"; 103 | writeFile(filePath, json); 104 | 105 | // Assert 106 | REQUIRE_CALL(*formDataRepository, loadRequest(filePath)) 107 | .RETURN(std::make_shared()); 108 | 109 | // Act 110 | sut->loadRequest(filePath); 111 | 112 | // Teardown 113 | std::remove(filePath); 114 | } 115 | 116 | SECTION("calls loadRequest on RawRequestRepository when the json contains a raw body") 117 | { 118 | // Arrange 119 | std::string json = R"({"raw": {}})"; 120 | writeFile(filePath, json); 121 | 122 | // Assert 123 | REQUIRE_CALL(*rawRepository, loadRequest(filePath)) 124 | .RETURN(std::make_shared()); 125 | 126 | // Act 127 | sut->loadRequest(filePath); 128 | 129 | // Teardown 130 | std::remove(filePath); 131 | } 132 | 133 | SECTION("throws a NoAvailableRepositoryException when the body isn't set") 134 | { 135 | // Arrange 136 | writeFile(filePath, R"({"uri": "", "method": "GET"})"); 137 | 138 | // Act & Assert 139 | REQUIRE_THROWS_AS( 140 | sut->loadRequest(filePath), 141 | getit::data::exceptions::NoAvailableRepositoryException 142 | ); 143 | 144 | // Teardown 145 | std::remove(filePath); 146 | } 147 | 148 | SECTION("throws a NoAvailableRepositoryException when the body is a nullptr") 149 | { 150 | // Arrange 151 | writeFile(filePath, R"({"uri": "", "method": "GET"})"); 152 | 153 | // Act & Assert 154 | REQUIRE_THROWS_AS( 155 | sut->loadRequest(filePath), 156 | getit::data::exceptions::NoAvailableRepositoryException 157 | ); 158 | 159 | // Teardown 160 | std::remove(filePath); 161 | } 162 | } -------------------------------------------------------------------------------- /tests/service/implementations/tst_CppRestRequestService.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "domain/implementations/FormDataRequestBody.hpp" 7 | #include "domain/implementations/RawRequestBody.hpp" 8 | #include "domain/models/Request.hpp" 9 | #include "service/implementations/CppRestRequestService.hpp" 10 | 11 | using namespace getit::domain; 12 | using namespace getit::service::implementations; 13 | using namespace web::http; 14 | using namespace web::http::experimental::listener; 15 | 16 | void listen(const std::shared_ptr& listener, const std::function& callback) 17 | { 18 | listener->support([=](const auto& request) { 19 | callback(request); 20 | request.reply(status_codes::OK); 21 | }); 22 | } 23 | 24 | TEST_CASE("CppRestRequestService.send") 25 | { 26 | const auto& sut = std::make_shared(); 27 | const auto& request = std::make_shared(); 28 | 29 | // Setup request listener 30 | const auto& uri = "http://localhost:1874/test_resource"; 31 | const auto& requestListener = std::make_shared(uri); 32 | requestListener->open().wait(); 33 | 34 | SECTION("adds the method to the request") 35 | { 36 | // Arrange 37 | const auto& expectedMethod = "PUT"; 38 | 39 | request->setMethod(expectedMethod); 40 | request->setUri(uri); 41 | 42 | // Assert 43 | listen(requestListener, [=](const http_request& actualRequest) { 44 | REQUIRE(actualRequest.method() == expectedMethod); 45 | }); 46 | 47 | // Act 48 | sut->send(request).wait(); 49 | } 50 | 51 | SECTION("adds the uri to the request") 52 | { 53 | // Arrange 54 | request->setMethod("GET"); 55 | request->setUri(uri); 56 | 57 | // Assert 58 | listen(requestListener, [=](const http_request& actualRequest) { 59 | /// If the control flow comes here, it means that the request is 60 | /// sent to the uri of the listener (i.e. the uri that's also set 61 | /// on the request) 62 | REQUIRE(true); 63 | }); 64 | 65 | // Act 66 | sut->send(request).wait(); 67 | } 68 | 69 | SECTION("adds the headers to the request") 70 | { 71 | // Arrange 72 | std::map expectedHeaders = { 73 | { "Content-Type", "application/json" }, 74 | { "Accept-Language", "en-US" } 75 | }; 76 | 77 | request->setMethod("GET"); 78 | request->setUri(uri); 79 | request->setHeaders(expectedHeaders); 80 | 81 | // Assert 82 | listen(requestListener, [=](const http_request& actualRequest) { 83 | for (auto [header, value] : expectedHeaders) { 84 | REQUIRE(actualRequest.headers().match(header, value)); 85 | } 86 | }); 87 | 88 | // Act 89 | sut->send(request).wait(); 90 | } 91 | 92 | SECTION("does not add a body to the request when no body is set") 93 | { 94 | // Arrange 95 | request->setMethod("GET"); 96 | request->setUri(uri); 97 | 98 | // Assert 99 | listen(requestListener, [=](const http_request& actualRequest) { 100 | REQUIRE(actualRequest.body().read().get() <= 0); 101 | }); 102 | 103 | // Act 104 | sut->send(request).wait(); 105 | } 106 | 107 | SECTION("does not add a body to the request when body points to null") 108 | { 109 | // Arrange 110 | request->setMethod("GET"); 111 | request->setUri(uri); 112 | request->setBody(nullptr); 113 | 114 | // Assert 115 | listen(requestListener, [=](const http_request& actualRequest) { 116 | REQUIRE(actualRequest.body().read().get() <= 0); 117 | }); 118 | 119 | // Act 120 | sut->send(request).wait(); 121 | } 122 | 123 | SECTION("does add a body to the request when body is of type form data") 124 | { 125 | // Arrange 126 | const auto& key = "email_address"; 127 | const auto& value = "my_first_email@localhost"; 128 | 129 | const auto& fileKey = "picture.png"; 130 | const auto& filePath = "./test_file.txt"; 131 | const auto& fileContent = "content"; 132 | 133 | const auto& body = std::make_shared(); 134 | body->addElement(key, value); 135 | body->addFile(fileKey, filePath); 136 | 137 | request->setMethod("GET"); 138 | request->setUri(uri); 139 | request->setBody(body); 140 | 141 | // Assert 142 | listen(requestListener, [=](const http_request& actualRequest) { 143 | REQUIRE(actualRequest.to_string().find(key)); 144 | REQUIRE(actualRequest.to_string().find(value)); 145 | 146 | REQUIRE(actualRequest.to_string().find(fileKey)); 147 | REQUIRE(actualRequest.to_string().find(filePath)); 148 | REQUIRE(actualRequest.to_string().find(fileContent)); 149 | }); 150 | 151 | // Act 152 | sut->send(request).wait(); 153 | } 154 | 155 | SECTION("does add a body to the request when body is of type raw") 156 | { 157 | // Arrange 158 | auto bodyContent = "This is my plain text raw body"; 159 | 160 | const auto& body = std::make_shared(); 161 | body->setBody(bodyContent); 162 | 163 | request->setMethod("GET"); 164 | request->setUri(uri); 165 | request->setBody(body); 166 | 167 | // Assert 168 | listen(requestListener, [=](const http_request& actualRequest) { 169 | REQUIRE(actualRequest.to_string().find(bodyContent)); 170 | }); 171 | 172 | // Act 173 | sut->send(request).wait(); 174 | } 175 | } -------------------------------------------------------------------------------- /tests/presentation/highlighters/tst_JsonSyntaxHighlighterRule.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "presentation/highlighters/JsonSyntaxHighlighterRule.hpp" 5 | 6 | using namespace getit::presentation::highlighters; 7 | 8 | TEST_CASE("JsonHighlighterRules - integerRule") 9 | { 10 | SECTION("Allows - (minus) sign in front of number") 11 | { 12 | // Arrange 13 | const auto& inputValue = "-2"; 14 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 15 | 16 | // Act 17 | std::cmatch match; 18 | const bool result = std::regex_match(inputValue, match, regex); 19 | 20 | // Assert 21 | REQUIRE(result == true); 22 | } 23 | 24 | SECTION("Does not allow - (minus) sign after number") 25 | { 26 | // Arrange 27 | const auto& inputValue = "2-"; 28 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 29 | 30 | // Act 31 | std::cmatch match; 32 | const bool result = std::regex_match(inputValue, match, regex); 33 | 34 | // Assert 35 | REQUIRE(result == false); 36 | } 37 | 38 | SECTION("Allows + (plus) sign in front of number") 39 | { 40 | // Arrange 41 | const auto& inputValue = "+2"; 42 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 43 | 44 | // Act 45 | std::cmatch match; 46 | const bool result = std::regex_match(inputValue, match, regex); 47 | 48 | // Assert 49 | REQUIRE(result == true); 50 | } 51 | 52 | SECTION("Does not allow + (plus) sign after number") 53 | { 54 | // Arrange 55 | const auto& inputValue = "2+"; 56 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 57 | 58 | // Act 59 | std::cmatch match; 60 | const bool result = std::regex_match(inputValue, match, regex); 61 | 62 | // Assert 63 | REQUIRE(result == false); 64 | } 65 | 66 | SECTION("Does not allow number in double quotes") 67 | { 68 | // Arrange 69 | const auto& inputValue = "\"2\""; 70 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 71 | 72 | // Act 73 | std::cmatch match; 74 | const bool result = std::regex_match(inputValue, match, regex); 75 | 76 | // Assert 77 | REQUIRE(result == false); 78 | } 79 | 80 | SECTION("Does not allow number in single quotes") 81 | { 82 | // Arrange 83 | const auto& inputValue = "'2'"; 84 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 85 | 86 | // Act 87 | std::cmatch match; 88 | const bool result = std::regex_match(inputValue, match, regex); 89 | 90 | // Assert 91 | REQUIRE(result == false); 92 | } 93 | 94 | SECTION("Allows floating point numbers with . (dot)") 95 | { 96 | // Arrange 97 | const auto& inputValue = "2.21"; 98 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 99 | 100 | // Act 101 | std::cmatch match; 102 | const bool result = std::regex_match(inputValue, match, regex); 103 | 104 | // Assert 105 | REQUIRE(result == true); 106 | } 107 | 108 | SECTION("Allows floating point numbers with large amount of numbers before the . (dot)") 109 | { 110 | // Arrange 111 | const auto& inputValue = "232248796434843.21"; 112 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 113 | 114 | // Act 115 | std::cmatch match; 116 | const bool result = std::regex_match(inputValue, match, regex); 117 | 118 | // Assert 119 | REQUIRE(result == true); 120 | } 121 | 122 | SECTION("Allows floating point numbers with large amount of numbers after the . (dot)") 123 | { 124 | // Arrange 125 | const auto& inputValue = "2.21232248796434843"; 126 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 127 | 128 | // Act 129 | std::cmatch match; 130 | const bool result = std::regex_match(inputValue, match, regex); 131 | 132 | // Assert 133 | REQUIRE(result == true); 134 | } 135 | 136 | SECTION("Does not allow floating point numbers with , (comma)") 137 | { 138 | // Arrange 139 | const auto& inputValue = "2,21"; 140 | std::regex regex(JsonSyntaxHighlighterRule::integerRule->regex); 141 | 142 | // Act 143 | std::cmatch match; 144 | const bool result = std::regex_match(inputValue, match, regex); 145 | 146 | // Assert 147 | REQUIRE(result == false); 148 | } 149 | } 150 | 151 | TEST_CASE("JsonHighlighterRules - stringRule") 152 | { 153 | SECTION("Allows string within double quotes") 154 | { 155 | // Arrange 156 | const auto& inputValue = R"("This is my string")"; 157 | std::regex regex(JsonSyntaxHighlighterRule::stringRule->regex); 158 | 159 | // Act 160 | std::cmatch match; 161 | const bool result = std::regex_match(inputValue, match, regex); 162 | 163 | // Assert 164 | REQUIRE(result == true); 165 | } 166 | 167 | SECTION("Does not allow string within single quotes") 168 | { 169 | // Arrange 170 | const auto& inputValue = "'This is my string'"; 171 | std::regex regex(JsonSyntaxHighlighterRule::stringRule->regex); 172 | 173 | // Act 174 | std::cmatch match; 175 | const bool result = std::regex_match(inputValue, match, regex); 176 | 177 | // Assert 178 | REQUIRE(result == false); 179 | } 180 | 181 | SECTION("Does not allow string without quotes") 182 | { 183 | // Arrange 184 | const auto& inputValue = "This is my string"; 185 | std::regex regex(JsonSyntaxHighlighterRule::stringRule->regex); 186 | 187 | // Act 188 | std::cmatch match; 189 | const bool result = std::regex_match(inputValue, match, regex); 190 | 191 | // Assert 192 | REQUIRE(result == false); 193 | } 194 | 195 | SECTION("Allows numbers") 196 | { 197 | // Arrange 198 | const auto& inputValue = R"("This is my string with the number 5")"; 199 | std::regex regex(JsonSyntaxHighlighterRule::stringRule->regex); 200 | 201 | // Act 202 | std::cmatch match; 203 | const bool result = std::regex_match(inputValue, match, regex); 204 | 205 | // Assert 206 | REQUIRE(result == true); 207 | } 208 | } -------------------------------------------------------------------------------- /docs/uml/class_diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml "Class diagram GetIt" 2 | skinparam linetype ortho 3 | 4 | ' Domain layer 5 | namespace domain { 6 | namespace models { 7 | class Request { 8 | - method: string 9 | - uri: string 10 | - headers: map 11 | 12 | + setMethod(method: string): void 13 | + setUri(uri: string): void 14 | + addHeader(header: string, value: string): void 15 | + setHeaders(headers: map): void 16 | + getMethod(): string 17 | + getUri(): string 18 | + getHeaders(): map 19 | + {abstract} getBody(): string 20 | + {abstract} getContentType(): string 21 | } 22 | 23 | class Response { 24 | + headers: map 25 | + body: string 26 | } 27 | } 28 | 29 | namespace contracts { 30 | interface RequestFactory { 31 | + getRequest(body: string): Request 32 | + getRequest(body: string, contentType: string): Request 33 | + getRequest(elements: map, files: map, boundary: string): Request 34 | } 35 | 36 | RequestFactory ..> domain.models.Request 37 | } 38 | 39 | namespace factories { 40 | class RequestFactory { } 41 | 42 | RequestFactory ..|> domain.contracts.RequestFactory 43 | } 44 | 45 | namespace implementations { 46 | class FormdataRequest { 47 | - boundary: string 48 | - elements: map 49 | - files: map 50 | - {static} CONTENT_TYPE_TEMPLATE: string 51 | - {static} BODY_WITH_BOUNDARY_TEMPLATE: string 52 | - {static} ELEMENT_TEMPLATE: string 53 | - {static} FILE_TEMPLATE: string 54 | 55 | + addElement(key: string, value: string): void 56 | + addFile(key: string, filePath: string): void 57 | + setBoundary(boundary: string): void 58 | + setElements(elements: map): void 59 | + setFiles(files: map): void 60 | + getBoundary(): string 61 | + getElements(): map 62 | + getFiles(): map 63 | 64 | - getElementsBody(): string 65 | - getFilesBody(): string 66 | } 67 | 68 | class RawRequest { 69 | - body: string 70 | - contentType: string 71 | - {static} DEFAULT_CONTENT_TYPE = "text/plain": string 72 | 73 | + setContentType(contentType: string): void 74 | + setBody(body: string): void 75 | } 76 | 77 | FormdataRequest --|> domain.models.Request 78 | RawRequest --|> domain.models.Request 79 | } 80 | } 81 | 82 | ' Data layer 83 | namespace data { 84 | namespace contracts { 85 | interface RequestRepository { 86 | + {static} METHOD_NAME = "method": string 87 | + {static} URI_NAME = "uri": string 88 | 89 | + saveRequest(filePath: string, request: Request): void 90 | + loadRequest(filePath: string): Request 91 | } 92 | 93 | interface RequestRepositoryFactory { 94 | + getRepository(request: Request): RequestRepository 95 | } 96 | 97 | RequestRepositoryFactory ..> RequestRepository 98 | RequestRepository ..> domain.models.Request 99 | } 100 | 101 | namespace exceptions { 102 | class NoAvailableRepositoryException { } 103 | } 104 | 105 | namespace factories { 106 | class RequestRepositoryFactory {} 107 | 108 | RequestRepositoryFactory ..|> data.contracts.RequestRepository 109 | RequestRepositoryFactory ..> data.exceptions.NoAvailableRepositoryException 110 | } 111 | 112 | namespace repositories { 113 | class FormdataRequestRepository { 114 | - {static} BOUNDARY_NAME = "boundary": string 115 | - {static} BODY_TYPE_NAME = "formdata": string 116 | - {static} ELEMENTS_ARRAY_NAME = "elements": string 117 | - {static} FILES_ARRAY_NAME = "files": string 118 | - {static} KEY_NAME = "key": string 119 | - {static} ELEMENT_VALUE_NAME = "value": string 120 | - {static} FILE_PATH_NAME = "filePath": string 121 | } 122 | 123 | class RawRequestRepository { 124 | - {static} BODY_TYPE_NAME = "raw": string 125 | - {static} BODY_NAME = "body": string 126 | - {static} CONTENT_TYPE_NAME = "contentType": string 127 | } 128 | 129 | FormdataRequestRepository ..|> data.contracts.RequestRepository 130 | RawRequestRepository ..|> data.contracts.RequestRepository 131 | } 132 | } 133 | 134 | ' Service layer 135 | namespace service { 136 | namespace contracts { 137 | interface RequestService { 138 | + send(request: Request): future 139 | } 140 | 141 | RequestService ..> domain.models.Request 142 | RequestService ..> domain.models.Response 143 | } 144 | 145 | namespace factories { 146 | class RequestServiceFactory { 147 | + getRequestService(): RequestService 148 | } 149 | 150 | RequestServiceFactory ..> service.contracts.RequestService 151 | } 152 | 153 | namespace implementations { 154 | class CppRestRequestService { } 155 | 156 | CppRestRequestService ..|> service.contracts.RequestService 157 | } 158 | } 159 | 160 | ' Presentation layer 161 | namespace presentation { 162 | namespace window { 163 | class MainWindow { 164 | // emit sendRequest signal 165 | // emit new tab signal 166 | // emit close tab signal 167 | // emit save tab signal 168 | // emit save tab as signal 169 | 170 | // listen on response slot 171 | } 172 | 173 | class MainWindowController { 174 | // send response signal 175 | 176 | // listen on sendRequest signal 177 | // listen on new tab slot 178 | // listen on close tab slot 179 | // listen on save tab slot 180 | // listen on save tab as slot 181 | } 182 | } 183 | 184 | namespace widget { 185 | class RawRequestWidget { 186 | // emit new header signal 187 | // emit new cookie signal 188 | // emit body changed signal 189 | } 190 | 191 | class RawRequestWidgetController { 192 | // listen on new header slot 193 | // listen on new cookie slot 194 | // listen on body changed slot 195 | } 196 | 197 | class FormdataWidget { 198 | // emit new header signal 199 | // emit new cookie signal 200 | // emit new element signal 201 | // emit new file signal 202 | } 203 | 204 | class FormdataWidgetController { 205 | // listen on new header slot 206 | // listen on new cookie slot 207 | // listen on new element slot 208 | // listen on new file signal 209 | } 210 | } 211 | } 212 | 213 | @enduml -------------------------------------------------------------------------------- /tests/data/repositories/tst_FormDataRequestRepository.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "data/repositories/FormDataRequestRepository.hpp" 8 | #include "domain/factories/RequestFactory.hpp" 9 | #include "domain/implementations/FormDataRequestBody.hpp" 10 | 11 | using namespace getit::data::repositories; 12 | 13 | void writeFormDataFile(const std::string& path, const std::string& content) 14 | { 15 | std::ofstream output(path); 16 | output << std::setw(4) << content << std::endl; 17 | output.close(); 18 | } 19 | 20 | TEST_CASE("FormDataRequestRepository.saveRequest") 21 | { 22 | const auto& factory = std::make_shared(); 23 | const auto& repository = std::make_shared(factory); 24 | const auto& filePath = "./Request.getit"; 25 | 26 | SECTION("saves all the fields to the specified file") 27 | { 28 | // Arrange 29 | const auto& expectedFileContents = R"({"formdata":{"boundary":"--bound","elements":[],"files":[]},"headers":[{"header":"Content-Type","value":"application/json"}],"method":"GET","uri":"https://github.com/bartkessels/getit"})"; 30 | 31 | const auto& request = std::make_shared(); 32 | const auto& body = std::make_shared(); 33 | request->setMethod("GET"); 34 | request->setUri("https://github.com/bartkessels/getit"); 35 | request->addHeader("Content-Type", "application/json"); 36 | request->setBody(body); 37 | body->setBoundary("--bound"); 38 | 39 | // Act 40 | repository->saveRequest(filePath, request); 41 | 42 | // Assert 43 | std::ifstream input(filePath); 44 | std::string fileContents( 45 | (std::istreambuf_iterator(input)), 46 | (std::istreambuf_iterator()) 47 | ); 48 | 49 | fileContents.erase(std::remove_if( 50 | fileContents.begin(), fileContents.end(), ::isspace 51 | ), fileContents.end()); 52 | 53 | REQUIRE(expectedFileContents == fileContents); 54 | 55 | std::remove(filePath); 56 | } 57 | 58 | SECTION("saves all the elements") 59 | { 60 | // Arrange 61 | const auto& expectedFileContents = R"({"formdata":{"boundary":"--bound","elements":[{"key":"password","value":"password"},{"key":"username","value":"someone"}],"files":[]},"headers":[],"method":"GET","uri":"https://github.com/bartkessels/getit"})"; 62 | 63 | const auto& request = std::make_shared(); 64 | const auto& body = std::make_shared(); 65 | request->setMethod("GET"); 66 | request->setUri("https://github.com/bartkessels/getit"); 67 | request->setBody(body); 68 | body->setBoundary("--bound"); 69 | body->addElement("username", "someone"); 70 | body->addElement("password", "password"); 71 | 72 | // Act 73 | repository->saveRequest(filePath, request); 74 | 75 | // Assert 76 | std::ifstream input(filePath); 77 | std::string fileContents( 78 | (std::istreambuf_iterator(input)), 79 | (std::istreambuf_iterator()) 80 | ); 81 | 82 | fileContents.erase(std::remove_if( 83 | fileContents.begin(), fileContents.end(), ::isspace 84 | ), fileContents.end()); 85 | 86 | REQUIRE(expectedFileContents == fileContents); 87 | 88 | std::remove(filePath); 89 | } 90 | 91 | SECTION("saves all the file paths") 92 | { 93 | // Arrange 94 | const auto& expectedFileContents = R"({"formdata":{"boundary":"--bound","elements":[],"files":[{"filePath":"./picture.png","key":"image"},{"filePath":"./test_file.txt","key":"test"}]},"headers":[],"method":"GET","uri":"https://github.com/bartkessels/getit"})"; 95 | 96 | const auto& request = std::make_shared(); 97 | const auto& body = std::make_shared(); 98 | request->setMethod("GET"); 99 | request->setUri("https://github.com/bartkessels/getit"); 100 | request->setBody(body); 101 | body->setBoundary("--bound"); 102 | body->addFile("image", "./picture.png"); 103 | body->addFile("test", "./test_file.txt"); 104 | 105 | // Act 106 | repository->saveRequest(filePath, request); 107 | 108 | // Assert 109 | std::ifstream input(filePath); 110 | std::string fileContents( 111 | (std::istreambuf_iterator(input)), 112 | (std::istreambuf_iterator()) 113 | ); 114 | 115 | fileContents.erase(std::remove_if( 116 | fileContents.begin(), fileContents.end(), ::isspace 117 | ), fileContents.end()); 118 | 119 | REQUIRE(expectedFileContents == fileContents); 120 | 121 | std::remove(filePath); 122 | } 123 | 124 | SECTION("saves all the headers") 125 | { 126 | // Arrange 127 | const auto& expectedFileContents = R"({"formdata":{"boundary":"","elements":[],"files":[]},"headers":[{"header":"Accept-Language","value":"nl-NL"},{"header":"Content-Type","value":"application/json"}],"method":"GET","uri":"https://github.com/bartkessels/getit"})"; 128 | 129 | const auto& request = std::make_shared(); 130 | const auto& body = std::make_shared(); 131 | request->setMethod("GET"); 132 | request->setUri("https://github.com/bartkessels/getit"); 133 | request->setBody(body); 134 | request->addHeader("Content-Type", "application/json"); 135 | request->addHeader("Accept-Language", "nl-NL"); 136 | 137 | // Act 138 | repository->saveRequest(filePath, request); 139 | 140 | // Assert 141 | std::ifstream input(filePath); 142 | std::string fileContents( 143 | (std::istreambuf_iterator(input)), 144 | (std::istreambuf_iterator()) 145 | ); 146 | 147 | fileContents.erase(std::remove_if( 148 | fileContents.begin(), fileContents.end(), ::isspace 149 | ), fileContents.end()); 150 | 151 | REQUIRE(expectedFileContents == fileContents); 152 | 153 | std::remove(filePath); 154 | } 155 | } 156 | 157 | TEST_CASE("FormDataRequestRepository.loadRequest") 158 | { 159 | const auto& factory = std::make_shared(); 160 | const auto& repository = std::make_shared(factory); 161 | const auto& filePath = "./Request.getit"; 162 | 163 | SECTION("reads all the fields into the request when it's a form data request") 164 | { 165 | // Arrange 166 | const auto& expectedMethod = "GET"; 167 | const auto& expectedUri = "https://github.com/bartkessels/getit"; 168 | const auto& expectedHeader = "Content-Type"; 169 | const auto& expectedHeaderValue = "application/json"; 170 | const auto& expectedBoundary = "--bound"; 171 | const auto& expectedKey = "key"; 172 | const auto& expectedValue = "value"; 173 | const auto& expectedFileName = "image.png"; 174 | const auto& expectedFilePath = "./bin/tst_file.txt"; 175 | 176 | boost::format requestFormat = boost::format( 177 | R"({"formdata":{"boundary":"%1%","elements":[{"key":"%2%","value":"%3%"}],"files":[{"key":"%4%","filePath":"%5%"}]},"headers":[{"header":"%6%","value":"%7%"}],"method":"%8%","uri":"%9%"})" 178 | ) % expectedBoundary % expectedKey % expectedValue % expectedFileName % expectedFilePath % expectedHeader % expectedHeaderValue % expectedMethod % expectedUri; 179 | 180 | writeFormDataFile(filePath, requestFormat.str()); 181 | 182 | // Act 183 | const auto& actual = repository->loadRequest(filePath); 184 | const auto& actualBody = std::dynamic_pointer_cast(actual->getBody()); 185 | 186 | // Assert 187 | REQUIRE(actual->getMethod() == expectedMethod); 188 | REQUIRE(actual->getUri() == expectedUri); 189 | REQUIRE(actual->getHeaders().find(expectedHeader)->first == expectedHeader); 190 | REQUIRE(actual->getHeaders().find(expectedHeader)->second == expectedHeaderValue); 191 | REQUIRE(actualBody->getBoundary() == expectedBoundary); 192 | REQUIRE(actualBody->getElements().find(expectedKey)->first == expectedKey); 193 | REQUIRE(actualBody->getElements().find(expectedKey)->second == expectedValue); 194 | REQUIRE(actualBody->getFiles().find(expectedFileName)->first == expectedFileName); 195 | REQUIRE(actualBody->getFiles().find(expectedFileName)->second == expectedFilePath); 196 | 197 | std::remove(filePath); 198 | } 199 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GetIt 2 | 3 | Open Source HTTP client to test your API's. 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 |
7 | [![Build & Test GetIt](https://github.com/bartkessels/GetIt/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/bartkessels/GetIt/actions/workflows/build_and_test.yml) 8 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/4b84329958e04d3abeb7fc5c45eac01c)](https://www.codacy.com/gh/bartkessels/GetIt/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bartkessels/GetIt&utm_campaign=Badge_Grade) 9 | [![Vulnerability scan](https://github.com/bartkessels/GetIt/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/bartkessels/GetIt/actions/workflows/codeql-analysis.yml) 10 |
11 | 12 | [![Build MacOS package](https://github.com/bartkessels/GetIt/actions/workflows/macos_bundle.yml/badge.svg)](https://github.com/bartkessels/GetIt/actions/workflows/macos_bundle.yml) 13 | [![Build Windows executable](https://github.com/bartkessels/GetIt/actions/workflows/windows_executable.yml/badge.svg)](https://github.com/bartkessels/GetIt/actions/workflows/windows_executable.yml) 14 | [![Build Linux Flatpak](https://github.com/bartkessels/GetIt/actions/workflows/linux_flatpak.yml/badge.svg)](https://github.com/bartkessels/GetIt/actions/workflows/linux_flatpak.yml) 15 | 16 | --- 17 | 18 | Welcome to the Git repository of GetIt. GetIt is a small open source application to send API 19 | request using a cross-platform GUI. The main advantage of GetIt is the fact that it's native, no 20 | matter what platform you use, and it's completely open source. 21 | 22 | # Table of contents 23 | 24 | - [1. Overview](#1-overview) 25 | - [1.1 Sharing requests](#11-sharing-requests) 26 | - [1.2 Roadmap](#12-roadmap) 27 | - [2. External dependencies](#2-external-dependencies) 28 | - [3. Build](#3-build) 29 | - [3.1 MacOS](#31-macos) 30 | - [3.2 Linux](#32-linux) 31 | - [3.3 Windows](#33-windows) 32 | - [4. Package](#4-package) 33 | - [4.1 MacOS](#41-macos) 34 | - [4.2 Linux](#42-linux) 35 | - [4.3 Windows](#43-windows) 36 | - [5. Automated testing](#5-automated-testing) 37 | - [6. Contribute](#6-contribute) 38 | - [6.1 Branching strategy](#61-branching-strategy) 39 | 40 | # 1. Overview 41 | 42 | GetIt's main focus is providing an easy user interface to send HTTP request to your API endpoints. 43 | GetIt is developed for developers who'd like a native application which is capable of saving requests 44 | in a modern file format. 45 | 46 | ![Main screen of GetIt](resources/images/getit.png) 47 | 48 | ## 1.1 Sharing requests 49 | 50 | GetIt uses the JSON-format to store request information, however, when you've got a request that uses 51 | files these files aren't incorporated into the GetIt request file. 52 | 53 | The main features of GetIt compared to the alternatives are the way requests can be shared. There are 54 | other applications which let's you synchronise a number of requests using a proprietary cloud solution 55 | with a subscription. In the design of GetIt we took the sharing of requests very seriously but since we're not 56 | an entire company we can't setup an entire cloud solution. Thus we've made sure the file contents are human readable 57 | an easy to check-in using VCS. 58 | 59 | ## 1.2 Roadmap 60 | 61 | Because GetIt is maintained in the spare time, not all features that we'd like are implemented as of yet. 62 | Some of the features we'd like to see in GetIt are listed below. These features are also added as issues 63 | on GitHub so the status can be viewed in the projects tab. 64 | 65 | - Multiple authentication flows 66 | - OAuth 67 | - JWT 68 | - Basic Auth 69 | - Environment variables 70 | - Variables that are stored in the request itself 71 | - Variables that are stored in the settings of GetIt 72 | - To allow API secrets to be stored, but not checked into VCS 73 | - Automated stress testing 74 | 75 | # 2. External dependencies 76 | 77 | GetIt uses CMake to download and compile most of it's dependencies. However, not all dependencies 78 | are aviable through this method so these need to be installed on the machine itself before compiling. 79 | When downloading and executing a pre-built release from the Releases tab on Github, these dependencies 80 | aren't required. 81 | 82 | - [Qt](https://qt.io) 83 | - The GUI framework used by GetIt 84 | - [Boost](https://boost.org) 85 | - Helper libraries for formatting strings and used by [CppRestSdk](https://github.com/microsoft/cpprestsdk), which in turn is used by GetIt as the HTTP library 86 | - [Ninja](https://ninja-build.org) 87 | - Cross-platform build system 88 | - [CMake](https://cmake.org) 89 | - Cross-platform build tool generator 90 | 91 | # 3. Build 92 | 93 | ## 3.1 MacOS 94 | 95 | ```sh 96 | # Disable warnings as error, these warning are from the CppRestSdk dependency 97 | cmake . -G Ninja -D CMAKE_CXX_FLAGS="-w" 98 | ninja GetIt 99 | ``` 100 | 101 | This will create the `./bin/GetIt.app` file. This can be executed by double-clicking it in Finder. 102 | 103 | Please note that this app bundle does __NOT__ include the dependencies or the Qt framework. When you move 104 | the bundle to a different location on your computer (or another computer altogether) the application might not start. 105 | 106 | ## 3.2 Linux 107 | 108 | __NOT TESTED YET!__ 109 | 110 | The following steps have not been tested yet, if you follow these steps, and it works, please open a PR 111 | to remove this warning. If these steps don't work, and you've got a working solution. Please also open a PR to 112 | include your steps here. 113 | 114 | ```sh 115 | cmake . -G Ninja 116 | ninja GetIt 117 | ``` 118 | 119 | This will create the `./bin/GetIt` binary file. It can be executed by either running `./bin/GetIt` from the terminal 120 | or double-clicking the file through your file browser. 121 | 122 | ## 3.3 Windows 123 | 124 | __NOT TESTED YET!__ 125 | 126 | The following steps have not been tested yet, if you follow these steps, and it works, please open a PR 127 | to remove this warning. If these steps don't work, and you've got a working solution. Please also open a PR to 128 | include your steps here. 129 | 130 | ```sh 131 | cmake . -G Ninja 132 | ninja GetIt 133 | ``` 134 | 135 | This will create the `./bin/GetIt.exe` executable. It can be executed by double-clicking the file from Explorer. 136 | 137 | # 4. Package 138 | 139 | ## 4.1 MacOS 140 | 141 | Build the MacOS app bundle with all dependencies and framework included by running the `package.sh` script 142 | in the MacOS packaging folder. 143 | 144 | ```sh 145 | sh ./packaging/macos/package.sh 146 | ``` 147 | 148 | This will create the `./bin/GetIt.app` app bundle with all required dependencies. This file can be distributed across 149 | multiple systems. 150 | 151 | ## 4.2 Linux 152 | 153 | This is not completed yet, there is going to be a script to generate a flatpak bundle. 154 | 155 | ## 4.3 Windows 156 | 157 | This is not completed yet, there is going to be a script to generate a Windows executable file. 158 | 159 | # 5. Automated testing 160 | 161 | To validate that GetIt works, and keeps working through various updates GetIt includes automated unit 162 | tests using [Catch2](https://github.com/catch/catch2). These tests are automatically executed when you create 163 | a PR or when directly pushing to the main branch (which should __NEVER__ be done!). 164 | 165 | To execute these tests manually you need to specify the `getit_tests` build target. 166 | 167 | ```sh 168 | cmake . -G Ninja 169 | ninja getit_tests 170 | 171 | # Execute the test cases 172 | ./bin/getit_tests 173 | ``` 174 | 175 | This will generate an application that will test GetIt when executed. 176 | 177 | # 6. Contribute 178 | 179 | If there's a feature you'd like to see, or a bug you've encountered? Please let us know! Or, even better, fix it 180 | yourself! If you'd just like to contribute back to GetIt but you're not quite sure how to help, take a look at the roadmap 181 | for features we'd love to see in GetIt. 182 | 183 | All contributions are welcome, so feel free to join the open source community and support GetIt through your expertise! 184 | 185 | To make sure everyone can contribute to GetIt we value the quality and understandability of our code. When you're building 186 | a feature, or patching a bug and you've made a decision. Please document this decision in the [`docs/design.md`](docs/design.md) document. 187 | This will help future contributors understand your rationale. If you've updated some logic in the code, please always update related 188 | test cases or add test cases to test your logic. 189 | 190 | ## 6.1 Branching strategy 191 | 192 | You've got your idea to help us out, but you're not quite sure how to setup your branch. We'll we've got you covered! 193 | 194 | This project does not use a development branch but uses short lived feature branches which are directly merged into `main`. 195 | We've chosen this strategy 'cause we believe in _release often, release fast_, there's no need for your feature to gather dust in 196 | a stale development branch. However we do have some guidelines: 197 | 198 | - If you're contributing a __feature__ use the `feature/` naming convention 199 | - If you're contributing a __bug fix__ use the `bugfix/` naming convention 200 | - If you're contributing to __documentation__ use the `documentation/` naming convention 201 | - If you're contributing a __small fix__, like a version bump to a dependency, use the `hotfix/-` naming convention 202 | -------------------------------------------------------------------------------- /tests/domain/implementations/tst_FormDataRequestBody.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "domain/implementations/FormDataRequestBody.hpp" 8 | 9 | using namespace getit::domain::implementations; 10 | 11 | TEST_CASE("FormDataRequestBody.getContentType") 12 | { 13 | const auto& boundary = "--boundary-"; 14 | const auto& body = std::make_shared(boundary); 15 | 16 | SECTION("returns the expected multipart/form-data Content-Type with the given boundary") 17 | { 18 | // Arrange 19 | const auto& expectedContentType = (boost::format( 20 | "multipart/form-data; boundary=\"%1%\"" 21 | ) % boundary).str(); 22 | 23 | // Act 24 | const auto& result = body->getContentType(); 25 | 26 | // Assert 27 | REQUIRE(expectedContentType == result); 28 | } 29 | } 30 | 31 | TEST_CASE("FormDataRequestBody.getBody") 32 | { 33 | const auto& boundary = "--boundary-"; 34 | const auto& body = std::make_shared(boundary); 35 | 36 | SECTION("returns the string with unescaped characters for elements") 37 | { 38 | // Arrange 39 | const auto& key = "element"; 40 | const auto& value = R"("P@ssW0rd!"")"; 41 | const auto& expectedBody = (boost::format( 42 | "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"\r\n\r\n%3%\r\n\r\n--%1%--\r\n" 43 | ) % boundary % key % value).str(); 44 | 45 | // Act 46 | body->addElement(key, value); 47 | const auto& result = body->getBody(); 48 | 49 | // Assert 50 | REQUIRE(expectedBody == result); 51 | } 52 | 53 | SECTION("returns the expected format for elements when one element is set") 54 | { 55 | // Arrange 56 | const auto& key = "element"; 57 | const auto& value = "value"; 58 | const auto& expectedBody = (boost::format( 59 | "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"\r\n\r\n%3%\r\n\r\n--%1%--\r\n" 60 | ) % boundary % key % value).str(); 61 | 62 | // Act 63 | body->addElement(key, value); 64 | const auto& result = body->getBody(); 65 | 66 | // Assert 67 | REQUIRE(expectedBody == result); 68 | } 69 | 70 | SECTION("returns the expected format for elements when multiple elements are set") 71 | { 72 | // Arrange 73 | const auto& key1 = "element_1"; 74 | const auto& value1 = "value_1"; 75 | const auto& key2 = "element_2"; 76 | const auto& value2 = "value_2"; 77 | const auto& key3 = "element_3"; 78 | const auto& value3 = "value_3"; 79 | 80 | const auto& elementTemplate = "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"\r\n\r\n%3%\r\n"; 81 | 82 | const auto& expectedBody = (boost::format("%1%%2%%3%%4%") 83 | % (boost::format(elementTemplate) % boundary % key1 % value1).str() 84 | % (boost::format(elementTemplate) % boundary % key2 % value2).str() 85 | % (boost::format(elementTemplate) % boundary % key3 % value3).str() 86 | % (boost::format("\r\n--%1%--\r\n") % boundary).str() 87 | ).str(); 88 | 89 | // Act 90 | body->addElement(key1, value1); 91 | body->addElement(key2, value2); 92 | body->addElement(key3, value3); 93 | 94 | const auto& result = body->getBody(); 95 | 96 | // Assert 97 | REQUIRE(expectedBody == result); 98 | } 99 | 100 | SECTION("returns the expected format for files when one file is set") 101 | { 102 | // Arrange 103 | const auto& key = "element"; 104 | const auto& filePath = "./bin/test_file.txt"; 105 | const auto& fileContents = "content"; 106 | const auto& expectedBody = (boost::format( 107 | "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"; filename=\"%3%\"\r\n\r\n%4%\r\n\r\n--%1%--\r\n" 108 | ) % boundary % key % filePath % fileContents).str(); 109 | 110 | // Act 111 | body->addFile(key, filePath); 112 | const auto& result = body->getBody(); 113 | 114 | // Assert 115 | REQUIRE(expectedBody == result); 116 | } 117 | 118 | SECTION("returns the expected format for files when multiple files are set") 119 | { 120 | // Arrange 121 | const auto& key1 = "element_1"; 122 | const auto& key2 = "element_2"; 123 | const auto& key3 = "element_3"; 124 | const auto& filePath = "./bin/test_file.txt"; 125 | const auto& fileContents = "content"; 126 | 127 | const auto& fileTemplate = "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"; filename=\"%3%\"\r\n\r\n%4%\r\n"; 128 | 129 | const auto& expectedBody = (boost::format("%1%%2%%3%%4%") 130 | % (boost::format(fileTemplate) % boundary % key1 % filePath % fileContents).str() 131 | % (boost::format(fileTemplate) % boundary % key2 % filePath % fileContents).str() 132 | % (boost::format(fileTemplate) % boundary % key3 % filePath % fileContents).str() 133 | % (boost::format("\r\n--%1%--\r\n") % boundary).str() 134 | ).str(); 135 | 136 | // Act 137 | body->addFile(key1, filePath); 138 | body->addFile(key2, filePath); 139 | body->addFile(key3, filePath); 140 | 141 | const auto& result = body->getBody(); 142 | 143 | // Assert 144 | REQUIRE(expectedBody == result); 145 | } 146 | 147 | SECTION("returns the expected format for both elements and files when both are set") 148 | { 149 | // Arrange 150 | const auto& fileKey1 = "element_1"; 151 | const auto& fileKey2 = "element_2"; 152 | const auto& fileKey3 = "element_3"; 153 | const auto& key1 = "element_1"; 154 | const auto& value1 = "value_1"; 155 | const auto& key2 = "element_2"; 156 | const auto& value2 = "value_2"; 157 | const auto& key3 = "element_3"; 158 | const auto& value3 = "value_3"; 159 | const auto& filePath = "./bin/test_file.txt"; 160 | const auto& fileContents = "content"; 161 | 162 | const auto& elementTemplate = "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"\r\n\r\n%3%\r\n"; 163 | const auto& fileTemplate = "--%1%\r\nContent-Disposition: form-data; name=\"%2%\"; filename=\"%3%\"\r\n\r\n%4%\r\n"; 164 | 165 | const auto& expectedBody = (boost::format("%1%%2%%3%%4%%5%%6%%7%") 166 | % (boost::format(elementTemplate) % boundary % key1 % value1).str() 167 | % (boost::format(elementTemplate) % boundary % key2 % value2).str() 168 | % (boost::format(elementTemplate) % boundary % key3 % value3).str() 169 | % (boost::format(fileTemplate) % boundary % fileKey1 % filePath % fileContents).str() 170 | % (boost::format(fileTemplate) % boundary % fileKey2 % filePath % fileContents).str() 171 | % (boost::format(fileTemplate) % boundary % fileKey3 % filePath % fileContents).str() 172 | % (boost::format("\r\n--%1%--\r\n") % boundary).str() 173 | ).str(); 174 | 175 | // Act 176 | body->addElement(key1, value1); 177 | body->addElement(key2, value2); 178 | body->addElement(key3, value3); 179 | 180 | body->addFile(fileKey1, filePath); 181 | body->addFile(fileKey2, filePath); 182 | body->addFile(fileKey3, filePath); 183 | 184 | const auto& result = body->getBody(); 185 | 186 | // Assert 187 | REQUIRE(expectedBody == result); 188 | } 189 | } 190 | 191 | TEST_CASE("FormDataRequestBody.setElements") 192 | { 193 | const auto& boundary = "--boundary-"; 194 | const auto& body = std::make_shared(boundary); 195 | 196 | SECTION("overwrites the previous set elements through addElement") 197 | { 198 | // Arrange 199 | std::map oldElements = { 200 | { "username", "someone" }, 201 | { "password", "TopSecret!" } 202 | }; 203 | std::map newElements = { 204 | { "token", "81b9ef1d-e5ea-41d9-bdb8-ee08296a59e8" }, 205 | { "refresh-token", "5c8b8ab1-411c-4ff8-a2d6-b97623586364" } 206 | }; 207 | 208 | for (const auto& [key, value] : oldElements) { 209 | body->addElement(key, value); 210 | } 211 | 212 | // Act 213 | body->setElements(newElements); 214 | 215 | // Assert 216 | REQUIRE(oldElements != body->getElements()); 217 | REQUIRE(newElements == body->getElements()); 218 | } 219 | } 220 | 221 | TEST_CASE("FormDataRequestBody.setFiles") 222 | { 223 | const auto& boundary = "--boundary-"; 224 | const auto& body = std::make_shared(boundary); 225 | 226 | SECTION("overwrites the previous set elements through addFile") 227 | { 228 | // Arrange 229 | std::map oldFiles = { 230 | { "image", "./a-butterfly.png" }, 231 | { "resume", "./please-hire-me-resume.odt" } 232 | }; 233 | std::map newFiles = { 234 | { "profile-picture", "../images/happy-me.png" }, 235 | { "passport", "../images/personal/passport_scan.png" } 236 | }; 237 | 238 | for (const auto& [key, value] : oldFiles) { 239 | body->addFile(key, value); 240 | } 241 | 242 | // Act 243 | body->setFiles(newFiles); 244 | 245 | // Assert 246 | REQUIRE(oldFiles != body->getFiles()); 247 | REQUIRE(newFiles == body->getFiles()); 248 | } 249 | } 250 | 251 | TEST_CASE("FormDataRequestBody.setBoundary") 252 | { 253 | SECTION("overwrites the previous set boundary through the constructor") 254 | { 255 | // Arrange 256 | const auto& oldBoundary = "--old-boundary-"; 257 | const auto& newBoundary = "--new-boundary-"; 258 | const auto& body = std::make_shared(oldBoundary); 259 | 260 | // Act 261 | body->setBoundary(newBoundary); 262 | 263 | // Assert 264 | REQUIRE(oldBoundary != body->getBoundary()); 265 | REQUIRE(newBoundary == body->getBoundary()); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # GetIt software design 2 | 3 | This document outlines the design philosofy for GetIt's source code. 4 | 5 | - [1. Class diagram](#1-class-diagram) 6 | - [1.1 Interfaces](#11-interfaces) 7 | - [1.2 Classes](#12-classes) 8 | - [1.3 Layers](#13-layers) 9 | - [2. Design decisions](#2-design-decisions) 10 | 11 | ## 1. Class diagram 12 | 13 | The source code for GetIt is made up of four layers 14 | 15 | 1. [Domain](#131-domain-layer) 16 | 2. [Service](#132-service-layer) 17 | 3. [Data](#133-data-layer) 18 | 4. [Presentation](#134-presentation-layer) 19 | 20 | Each of these layers maps to a specific namespace in which contracts, implementations and factories are defined. Besides the presentation layer, each layer must only expose data types that are either defined in the standard C++ library or use data types defined in the domain layer of GetIt itself. 21 | 22 | To communicate with classes from a different layer a factory must be used. 23 | 24 | ![Class diagram](./uml/class_diagram.png) 25 | 26 | ### 1.1 Interfaces 27 | 28 | As a general rule, all interfaces are defined in the contracts interface. To imply the layer the 29 | 30 | |__Namespace__|__Interface__|__Description__| 31 | |---|---|---| 32 | |`domain::contracts`|`RequestFactory`|To create a new Request, the request factory is used. But to inject the factory itself, an interface is defined which used by the dependent classes.| 33 | |`service::contracts`|`RequestService`|To send a request, the request service is used to send the request object. But to inject the factory that instantiates the service for sending the request, an interface is used by the dependent classes.| 34 | 35 | ### 1.2 Classes 36 | 37 | |__Namespace__|__Class__|__Description__| 38 | |---|---|---| 39 | |`domain::implementations`|`RawRequest`|This class is used to represent a raw http request. This implies the use of a raw body. If the body is a string, that string is directly sent to the server without any parsing beforehand.

\* This class inherits from the Request model.| 40 | |`domain::implementations`|`FormdataRequest`|This class is used to represent a formdata http request. This implies the use of the `multipart/form-data` header. Every element or file that's added to this request is converted to the expected formdata http body, see the IETF RFC 7578 [(Adobe, 2015)](https://datatracker.ietf.org/doc/html/rfc7578#section-4). \*This class inherits from the Request model| 41 | |`domain::factories`|`RequestFactory`|This class is used to instantiate a new Request model implementation based on the parameters. I.e. if the parameters are only applicable to a `RawRequest`, a RawRequest object is instantiated and returned.| 42 | |`domain::models`|`Request`|This is an abstract representation of an actual request. The `getBody` and `getContentType` methods are override by implementations so these are different for the different implementations of a request.| 43 | |`domain::models`|`Response`|This is a representation of a response returned by a server after sending a Request.| 44 | |`service::implementations`|`CppRestRequestService`|Implementation of the RequestService that's using the CppRestRequest library from Microsoft [(Microsoft, n.d.)](https://github.com/microsoft/cpprestsdk). As of right now, this is the only implementation available for sending a request using the RequestService.| 45 | |`service::factories`|`RequestServiceFactory`|Factory that's used to instantiate a new RequestService. Because the CppRestRequest library from Microsoft is the only supported library for sending a request this class only constructs a CppRestRequestService object. However, in the future this factory could be expanded with other request libraries.| 46 | 47 | In the following paragraphs each layer will be explained in more detail. 48 | 49 | ### 1.3 Layers 50 | 51 | GetIt uses the layered pattern, or N-tier pattern, to decouple the different types of partitions 52 | most common applications consist of . This pattern groups classes by responsibility and encourages you to place 53 | them into the same layer [(Price et Al., 2022)](https://docs.microsoft.com/en-us/azure/architecture/guide/architecture-styles/n-tier). Thus meaning that all classes and logic related to data storage or retrieval 54 | are put into the data layer. 55 | 56 | The table below lists and describes the different layers GetIt acknowledges. 57 | 58 | |Layer| Description | 59 | |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 60 | |Domain| The domain layer consists of classes exclusive to the domain. This includes classes such as `Request` that are directly sent to a server.

The domain layer can only contain classes that are dependent on other classes inside the domain layer. This is done to prevent knowledge of anything outside of the request domain. | 61 | |Service| The service layer is used to decouple the domain from the actual implementation of sending a request and retrieving a response from a server.

The service layer can be thought of as the layer that can be used to interact with domain objects. Even though it's possible to interact with the domain objects, they can't be used to send an actual request. | 62 | |Data|The data layer is used to decouple the domain layer from storing domain objects. In the data layer it's possible to create multiple implementations of methods for saving a request, or retrieving a request.

With this decoupling, the domain layer can be used to describe the domain without worrying about any details that are required for saving a request.| 63 | |Presentation|The presentation layer consists of QT-libraries and interaction for the GUI. The presentation layer is only allowed to communicate with the service and data layer. However, the objects from the domain layer are used as models in the MVC pattern.| 64 | 65 | ## 2. Design decisions 66 | 67 | This paragraph describes the different decisions that were made during the design of GetIt. 68 | 69 | |__GDD01__| Raw- and FormdataRequestBody | 70 | |---|---------------------------------------------------------------------------------------------------------------------------------| 71 | |__Decision__| Create a different implementation for the RawRequestBody and FormdataRequestBody which implement from the RequestBody interface | 72 | |__Motives__| When working with a Request you don't need to know the specific body type, therefore an interface `RequestBody` is used | 73 | |__Alternatives__| Use different Request implementations for the different body types | 74 | 75 | |__GDD02__|Abstract factory| 76 | |---|---| 77 | |__Decision__|Use the abstract factory pattern to inject factories into classes| 78 | |__Motives__|To make use of the IOC pattern, the factories are injected as interfaces as well.| 79 | 80 | |__GDD03__|Inversion of Control (IOC)| 81 | |---|---| 82 | |__Decision__|Use inversion of control to communicate with dependencies| 83 | |__Motives__|To communicate with dependencies inversion of control is used to abstract the instantation of classes away from constructors/methods that are dependent on the implementations.| 84 | 85 | |__GDD04__|Constructor injection| 86 | |---|---| 87 | |__Decision__|Dependency injection is implemented with constructor injection| 88 | |__Motives__|By using constructor injection the dependencies need to be built in the `main.cpp` file instead of relaying on the complexity of a dependency injection framework.

However, in the future a DI framework might be implemented without changing the underlying code of GetIt if the DI framework supports constructor injection.| 89 | 90 | |__GDD05__|Construct classes using a factory| 91 | |---|---| 92 | |__Decision__|Only construct new objects using a factory| 93 | |__Motives__|To keep the construction of new classes in a single place, factories are used to construct new instances of objects.

However, some exceptions are expected. But these exceptions only remain in the convert classes or the `main` method. If objects are instantiated outside of a factory, it must be explicitly expressed in a comment.| 94 | 95 | |__GDD06__|Use C++ standard 2017| 96 | |---|---| 97 | |__Decision__| Using the C++ 2017 standard for compiling GetIt | 98 | |__Motives__| When compiling CppRestSDK with C++ standard 2020 the compilation fails due to `implicit capture of ‘this’ via ‘[=]’ is deprecated in C++20` in the `pplxtasks.h` header. | 99 | 100 | |__GDD07__|String literals| 101 | |---|---| 102 | |__Decision__|Only use string literals when there are no line breaks in the string.| 103 | |__Motives__|To keep the code readable and strings on one line, the use of `\r\n` line breaks must be used instead of actual line breaks in the string.| 104 | 105 | 106 | | __GDD08__ | Separate pipeline to create Github release | 107 | |--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 108 | | __Decision__ | Use a different pipeline to create a release based on the latest tag. | 109 | | __Motives__ | To keep the pipelines for packaging GetIt for different platforms independent of each other, the release cannot be created in one of those pipelines. When the pipeline that creates the release fails, the other pipelines subsequently fail as well. Therefore a separate pipeline is the best alternative. | --------------------------------------------------------------------------------