├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile.in ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE.md ├── Makefile ├── Procfile ├── README.md ├── app.json ├── cmd └── server │ ├── convert.go │ ├── convert_test.go │ ├── doc.go │ ├── helpers.go │ ├── helpers_test.go │ ├── main.go │ ├── public │ ├── favicon.ico │ ├── index.html │ └── swagger │ │ ├── index.html │ │ └── swagger.yml │ └── types.go ├── examples └── main_app.go ├── icon.png ├── pkg ├── cache │ ├── doc.go │ ├── memory │ │ ├── doc.go │ │ ├── memory.go │ │ └── memory_test.go │ ├── redis │ │ ├── doc.go │ │ ├── redis.go │ │ └── redis_test.go │ └── types.go ├── exchanger │ ├── 1forge.go │ ├── 1forge_test.go │ ├── currency_list.go │ ├── currencylayer.go │ ├── currencylayer_test.go │ ├── doc.go │ ├── fixer.go │ ├── fixer_test.go │ ├── google.go │ ├── google_test.go │ ├── helpers.go │ ├── openexchangerates.go │ ├── openexchangerates_test.go │ ├── themoneyconverter.go │ ├── themoneyconverter_test.go │ ├── types.go │ ├── yahoo.go │ └── yahoo_test.go └── swap │ ├── doc.go │ ├── swap.go │ ├── swap_test.go │ └── types.go ├── scripts ├── build-docker.sh ├── build-local.sh ├── build.sh ├── docker-entrypoint.sh ├── server-docker.sh ├── server-redis.sh ├── server.sh └── test.sh ├── test ├── scripts │ ├── google.json │ ├── multi_1.json │ ├── request.sh │ └── yahoo.json └── staticMock │ ├── 1forge_json_aed_usd.json │ ├── currencylayer_json_aed_usd.json │ ├── doc.go │ ├── fixer_json_aed_usd.json │ ├── google_html_aed_usd.html │ ├── openexchangerates_json_aed_usd.json │ ├── themoneyconverter_html_aed_usd.html │ ├── transport.go │ └── yahoo_json_aed_usd.json └── version.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | # Unix-style newlines with a newline ending every file 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.json] 15 | indent_size = 2 16 | indent_style = space 17 | 18 | [Makefile] 19 | indent_style = tab 20 | 21 | [makefile] 22 | indent_style = tab 23 | 24 | [*.go] 25 | indent_style = tab 26 | 27 | [*.yml] 28 | indent_style = space 29 | indent_size = 2 30 | end_of_line = lf 31 | charset = utf-8 32 | trim_trailing_whitespace = true 33 | insert_final_newline = true 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | ## Current Behavior 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (for bugs) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * Version used: 30 | * Browser Name and version: 31 | * Operating System and version (desktop or mobile): 32 | * Link to your project: 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate): 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 22 | 23 | ## Checklist: 24 | 25 | 26 | - [ ] My code follows the code style of this project. 27 | - [ ] My change requires a change to the documentation. 28 | - [ ] I have updated the documentation accordingly. 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /.go 3 | /.push-* 4 | /.container-* 5 | /.dockerfile-* 6 | .idea/ 7 | /vendor 8 | /_vendor* 9 | /coverage.txt 10 | # Mac OS X files 11 | .DS_Store 12 | 13 | # Binaries for programs and plugins 14 | *.exe 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, build with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 26 | .glide/ 27 | .env 28 | .docker_build/ 29 | profile.cov 30 | dist 31 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: go-swap-server 2 | before: 3 | hooks: 4 | # - make clean 5 | - go generate ./... 6 | builds: 7 | - main: ./cmd/server/. 8 | binary: go-swap-server 9 | ldflags: 10 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 11 | env: 12 | - CGO_ENABLED=0 13 | 14 | archive: 15 | replacements: 16 | windows: Windows 17 | amd64: 64-bit 18 | 386: 32-bit 19 | darwin: macOS 20 | linux: Linux 21 | format: tar.gz 22 | format_overrides: 23 | - goos: windows 24 | format: zip 25 | files: 26 | - LICENSE.md 27 | - README.md 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: '1.11.x' 3 | 4 | services: 5 | - docker 6 | - redis-server 7 | env: 8 | global: 9 | - secure: 2I0+pzIvKKi9RbmAjOBCS4gQFf7+KTDLIcGpoTPsvMUG6XMRiv5WUV0GWGRtKEpBs/iHOEOL2OkCI1SsN/mMyK/HcRLyotVDht12KILhmQxc5+ApawcaoU0LM4pnMthOztm3y+jX58F8NBqksLt3JCSinLr4eX/wTjsaJnjnGjncACFEiXUZK2566rFoTpJZR+UOCvhGGK6Nq8OWgh4K3OHAIeJB7xjGI8R3YTHkWtqhgQZAmMCbjxQHASqbfgYAFrMwvajnHWUpKtULLt1h4h6VDKoCE7yWyTGiF/8jQ4jDswlwfkRKZhG08AFtQQWf0ow3ojuZzz2SnmzV31zK73hLTuDHBCr3tjMAV69ErdK15FAvgCdlfQS1/MCNOsVcYRhpsNV79Qt644oS7yOh4uJglh9bx49cd1BiEremzsbxNK2MA2s6RmW0dG2ROYeHNXpcowPJIfuKFdHtDhTyza8qLQEiFAGYG64lJ87Hm4SYOT59aoAT7l3wSM+U9uTC8+PS/OyBTfanFe8jJ5L9OVL6v6SRzhLOPxrgcjSCn9HkUIzpRMlxnY4RMFVQ/tdpLl5s/9GRoEk2P3gimIgNlkzid6EzJ3XfcOmxm95RMbH2bBAg4a8qegFT4Q1+j2NnJ4f3AW8e20LQ3x4lPjz+MYtIdLeXLemiIEo2UECsTWU= 10 | - secure: fVf1QTPl1+RWTLO1SCSGglXIgzhQVKBGRqNovE2vJRIxCNqV+qYTXXw/LG3OC3KxMYoyOtV26RTM6CpRqb8ZDn3+1UTi2XRJqtad+CKpPxLn6aWd3wy2lo+FmzAFeHS80LtH/kDumY7joBY6uDQht2buN7k48ZOmydVfQjWxvWfic6AhgjSaAOPWCvVyMiyVRxBeANRzq3Cn63756zgkHTEKndY1t2tskLlKFZYXu0Omx0FTc9hBEIl1aU6U3TKV0Rnn5i7NN6ezTLW9oqAOJUAha/Z8E06a1SO+GDR0bgmgRySOBFgJXaDuYRgyH5sDTzvFZYTCZSagfcAmPWavT3zoW/6X2BVGRCF46FbW9vfviZcQnoIkkHo6cS5DwFm9nH0hFV3T1ygGwdf5kVqPYZbEfoxzV5L3CGsBa5KM+ujugjPp60Fsg+Q5MhxmFlgk9f/39RdU7wvmZWrfL17mIc4S5Np/0FwRcbHHbJrXnC3RG5cWG39QPzeiyyWFyA8+1NkgkfP+EHvyTCfxo8wTqplfD0mYAoPAvzbWv4QZRXSjnxdhyKGJ/poMNkSK6FGomIXipd6C2i1VbBu8YkeOTS3kadwBtZtrZG+Z0g+2WibkoHcKIG/0HM67qMSaHEdvW9qsMSdIUR8c9YPlcVy/4FQs6WDc8ybmwTGH1lpFIfg= 11 | - secure: vEAs85h+3982ndXYR2WNJ1fqhmkvq82ADOYp4DdXAWV2pKQOrVrjZoUyvDpMljJ58UDMlUQtIeohw5q+hacrw8yHNCSlAXG7tQ1Nt+C/q6zgbNj+UnrbuanR4lc2DDUFJ5AN6HjAEVyxXRLkggn0vcwLjaOTq/n9HgXiXIZrnXCeevAA5qPDDmc6PZERwDlaQdGfcggTbLSkI2dktZ6ucka1rrIrhwSzfzGy2TRfySBrV81cXGqR0tRWtWCMBJg9oVTgOJ0tUMqMRd52KXkmHZO7LSxSR08ZdcKawNXbXLg3H54CUVG+YziSXbdD1+dKbkjqOkBwR8P7nqFYa64MMAP8gUbtucPhW0pXZClbHYRlrEHesP1keS2R1B43hgpQLw3IU3x1r/AEwyHYiovuQJmp2Oo2kcvoSzJKTwys0rvNgY4uM6bi3/fyODB5GHTBz2OW7BPoaHJJIgAPz2tu4rcq5XCxhKuqaBK/PXXEJXzvGtL0AkKDMnbmqt+MR5SUOlnKVDYpTzamnhRZOFxcjrAOt+5y1TDCApJNWNXr/EoxD0yNp+8J2hPz40lZo+3yqga+elBHAWTkfflJ+tx+r/uDAfQhnHrBu6QVMOWejWktHGv6gEsawf8HTG+TtOGn3hpYAwQCgEJH9TSZHpZtqJIZAZvUFCryB89qjrMzRIU= 12 | - secure: igx4biTUX8vlp3bW+BasJs5uCAvZpXDdNdFmyUeglwenS3NKAu0Y1fLiL+P0vv5sBNYx/8ABnKzy2y+1nTlEOSLSjd/O6Lcy2BLeifRfsJVmjrM8hHNChFpAObl36GmWCuoUBHiAnxGcobhw+wdtbwXLldFPK+aGjrVpJGz0fxacGH5taDfQJZbZQAuwSf8mBowTbV/qtSt0ARxoI3sNRW6gt0+xZ4fyhDw7rQFfoVDngWZ+HpdH9osKBT1Bbtf3RihXWrSicqRIKBnEVW5yAkz0gpB7jVDUOSOQ97J96Oor6MvgFTSIcx4uCCbI7DEZrT8Q9e4TjNpMBvgOuycV8ExqCBbtnkUvi30sLdaVmwz6ReOAKo2gf2jo/OQvM9y1Qy242boweRgSSk5PdA8CgaM1T4fAao+xmIAdQRlS9LiJ/Bb7eqXZ4zXWwo7mXS56mInEFGDWff4TYAvtjMpnc3zFKUT8dICKmBI6OAPhxXzKkGvQamyMbM08pEzTVq1J70wh97q5vxt7LYv8JM07rc9uGmQw020asn7iAkOso78ifugXn7IsO86NR6enQkLLPXcF1oSFDnTgImozOxtjPimQEz17DttX2iGasjQIo0EBCItJJ90V6l5NNMsc6vs3O3M5l0TniyMrKPe6Gq8NhljK58921Hh8mS+55bg73yE= 13 | before_install: 14 | - export COVERALLS_PARALLEL=true 15 | - export GOMAXPROCS=1 16 | - export GITHUB_TOKEN=$GITHUB_TOKEN_RELEASE 17 | - go get github.com/mattn/goveralls 18 | script: 19 | - make test-docker 20 | - "$GOPATH/bin/goveralls -repotoken $COVERALLS -coverprofile=profile.cov -service=travis-ci" 21 | - make build 22 | - test -f bin/linux-amd64/server 23 | - make container 24 | - docker images | grep meio/go-swap-server 25 | 26 | deploy: 27 | - provider: script 28 | skip_cleanup: true 29 | script: ./scripts/build-docker.sh 30 | on: 31 | tags: true 32 | condition: $TRAVIS_OS_NAME = linux 33 | 34 | - provider: script 35 | skip_cleanup: true 36 | script: curl -sL https://git.io/goreleaser | bash 37 | on: 38 | tags: true 39 | condition: $TRAVIS_OS_NAME = linux 40 | 41 | notifications: 42 | webhooks: 43 | urls: https://coveralls.io/webhook?repo_token=$COVERALLS 44 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ccc@me.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Only one feature or change per pull request 4 | 5 | Make pull requests only one feature or change at the time. Make pull requests from feature branch. Pull requests should not come from your master branch. 6 | 7 | For example you have fixed a bug. You also have optimized some code. Optimization is not related to a bug. These should be submitted as separate pull requests. This way I can easily choose what to include. It is also easier to understand the code changes. 8 | 9 | ## Write meaningful commit messages 10 | 11 | Proper commit message is full sentence. It starts with capital letter but does not end with period. Headlines do not end with period. The GitHub default `Update filename.js` is not enough. When needed include also longer explanation what the commit does. 12 | 13 | ``` 14 | Capitalized, short (50 chars or less) summary 15 | 16 | More detailed explanatory text, if necessary. Wrap it to about 72 17 | characters or so. In some contexts, the first line is treated as the 18 | subject of an email and the rest of the text as the body. The blank 19 | line separating the summary from the body is critical (unless you omit 20 | the body entirely); tools like rebase can get confused if you run the 21 | two together. 22 | ``` 23 | 24 | When in doubt see Tim Pope's blogpost [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 25 | 26 | ## Send coherent history 27 | 28 | Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 29 | 30 | ## Follow the existing coding standards 31 | 32 | When contributing to open source project it is polite to follow the original authors coding standards. They might be different than yours. It is not a holy war. This project uses **[GO Style Coding Standard](https://github.com/golang/go/wiki/CodeReviewComments)** 33 | 34 | ## Running Tests 35 | 36 | You can run individual tests either manually... 37 | 38 | ``` bash 39 | $ Make test-local 40 | $ make test-docker 41 | ``` 42 | 43 | ... or automatically on every code change. You will need [entr](http://entrproject.org/) for this to work. 44 | 45 | -------------------------------------------------------------------------------- /Dockerfile.in: -------------------------------------------------------------------------------- 1 | FROM ARG_FROM 2 | 3 | # Build-time metadata as defined at http://label-schema.org 4 | ARG BUILD_DATE 5 | ARG VCS_REF 6 | ARG VERSION 7 | ARG DOCKER_TAG 8 | 9 | LABEL org.label-schema.build-date=$BUILD_DATE \ 10 | org.label-schema.name="Currency Exchange Server" \ 11 | org.label-schema.description="Currency Exchange Server" \ 12 | org.label-schema.url="https://github.com/me-io/go-swap" \ 13 | org.label-schema.vcs-ref=$VCS_REF \ 14 | org.label-schema.vcs-url="https://github.com/me-io/go-swap" \ 15 | org.label-schema.vendor="ME.IO" \ 16 | org.label-schema.version=$VERSION \ 17 | org.label-schema.schema-version="$DOCKER_TAG" 18 | 19 | RUN apk update \ 20 | && apk upgrade \ 21 | && apk add --no-cache ca-certificates \ 22 | && update-ca-certificates 23 | 24 | ADD bin/ARG_OS-ARG_ARCH/ARG_SRC_BIN /ARG_BIN 25 | ENV BINSRC_ENV="/ARG_BIN" 26 | COPY scripts/docker-entrypoint.sh /usr/local/bin/ 27 | 28 | EXPOSE 5000 29 | 30 | USER nobody:nobody 31 | ENTRYPOINT ["docker-entrypoint.sh"] 32 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:b520b55fc1146c5b0eea03b07233f7a3d4a9be985c037c91ea6b82ecb81bd521" 6 | name = "github.com/bitly/go-simplejson" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "aabad6e819789e569bd6aabf444c935aa9ba1e44" 10 | version = "v0.5.0" 11 | 12 | [[projects]] 13 | digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" 14 | name = "github.com/davecgh/go-spew" 15 | packages = ["spew"] 16 | pruneopts = "UT" 17 | revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" 18 | version = "v1.1.1" 19 | 20 | [[projects]] 21 | digest = "1:c116114869f44c711fa5baf9e019fa327101c82ddc4dcc1de84db1f6c43a2d36" 22 | name = "github.com/go-ozzo/ozzo-validation" 23 | packages = ["."] 24 | pruneopts = "UT" 25 | revision = "106681dbb37bfa3e7683c4c8129cb7f5925ea3e9" 26 | version = "v3.5.0" 27 | 28 | [[projects]] 29 | digest = "1:7c2fd446293ff7799cc496d3446e674ee67902d119f244de645caf95dff1bb98" 30 | name = "github.com/go-redis/redis" 31 | packages = [ 32 | ".", 33 | "internal", 34 | "internal/consistenthash", 35 | "internal/hashtag", 36 | "internal/pool", 37 | "internal/proto", 38 | "internal/singleflight", 39 | "internal/util", 40 | ] 41 | pruneopts = "UT" 42 | revision = "f3bba01df2026fc865f7782948845db9cf44cf23" 43 | version = "v6.14.1" 44 | 45 | [[projects]] 46 | digest = "1:5b3b29ce0e569f62935d9541dff2e16cc09df981ebde48e82259076a73a3d0c7" 47 | name = "github.com/op/go-logging" 48 | packages = ["."] 49 | pruneopts = "UT" 50 | revision = "b2cb9fa56473e98db8caba80237377e83fe44db5" 51 | version = "v1" 52 | 53 | [[projects]] 54 | digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" 55 | name = "github.com/pmezard/go-difflib" 56 | packages = ["difflib"] 57 | pruneopts = "UT" 58 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 59 | version = "v1.0.0" 60 | 61 | [[projects]] 62 | digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" 63 | name = "github.com/stretchr/testify" 64 | packages = ["assert"] 65 | pruneopts = "UT" 66 | revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" 67 | version = "v1.2.2" 68 | 69 | [solve-meta] 70 | analyzer-name = "dep" 71 | analyzer-version = 1 72 | input-imports = [ 73 | "github.com/bitly/go-simplejson", 74 | "github.com/go-ozzo/ozzo-validation", 75 | "github.com/go-redis/redis", 76 | "github.com/op/go-logging", 77 | "github.com/stretchr/testify/assert", 78 | ] 79 | solver-name = "gps-cdcl" 80 | solver-version = 1 81 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | [metadata] 28 | codename = "go-swap-server" 29 | 30 | required = [ 31 | "github.com/bitly/go-simplejson", 32 | "github.com/go-redis/redis", 33 | "github.com/op/go-logging", 34 | "github.com/go-ozzo/ozzo-validation", 35 | "github.com/stretchr/testify" 36 | ] 37 | 38 | 39 | [[constraint]] 40 | name = "github.com/bitly/go-simplejson" 41 | version = "0.5.0" 42 | 43 | [[constraint]] 44 | name = "github.com/go-redis/redis" 45 | version = "6.14.1" 46 | 47 | [[constraint]] 48 | name = "github.com/op/go-logging" 49 | version = "1.0.0" 50 | 51 | [[constraint]] 52 | name = "github.com/stretchr/testify" 53 | version = "1.2.2" 54 | 55 | [prune] 56 | go-tests = true 57 | unused-packages = true 58 | 59 | 60 | [metadata.heroku] 61 | root-package = "github.com/me-io/go-swap" 62 | go-version = "1.11" 63 | install = ["./cmd/server/..."] 64 | ensure = "true" 65 | 66 | [[constraint]] 67 | name = "github.com/go-ozzo/ozzo-validation" 68 | version = "3.5.0" 69 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ME.IO 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # The binary to build (just the basename). 2 | BIN := go-swap-server 3 | SRC_BIN := server 4 | 5 | # This repo's root import path (under GOPATH). 6 | PKG := github.com/me-io/go-swap 7 | 8 | # Where to push the docker image. 9 | REGISTRY ?= meio 10 | 11 | # Which architecture to build - see $(BUILD_PLATFORMS) for options. 12 | ARCH ?= amd64 13 | OS ?= linux 14 | 15 | # This version-strategy uses git tags to set the version string 16 | VERSION := $(shell git describe --tags --always --dirty) 17 | # 18 | # This version-strategy uses a manual value to set the version string 19 | #VERSION := 1.2.3 20 | 21 | ### 22 | ### These variables should not need tweaking. 23 | ### 24 | 25 | SRC_DIRS := cmd pkg # directories which hold app source (not vendored) 26 | 27 | ## 28 | REDIS_URL ?= redis://localhost:6379 29 | 30 | # $(OS)-$(ARCH) pairs to build binaries and containers for 31 | BUILD_PLATFORMS := linux-amd64 linux-arm linux-arm64 linux-ppc64le freebsd-amd64 freebsd-386 32 | CONTAINER_PLATFORMS := linux-amd64 linux-arm64 linux-ppc64le # must be a subset of BUILD_PLATFORMS 33 | 34 | # Set default base image dynamically for each arch 35 | ifeq ($(ARCH),amd64) 36 | BASEIMAGE?=alpine 37 | endif 38 | ifeq ($(ARCH),arm) 39 | BASEIMAGE?=armel/busybox 40 | endif 41 | ifeq ($(ARCH),arm64) 42 | BASEIMAGE?=aarch64/busybox 43 | endif 44 | ifeq ($(ARCH),ppc64le) 45 | BASEIMAGE?=ppc64le/busybox 46 | endif 47 | 48 | IMAGE := $(REGISTRY)/$(BIN)-$(OS)-$(ARCH) 49 | 50 | BUILD_IMAGE ?= golang:1.11-alpine 51 | 52 | # If you want to build all binaries, see the 'all-build' rule. 53 | # If you want to build all containers, see the 'all-container' rule. 54 | # If you want to build AND push all containers, see the 'all-push' rule. 55 | all: build 56 | 57 | build-%: 58 | @$(MAKE) --no-print-directory ARCH=$(word 2,$(subst -, ,$*)) OS=$(word 1,$(subst -, ,$*)) build 59 | 60 | container-%: 61 | @$(MAKE) --no-print-directory ARCH=$(word 2,$(subst -, ,$*)) OS=$(word 1,$(subst -, ,$*)) container 62 | 63 | push-%: 64 | @$(MAKE) --no-print-directory ARCH=$(word 2,$(subst -, ,$*)) OS=$(word 1,$(subst -, ,$*)) push 65 | 66 | all-build: $(addprefix build-, $(BUILD_PLATFORMS)) 67 | 68 | all-container: $(addprefix container-, $(CONTAINER_PLATFORMS)) 69 | 70 | all-push: $(addprefix push-, $(CONTAINER_PLATFORMS)) 71 | 72 | build: bin/$(OS)-$(ARCH)/$(BIN) 73 | 74 | bin/$(OS)-$(ARCH)/$(BIN): build-dirs 75 | ifeq ($(filter $(OS)-$(ARCH),$(BUILD_PLATFORMS)),) 76 | $(error unsupported build platform $(OS)-$(ARCH) not in $(BUILD_PLATFORMS)) 77 | endif 78 | @echo "building: $@" 79 | @docker run \ 80 | -ti \ 81 | --rm \ 82 | -u $$(id -u):$$(id -g) \ 83 | -v "$$(pwd)/.go:/go" \ 84 | -v "$$(pwd):/go/src/$(PKG)" \ 85 | -v "$$(pwd)/bin/$(OS)-$(ARCH):/go/bin" \ 86 | -v "$$(pwd)/bin/$(OS)-$(ARCH):/go/bin/$(OS)_$(ARCH)" \ 87 | -v "$$(pwd)/.go/std/$(OS)-$(ARCH):/usr/local/go/pkg/$(OS)_$(ARCH)_static" \ 88 | -v "$$(pwd)/.go/cache:/.cache" \ 89 | -w /go/src/$(PKG) \ 90 | $(BUILD_IMAGE) \ 91 | /bin/sh -c " \ 92 | ARCH=$(ARCH) \ 93 | OS=$(OS) \ 94 | VERSION=$(VERSION) \ 95 | PKG=$(PKG) \ 96 | ./scripts/build.sh \ 97 | " 98 | 99 | # Example: make shell CMD="-c 'date > datefile'" 100 | shell: build-dirs 101 | @echo "launching a shell in the containerized build environment" 102 | @docker run \ 103 | -ti \ 104 | --rm \ 105 | -u $$(id -u):$$(id -g) \ 106 | -v "$$(pwd)/.go:/go" \ 107 | -v "$$(pwd):/go/src/$(PKG)" \ 108 | -v "$$(pwd)/bin/$(OS)-$(ARCH):/go/bin" \ 109 | -v "$$(pwd)/bin/$(OS)-$(ARCH):/go/bin/$(OS)_$(ARCH)" \ 110 | -v "$$(pwd)/.go/std/$(OS)-$(ARCH):/usr/local/go/pkg/$(OS)_$(ARCH)_static" \ 111 | -v "$$(pwd)/.go/cache:/.cache" \ 112 | -w /go/src/$(PKG) \ 113 | $(BUILD_IMAGE) \ 114 | /bin/sh $(CMD) 115 | 116 | DOTFILE_IMAGE = $(subst :,_,$(subst /,_,$(IMAGE))-$(VERSION)) 117 | 118 | container: .container-$(DOTFILE_IMAGE) container-name 119 | .container-$(DOTFILE_IMAGE): bin/$(OS)-$(ARCH)/$(BIN) Dockerfile.in 120 | @sed \ 121 | -e 's|ARG_BIN|$(BIN)|g' \ 122 | -e 's|ARG_SRC_BIN|$(SRC_BIN)|g' \ 123 | -e 's|ARG_OS|$(OS)|g' \ 124 | -e 's|ARG_ARCH|$(ARCH)|g' \ 125 | -e 's|ARG_FROM|$(BASEIMAGE)|g' \ 126 | Dockerfile.in > .dockerfile-$(OS)-$(ARCH) 127 | @docker build -t $(IMAGE):$(VERSION) -f .dockerfile-$(OS)-$(ARCH) . 128 | @docker images -q $(IMAGE):$(VERSION) > $@ 129 | 130 | container-name: 131 | @echo "container: $(IMAGE):$(VERSION)" 132 | 133 | push: .push-$(DOTFILE_IMAGE) push-name 134 | .push-$(DOTFILE_IMAGE): .container-$(DOTFILE_IMAGE) 135 | ifeq ($(findstring gcr.io,$(REGISTRY)),gcr.io) 136 | @gcloud docker -- push $(IMAGE):$(VERSION) 137 | else 138 | @docker push $(IMAGE):$(VERSION) 139 | endif 140 | @docker images -q $(IMAGE):$(VERSION) > $@ 141 | 142 | push-name: 143 | @echo "pushed: $(IMAGE):$(VERSION)" 144 | 145 | version: 146 | @echo $(VERSION) 147 | 148 | test-local: 149 | REDIS_URL=${REDIS_URL} go test -v ./... 150 | 151 | test-docker: build-dirs 152 | @dep version >/dev/null 2>&1 || ( wget -O - https://raw.githubusercontent.com/golang/dep/master/install.sh | sh ) 153 | @dep ensure -vendor-only 154 | @docker container rm go-swap-server-redis -f > /dev/null 2>&1 || true 155 | @docker run \ 156 | -ti \ 157 | --rm \ 158 | --name go-swap-server-redis \ 159 | -d redis:alpine redis-server --appendonly yes 160 | @docker run \ 161 | -e "REDIS_URL=redis://redis:6379" \ 162 | --link go-swap-server-redis:redis \ 163 | -ti \ 164 | --rm \ 165 | -u $$(id -u):$$(id -g) \ 166 | -v "$$(pwd)/.go:/go" \ 167 | -v "$$(pwd):/go/src/$(PKG)" \ 168 | -v "$$(pwd)/bin/$(OS)-$(ARCH):/go/bin" \ 169 | -v "$$(pwd)/.go/std/$(OS)-$(ARCH):/usr/local/go/pkg/$(OS)_$(ARCH)_static" \ 170 | -v "$$(pwd)/.go/cache:/.cache" \ 171 | -w /go/src/$(PKG) \ 172 | $(BUILD_IMAGE) \ 173 | /bin/sh -c " \ 174 | ./scripts/test.sh $(SRC_DIRS) \ 175 | " ; \ 176 | docker container rm go-swap-server-redis -f > /dev/null 2>&1 177 | 178 | build-dirs: 179 | @mkdir -p bin/$(OS)-$(ARCH) 180 | @mkdir -p .go/cache .go/src/$(PKG) .go/pkg .go/bin .go/std/$(OS)-$(ARCH) 181 | 182 | clean: container-clean bin-clean 183 | 184 | container-clean: 185 | rm -rf .container-* .dockerfile-* .push-* dist 186 | 187 | bin-clean: 188 | rm -rf .go bin 189 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/server -P=$PORT -CACHE=$CACHE_ENV -REDIS_URL=$REDIS_URL -STATIC_PATH=/app/cmd/server/public 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Currency Exchange Server - Golang 3 | 4 | [![Build Status](https://travis-ci.org/me-io/go-swap.svg?branch=master)](https://travis-ci.org/me-io/go-swap) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/me-io/go-swap)](https://goreportcard.com/report/github.com/me-io/go-swap) 6 | [![Coverage Status](https://coveralls.io/repos/github/me-io/go-swap/badge.svg?branch=master)](https://coveralls.io/github/me-io/go-swap?branch=master) 7 | [![GoDoc](https://godoc.org/github.com/me-io/go-swap?status.svg)](https://godoc.org/github.com/me-io/go-swap) 8 | [![GitHub release](https://img.shields.io/github/release/me-io/go-swap.svg)](https://github.com/me-io/go-swap/releases) 9 | 10 | 11 | [![Blog URL](https://img.shields.io/badge/Author-blog-green.svg?style=flat-square)](https://meabed.com) 12 | [![COMMIT](https://images.microbadger.com/badges/commit/meio/go-swap-server.svg)](https://microbadger.com/images/meio/go-swap-server) 13 | [![SIZE-LAYERS](https://images.microbadger.com/badges/image/meio/go-swap-server.svg)](https://microbadger.com/images/meio/go-swap-server) 14 | [![Pulls](https://shields.beevelop.com/docker/pulls/meio/go-swap-server.svg?style=flat-square)](https://hub.docker.com/r/meio/go-swap-server) 15 | 16 | Swap allows you to retrieve currency exchange rates from various services such as **[Google](https://google.com)**, **[Yahoo](https://yahoo.com)**, **[Fixer](https://fixer.io)**, **[CurrencyLayer](https://currencylayer.com)** or **[1Forge](https://1forge.com)** 17 | and optionally cache the results. 18 | 19 | ## Playground 20 | 21 | Swagger UI 22 | 23 | 24 | heroku test instance @ https://go-swap-server.herokuapp.com 25 | 26 | 27 | #### /GET Examples for single exchanger: 28 | - [GET /convert?from=USD&to=AED&amount=2&exchanger=yahoo](https://go-swap-server.herokuapp.com/convert?from=USD&to=AED&amount=100&exchanger=yahoo) 29 | - [GET /convert?from=EUR&to=GBP&amount=1&exchanger=google&cacheTime=300s](https://go-swap-server.herokuapp.com/convert?from=EUR&to=GBP&amount=100&exchanger=google&cacheTime=300s) 30 | - [GET /convert?from=USD&to=SAR&amount=1&exchanger=themoneyconverter](https://go-swap-server.herokuapp.com/convert?from=USD&to=SAR&amount=100&exchanger=themoneyconverter) 31 | 32 | #### /POST Examples for single or multi exchanger: 33 | - CURL examples: 34 | ```bash 35 | curl -X POST \ 36 | https://go-swap-server.herokuapp.com/convert \ 37 | -H 'Content-Type: application/json' \ 38 | -d '{ 39 | "amount": 2.5, 40 | "from": "USD", 41 | "to": "AED", 42 | "decimalPoints": 4, 43 | "cacheTime": "120s", 44 | "exchanger": [ 45 | { 46 | "name": "yahoo" 47 | }, 48 | { 49 | "name": "google" 50 | }, 51 | { 52 | "name": "themoneyconverter", 53 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 54 | } 55 | ] 56 | }' 57 | 58 | # Response example 59 | # { 60 | # "to": "AED", 61 | # "from": "USD", 62 | # "exchangerName": "yahoo", 63 | # "exchangeValue": 3.6721, 64 | # "originalAmount": 2.5, 65 | # "convertedAmount": 9.1802, 66 | # "convertedText": "2.5 USD is worth 9.1802 AED", 67 | # "rateDateTime": "2018-09-30T07:45:45Z", 68 | # "rateFromCache": false 69 | # } 70 | ``` 71 | - [Run in SwaggerUI](https://go-swap-server.herokuapp.com/swagger) 72 | - [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/5f8445ef9a390fd3faa1) 73 | 74 | ## QuickStart 75 | 76 | 77 | Deploy 78 | 79 |
80 | 81 | 82 | 83 | ```bash 84 | # Or using docker 85 | $ docker pull meio/go-swap-server:latest && \ 86 | docker run --rm --name go-swap-server -p 5000:5000 -it meio/go-swap-server:latest 87 | ``` 88 | 89 | ### Programmatically 90 | ```bash 91 | $ go get github.com/me-io/go-swap 92 | ``` 93 | 94 | ```go 95 | package main 96 | 97 | import ( 98 | "fmt" 99 | ex "github.com/me-io/go-swap/pkg/exchanger" 100 | "github.com/me-io/go-swap/pkg/swap" 101 | ) 102 | 103 | func main() { 104 | SwapTest := swap.NewSwap() 105 | 106 | SwapTest. 107 | AddExchanger(ex.NewGoogleApi(nil)). 108 | Build() 109 | 110 | euroToUsdRate := SwapTest.Latest("EUR/USD") 111 | fmt.Println(euroToUsdRate.GetRateValue()) 112 | } 113 | 114 | ``` 115 | 116 | ## Features 117 | - Convert with Single exchange source `/GET` 118 | - Convert with Multi exchange sources with fallback mechanism `/POST` 119 | - Google 120 | - Yahoo 121 | - CurrencyLayer 122 | - Fixer.io 123 | - themoneyconverter.com 124 | - openexchangerates.org 125 | - 1forge.com 126 | - Rate Caching - `120s Default` 127 | - Memory - `Default` 128 | - Redis 129 | - Rate decimal points rounding `4 Default` 130 | - Swagger UI 131 | - Clear API Request and Response 132 | - Docker image, Binary release and Heroku Demo 133 | - Clear documentation and 90%+ code coverage 134 | - Unit tested on live and mock data 135 | 136 | ### Screens 137 | 138 | 139 | ## Documentation 140 | The documentation for the current branch can be found [here](#documentation). 141 | 142 | 143 | ## Services 144 | |Exchanger |type |# |$| 145 | |:--- |:---- |:--- |:---| 146 | |[Google][1] |HTML / Regex |:heavy_check_mark: |Free| 147 | |[Yahoo][2] |JSON / API |:heavy_check_mark: |Free| 148 | |[Currency Layer][3] |JSON / API |:heavy_check_mark: |Paid - ApiKey| 149 | |[Fixer.io][4] |JSON / API |:heavy_check_mark: |Paid - ApiKey| 150 | |[1forge][7] |API |:heavy_check_mark: |Freemium / Paid - ApiKey| 151 | |[The Money Converter][5] |HTML / Regex |:heavy_check_mark: |Free| 152 | |[Open Exchange Rates][6] |API |:heavy_check_mark: |Freemium / Paid - ApiKey| 153 | 154 | [1]: //google.com 155 | [2]: //yahoo.com 156 | [3]: //currencylayer.com 157 | [4]: //fixer.io 158 | [5]: //themoneyconverter.com 159 | [6]: //openexchangerates.org 160 | [7]: //1forge.com 161 | 162 | ### Uptime Monitor 163 | 164 | Deploy 165 | 166 | 167 | ## TODO LIST 168 | - [ ] error structure for empty json or regex not matched 169 | - [ ] convert panic to api json error 170 | - [ ] increase tests 171 | - [ ] verbose logging 172 | - [ ] godoc 173 | - [ ] static bundle public folder `./cmd/server/public` 174 | - [ ] v 1.0.0 release ( docker / binary github / homebrew mac ) 175 | - [ ] support historical rates if possible 176 | - [ ] benchmark & performance optimization ` memory leak` 177 | - [ ] contributors list 178 | 179 | ## Contributing 180 | 181 | Anyone is welcome to [contribute](CONTRIBUTING.md), however, if you decide to get involved, please take a moment to review the guidelines: 182 | 183 | * [Only one feature or change per pull request](CONTRIBUTING.md#only-one-feature-or-change-per-pull-request) 184 | * [Write meaningful commit messages](CONTRIBUTING.md#write-meaningful-commit-messages) 185 | * [Follow the existing coding standards](CONTRIBUTING.md#follow-the-existing-coding-standards) 186 | 187 | #### Credits 188 | > Inspired by [florianv/swap](https://github.com/florianv/swap) 189 | 190 | ## License 191 | 192 | The code is available under the [MIT license](LICENSE.md). 193 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Currency Converter Server", 3 | "description": "Golang Currency Converter Server", 4 | "website": "https://github.com/me-io/go-swap", 5 | "repository": "https://github.com/me-io/go-swap", 6 | "logo": "https://raw.githubusercontent.com/me-io/go-swap/master/icon.png", 7 | "keywords": [ 8 | "golang", 9 | "currency", 10 | "currencylayer", 11 | "yahoo currency", 12 | "google currency", 13 | "fixer", 14 | "1forge" 15 | ], 16 | "success_url": "/", 17 | "stack": "heroku-16", 18 | "image": "heroku/go:latest", 19 | "formation": { 20 | "web": { 21 | "quantity": 1, 22 | "size": "free" 23 | } 24 | }, 25 | "addons": [ 26 | "heroku-redis:hobby-dev" 27 | ], 28 | "env": { 29 | "CACHE_ENV": { 30 | "description": "Cache driver ( memory or redis )", 31 | "value": "redis" 32 | } 33 | }, 34 | "buildpacks": [ 35 | { 36 | "url": "heroku/go" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /cmd/server/convert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/go-ozzo/ozzo-validation" 9 | ex "github.com/me-io/go-swap/pkg/exchanger" 10 | "github.com/me-io/go-swap/pkg/swap" 11 | "io/ioutil" 12 | "math" 13 | "net/http" 14 | "time" 15 | ) 16 | 17 | // Validate ... Validation function for convertReqObj 18 | func (c *convertReqObj) Validate() error { 19 | 20 | return validation.ValidateStruct(c, 21 | validation.Field(&c.Amount, validation.Required), 22 | validation.Field(&c.From, validation.Required, validation.In(ex.CurrencyListArr...)), 23 | validation.Field(&c.To, validation.Required, validation.In(ex.CurrencyListArr...)), 24 | validation.Field(&c.Exchanger, validation.Required), 25 | ) 26 | 27 | //if ex.CurrencyList[c.To] == "" || ex.CurrencyList[c.From] == "" { 28 | // return fmt.Errorf("currency %s or %s is not supported", c.From, c.To) 29 | //} 30 | } 31 | 32 | // Hash ... return md5 string hash of the convertReqObj with 1 Unit Amount to cache the rate only for 1 Unit Amount 33 | func (c convertReqObj) Hash() string { 34 | // hash exchange key only with 1 Unit value 35 | c.Amount = 1 36 | jsonBytes, _ := json.Marshal(c) 37 | md5Sum := md5.Sum(jsonBytes) 38 | return fmt.Sprintf("%x", md5Sum[:]) 39 | } 40 | 41 | // Convert ... Main convert function attached to the router handler 42 | var Convert = func(w http.ResponseWriter, r *http.Request) { 43 | if r.Method == "POST" { 44 | ConvertPost(w, r) 45 | } 46 | if r.Method == "GET" { 47 | ConvertGet(w, r) 48 | } 49 | } 50 | 51 | // ConvertGet ... handle GET request and simulate payload from get query params to ConvertPost function 52 | var ConvertGet = func(w http.ResponseWriter, r *http.Request) { 53 | 54 | query := r.URL.Query() 55 | apiKey := query.Get("apiKey") 56 | exchanger := query.Get("exchanger") 57 | amount := query.Get("amount") 58 | from := query.Get("from") 59 | to := query.Get("to") 60 | cacheTime := query.Get("cacheTime") 61 | 62 | payload := fmt.Sprintf(`{ 63 | "amount": %s, 64 | "exchanger": [ 65 | { 66 | "name": "%s", 67 | "apiKey": "%s" 68 | } 69 | ], 70 | "from": "%s", 71 | "to": "%s", 72 | "cacheTime":"%s" 73 | }`, amount, exchanger, apiKey, from, to, cacheTime) 74 | 75 | bytePayload := []byte(payload) 76 | bytePayloadReader := bytes.NewReader(bytePayload) 77 | 78 | r.Body = ioutil.NopCloser(bytePayloadReader) 79 | ConvertPost(w, r) 80 | } 81 | 82 | // ConvertPost ... handle POST request, build Swap object, get and cache the currency exchange rate and amount 83 | var ConvertPost = func(w http.ResponseWriter, r *http.Request) { 84 | 85 | convertReq := &convertReqObj{} 86 | 87 | if err := json. 88 | NewDecoder(r.Body). 89 | Decode(convertReq); err != nil { 90 | Logger.Panic(err) 91 | } 92 | 93 | if err := convertReq.Validate(); err != nil { 94 | Logger.Panic(err) 95 | } 96 | 97 | decimalPoint := convertReq.DecimalPoints 98 | if decimalPoint == 0 { 99 | decimalPoint = 4 100 | } 101 | 102 | currencyCacheKey := convertReq.Hash() 103 | 104 | currencyCachedVal := Storage.Get(currencyCacheKey) 105 | // default cache time 106 | if convertReq.CacheTime == "" { 107 | convertReq.CacheTime = "120s" 108 | } 109 | currencyCacheTime, _ := time.ParseDuration(convertReq.CacheTime) 110 | 111 | convertRes := &convertResObj{} 112 | if string(currencyCachedVal) == "" { 113 | Swap := swap.NewSwap() 114 | for _, v := range convertReq.Exchanger { 115 | 116 | var e ex.Exchanger 117 | opt := map[string]string{`userAgent`: v.UserAgent, `apiKey`: v.ApiKey, `apiVersion`: v.ApiVersion} 118 | 119 | switch v.Name { 120 | case `google`: 121 | e = ex.NewGoogleApi(opt) 122 | break 123 | case `yahoo`: 124 | e = ex.NewYahooApi(opt) 125 | break 126 | case `currencylayer`: 127 | e = ex.NewCurrencyLayerApi(opt) 128 | break 129 | case `fixer`: 130 | e = ex.NewFixerApi(opt) 131 | break 132 | case `1forge`: 133 | e = ex.NewOneForgeApi(opt) 134 | break 135 | case `themoneyconverter`: 136 | e = ex.NewTheMoneyConverterApi(opt) 137 | break 138 | case `openexchangerates`: 139 | e = ex.NewOpenExchangeRatesApi(opt) 140 | break 141 | } 142 | Swap.AddExchanger(e) 143 | } 144 | Swap.Build() 145 | 146 | rate := Swap.Latest(convertReq.From + `/` + convertReq.To) 147 | 148 | convertRes.From = convertReq.From 149 | convertRes.To = convertReq.To 150 | convertRes.ExchangeValue = rate.GetRateValue() 151 | convertRes.RateDateTime = rate.GetRateDateTime() 152 | convertRes.ExchangerName = rate.GetExchangerName() 153 | convertRes.RateFromCache = false 154 | 155 | var err error 156 | if currencyCachedVal, err = json.Marshal(convertRes); err != nil { 157 | Logger.Panic(err) 158 | } 159 | Storage.Set(currencyCacheKey, currencyCachedVal, currencyCacheTime) 160 | w.Header().Set("X-Cache", "Miss") 161 | } else { 162 | // get from cache 163 | w.Header().Set("X-Cache", "Hit") 164 | json.Unmarshal(currencyCachedVal, &convertRes) 165 | convertRes.RateFromCache = true 166 | } 167 | 168 | convertedAmount := math.Round(convertReq.Amount*convertRes.ExchangeValue*math.Pow10(decimalPoint)) / math.Pow10(decimalPoint) 169 | convertRes.ConvertedAmount = convertedAmount 170 | convertRes.OriginalAmount = convertReq.Amount 171 | 172 | // formatted message like "1 USD is worth 3.675 AED" 173 | convertRes.ConvertedText = fmt.Sprintf("%g %s is worth %g %s", convertRes.OriginalAmount, convertRes.From, convertRes.ConvertedAmount, convertRes.To) 174 | 175 | currencyJsonVal, err := json.Marshal(convertRes) 176 | if err != nil { 177 | Logger.Panic(err) 178 | } 179 | 180 | //Set Content-Type header so that clients will know how to read response 181 | w.Header().Set("Content-Type", "application/json") 182 | w.WriteHeader(http.StatusOK) 183 | w.Write(currencyJsonVal) 184 | } 185 | -------------------------------------------------------------------------------- /cmd/server/convert_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/stretchr/testify/assert" 6 | "io/ioutil" 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | type testResponseWriter struct { 12 | } 13 | 14 | var testResponse string 15 | 16 | func (c testResponseWriter) Header() http.Header { 17 | return map[string][]string{} 18 | } 19 | 20 | func (c testResponseWriter) Write(i []byte) (int, error) { 21 | testResponse = string(i) 22 | return 0, nil 23 | } 24 | 25 | func (c testResponseWriter) WriteHeader(statusCode int) { 26 | } 27 | 28 | func TestConvertObj_Convert(t *testing.T) { 29 | payloadArr := []string{ 30 | `{ 31 | "amount": 4.5, 32 | "exchanger": [ 33 | { 34 | "name": "google", 35 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 36 | } 37 | ], 38 | "from": "USD", 39 | "to": "AED" 40 | }`, 41 | `{ 42 | "amount": 4.5, 43 | "exchanger": [ 44 | { 45 | "name": "yahoo", 46 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 47 | } 48 | ], 49 | "from": "USD", 50 | "to": "AED" 51 | }`, `{ 52 | "amount": 5.5, 53 | "exchanger": [ 54 | { 55 | "name": "yahoo", 56 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 57 | } 58 | ], 59 | "from": "USD", 60 | "to": "AED" 61 | }`, 62 | `{ 63 | "amount": 4.5, 64 | "exchanger": [ 65 | { 66 | "name": "google", 67 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 68 | }, 69 | { 70 | "name": "yahoo", 71 | "userAgent": "Chrome" 72 | }, 73 | { 74 | "name": "currencyLayer", 75 | "apiKey": "12312", 76 | "userAgent": "currencyLayer Chrome" 77 | }, 78 | { 79 | "name": "fixer", 80 | "apiKey": "12312", 81 | "userAgent": "currencyLayer fixer" 82 | } 83 | ], 84 | "from": "USD", 85 | "to": "AED" 86 | }`, 87 | } 88 | 89 | expectedName := map[int]string{ 90 | 0: "google", 91 | 1: "yahoo", 92 | 2: "yahoo", 93 | 3: "google", 94 | } 95 | expectedRateCache := map[int]string{ 96 | 0: "false", 97 | 1: "false", 98 | 2: "true", 99 | 3: "false", 100 | } 101 | 102 | for k, payload := range payloadArr { 103 | // mock the payload 104 | bytePayload := []byte(payload) 105 | bytePayloadReader := bytes.NewReader(bytePayload) 106 | 107 | w := testResponseWriter{} 108 | 109 | r := &http.Request{} 110 | r.Body = ioutil.NopCloser(bytePayloadReader) 111 | 112 | ConvertPost(w, r) 113 | assert.Contains(t, testResponse, expectedName[k]) 114 | assert.Contains(t, testResponse, expectedRateCache[k]) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /cmd/server/doc.go: -------------------------------------------------------------------------------- 1 | package main // import "github.com/me-io/go-swap/cmd/server" 2 | -------------------------------------------------------------------------------- /cmd/server/helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // LsFiles ... list file in directory 11 | func LsFiles(pattern string) { 12 | err := filepath.Walk(pattern, 13 | func(path string, info os.FileInfo, err error) error { 14 | if err != nil { 15 | return err 16 | } 17 | fmt.Println(path, info.Size()) 18 | return nil 19 | }) 20 | if err != nil { 21 | log.Println(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/server/helpers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/me-io/go-swap/pkg/cache" 7 | "github.com/me-io/go-swap/pkg/cache/memory" 8 | "github.com/me-io/go-swap/pkg/cache/redis" 9 | "github.com/op/go-logging" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "time" 15 | ) 16 | 17 | var ( 18 | host *string 19 | port *int 20 | cacheDriver *string 21 | redisUrl *string 22 | // Storage ... Server Cache Storage 23 | Storage cache.Storage 24 | // Logger ... Logger Driver 25 | Logger = logging.MustGetLogger("go-swap-server") 26 | 27 | format = logging.MustStringFormatter( 28 | `%{color}%{time:2006-01-02T15:04:05.999999} %{shortfunc} ▶ %{level:.8s} %{id:03x}%{color:reset} %{message}`, 29 | ) 30 | 31 | routes = map[string]func(w http.ResponseWriter, r *http.Request){ 32 | `/convert`: Convert, 33 | } 34 | _, filename, _, _ = runtime.Caller(0) 35 | defaultStaticPath = filepath.Dir(filename) + `/public` 36 | staticPath = &defaultStaticPath 37 | ) 38 | 39 | // init ... init function of the server 40 | func init() { 41 | // Logging 42 | backendStderr := logging.NewLogBackend(os.Stderr, "", 0) 43 | backendFormatted := logging.NewBackendFormatter(backendStderr, format) 44 | // Only DEBUG and more severe messages should be sent to backend1 45 | backendLevelFormatted := logging.AddModuleLevel(backendFormatted) 46 | backendLevelFormatted.SetLevel(logging.DEBUG, "") 47 | // Set the backend to be used. 48 | logging.SetBackend(backendLevelFormatted) 49 | 50 | // Caching 51 | host = flag.String(`H`, `0.0.0.0`, `Host binding address`) 52 | port = flag.Int(`P`, 5000, `Host binding port`) 53 | cacheDriver = flag.String(`CACHE`, `memory`, `Cache driver (default memory)`) 54 | redisUrl = flag.String(`REDIS_URL`, ``, `Redis URI for redis cache driver`) 55 | staticPath = flag.String(`STATIC_PATH`, defaultStaticPath, `Webserver static path`) 56 | 57 | flag.Parse() 58 | 59 | var err error 60 | 61 | switch *cacheDriver { 62 | case `redis`: 63 | if Storage, err = redis.NewStorage(*redisUrl); err != nil { 64 | Logger.Panic(err) 65 | } 66 | break 67 | default: 68 | Storage = memory.NewStorage() 69 | } 70 | 71 | } 72 | 73 | // main ... main function start the server 74 | func main() { 75 | 76 | Logger.Infof("host %s", *host) 77 | Logger.Infof("port %d", *port) 78 | Logger.Infof("cacheDriver %s", *cacheDriver) 79 | Logger.Infof("REDIS_URL %s", *redisUrl) 80 | Logger.Infof("Static dir %s", *staticPath) 81 | 82 | // handle routers 83 | for k, v := range routes { 84 | http.HandleFunc(k, v) 85 | } 86 | 87 | go serveHTTP(*host, *port) 88 | select {} 89 | } 90 | 91 | // serveHTTP ... initiate the HTTP Server 92 | func serveHTTP(host string, port int) { 93 | 94 | mux := http.NewServeMux() 95 | for k, v := range routes { 96 | mux.HandleFunc(k, v) 97 | } 98 | 99 | handleStatic(mux) 100 | 101 | addr := fmt.Sprintf("%v:%d", host, port) 102 | server := &http.Server{ 103 | Addr: addr, 104 | Handler: mux, 105 | ReadTimeout: 10 * time.Second, 106 | WriteTimeout: 10 * time.Second, 107 | MaxHeaderBytes: 1 << 20, 108 | } 109 | 110 | Logger.Infof("Server Started @ %v:%d", host, port) 111 | 112 | err := server.ListenAndServe() 113 | Logger.Error(err.Error()) 114 | } 115 | 116 | func handleStatic(mux *http.ServeMux) { 117 | mux.Handle(`/`, http.FileServer(http.Dir(*staticPath))) 118 | } 119 | -------------------------------------------------------------------------------- /cmd/server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/me-io/go-swap/b8d825d1873d6bc8f26df343b47f6d0a6b115a50/cmd/server/public/favicon.ico -------------------------------------------------------------------------------- /cmd/server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello Swap Server 7 | 8 | 9 | -------------------------------------------------------------------------------- /cmd/server/public/swagger/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI | go-swap-server 7 | 8 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /cmd/server/public/swagger/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | description: "This is a sample server go-swap-server. You can find out more about information at [https://github.com/me-io/go-swap](https://github.com/me-io/go-swap)" 4 | version: "1.0.0" 5 | title: "Go-Swap-Server" 6 | contact: 7 | email: "meabed@me.io" 8 | license: 9 | name: "MIT" 10 | url: "https://github.com/me-io/go-swap/blob/master/LICENSE.md" 11 | tags: 12 | - name: "Currency" 13 | description: "Swap & Convert between Currencies" 14 | externalDocs: 15 | description: "Find out more" 16 | url: "https://github.com/me-io/go-swap" 17 | schemes: 18 | - "https" 19 | - "http" 20 | paths: 21 | /convert: 22 | post: 23 | tags: 24 | - "Currency" 25 | summary: "Request Currency Conversion Rate & Value Multiple Exchanger - POST" 26 | description: "" 27 | operationId: "ConvertCurrencyPost" 28 | consumes: 29 | - "application/json" 30 | produces: 31 | - "application/json" 32 | parameters: 33 | - in: "body" 34 | name: "body" 35 | description: "Currency Conversion Request" 36 | required: true 37 | schema: 38 | $ref: "#/definitions/ConvertPostRequest" 39 | responses: 40 | 200: 41 | description: "successful operation" 42 | schema: 43 | $ref: "#/definitions/ConvertResponse" 44 | 405: 45 | description: "Invalid input" 46 | 400: 47 | description: "Invalid input" 48 | 404: 49 | description: "Invalid input" 50 | get: 51 | tags: 52 | - "Currency" 53 | summary: "Request Currency Conversion Rate & Value Single Exchanger - GET" 54 | description: "" 55 | operationId: "ConvertCurrencyGet" 56 | produces: 57 | - "application/json" 58 | parameters: 59 | - in: "query" 60 | name: "from" 61 | type: "string" 62 | required: true 63 | default: "USD" 64 | - in: "query" 65 | name: "to" 66 | type: "string" 67 | required: true 68 | default: "EGP" 69 | - in: "query" 70 | name: "amount" 71 | type: "number" 72 | required: true 73 | default: "10.45" 74 | - in: "query" 75 | name: "exchanger" 76 | type: "string" 77 | required: true 78 | default: "google" 79 | - in: "query" 80 | name: "apiKey" 81 | type: "string" 82 | required: false 83 | - in: "query" 84 | name: "cacheTime" 85 | type: "string" 86 | required: false 87 | default: "120s" 88 | responses: 89 | 200: 90 | description: "successful operation" 91 | schema: 92 | $ref: "#/definitions/ConvertResponse" 93 | 405: 94 | description: "Invalid input" 95 | 400: 96 | description: "Invalid input" 97 | 404: 98 | description: "Invalid input" 99 | 100 | definitions: 101 | ConvertPostRequest: 102 | example: 103 | amount: 2.5 104 | from: "USD" 105 | to: "AED" 106 | decimalPoints: 4 107 | cacheTime: "120s" 108 | exchanger: 109 | - name: "yahoo" 110 | - name: "google" 111 | - name: "themoneyconverter" 112 | userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 113 | type: "object" 114 | required: 115 | - "amount" 116 | - "from" 117 | - "to" 118 | - "exchanger" 119 | properties: 120 | amount: 121 | type: "number" 122 | format: "float" 123 | example: 1.54 124 | from: 125 | type: "string" 126 | example: "USD" 127 | to: 128 | type: "string" 129 | example: "AED" 130 | decimalPoints: 131 | type: "integer" 132 | example: 4 133 | exchanger: 134 | type: "array" 135 | minItems: 1 136 | maxItems: 10 137 | items: 138 | type: "object" 139 | required: 140 | - "name" 141 | properties: 142 | name: 143 | type: "string" 144 | example: "google" 145 | apiKey: 146 | type: "string" 147 | example: "demo_api_key_2018" 148 | apiVersion: 149 | type: "string" 150 | example: "1.0.3" 151 | userAgent: 152 | type: "string" 153 | example: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 154 | cacheTime: 155 | type: "string" 156 | example: "60s" 157 | ConvertResponse: 158 | type: "object" 159 | properties: 160 | from: 161 | type: "string" 162 | example: "USD" 163 | to: 164 | type: "string" 165 | example: "AED" 166 | originalAmount: 167 | type: "number" 168 | format: "float" 169 | example: 1.54 170 | exchangeValue: 171 | type: "number" 172 | format: "float" 173 | example: 17.91 174 | convertedAmount: 175 | type: "number" 176 | format: "float" 177 | example: 27.5814 178 | convertedText: 179 | type: "string" 180 | example: "1 USD is worth 3.675 AED" 181 | exchangerName: 182 | type: "string" 183 | example: "google" 184 | rateDateTime: 185 | type: "string" 186 | example: "2018-09-25T12:17:17Z" 187 | rateFromCache: 188 | type: "boolean" 189 | example: false 190 | externalDocs: 191 | description: "Find out more about Go-Swap-Server" 192 | url: "https://github.com/me-io/go-swap" 193 | -------------------------------------------------------------------------------- /cmd/server/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | ) 6 | 7 | // exchangerReqObj ... Exchanger object that form array of Exchangers in the Convert Request Data Object 8 | type exchangerReqObj struct { 9 | Name string `json:"name"` 10 | UserAgent string `json:"userAgent,omitempty"` 11 | ApiKey string `json:"apiKey,omitempty"` 12 | ApiVersion string `json:"apiVersion,omitempty"` 13 | } 14 | 15 | // convertReqObj ... Convert Request Data Object 16 | type convertReqObj struct { 17 | Amount float64 `json:"amount"` 18 | Exchanger []exchangerReqObj 19 | From string `json:"from"` 20 | To string `json:"to"` 21 | CacheTime string `json:"cacheTime"` 22 | DecimalPoints int `json:"decimalPoints"` 23 | } 24 | 25 | // convertResObj ... Convert Response Data Object 26 | type convertResObj struct { 27 | From string `json:"from"` 28 | To string `json:"to"` 29 | ExchangerName string `json:"exchangerName"` 30 | ExchangeValue float64 `json:"exchangeValue"` 31 | OriginalAmount float64 `json:"originalAmount"` 32 | ConvertedAmount float64 `json:"convertedAmount"` 33 | ConvertedText string `json:"convertedText"` 34 | RateDateTime string `json:"rateDateTime"` 35 | RateFromCache bool `json:"rateFromCache"` 36 | } 37 | 38 | // Secret ... Secret Type for logging in the Logger 39 | // Password is just an example type implementing the Redactor interface. Any 40 | // time this is logged, the Redacted() function will be called. 41 | type Secret string 42 | 43 | // Redacted ... Secret Redacted function 44 | func (p Secret) Redacted() interface{} { 45 | return logging.Redact(string(p)) 46 | } 47 | -------------------------------------------------------------------------------- /examples/main_app.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | ex "github.com/me-io/go-swap/pkg/exchanger" 8 | "github.com/me-io/go-swap/pkg/swap" 9 | ) 10 | 11 | func main() { 12 | SwapTest := swap.NewSwap() 13 | 14 | SwapTest. 15 | AddExchanger(ex.NewGoogleApi(nil)). 16 | AddExchanger(ex.NewYahooApi(nil)). 17 | Build() 18 | 19 | euroToUsdRate := SwapTest.Latest("EUR/USD") 20 | fmt.Println(euroToUsdRate.GetRateValue()) 21 | fmt.Println(euroToUsdRate.GetRateDateTime()) 22 | fmt.Println(euroToUsdRate.GetExchangerName()) 23 | } 24 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/me-io/go-swap/b8d825d1873d6bc8f26df343b47f6d0a6b115a50/icon.png -------------------------------------------------------------------------------- /pkg/cache/doc.go: -------------------------------------------------------------------------------- 1 | package cache // github.com/me-io/go-swap/pkg/cache 2 | -------------------------------------------------------------------------------- /pkg/cache/memory/doc.go: -------------------------------------------------------------------------------- 1 | package memory // github.com/me-io/go-swap/pkg/cache/memory 2 | -------------------------------------------------------------------------------- /pkg/cache/memory/memory.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Item is a cached reference 9 | type Item struct { 10 | Content []byte 11 | Expiration int64 12 | } 13 | 14 | // Expired returns true if the item has expired. 15 | func (item Item) Expired() bool { 16 | if item.Expiration == 0 { 17 | return false 18 | } 19 | return time.Now().UnixNano() > item.Expiration 20 | } 21 | 22 | //Storage mechanism for caching strings in memory 23 | type Storage struct { 24 | items map[string]Item 25 | mu *sync.RWMutex 26 | } 27 | 28 | //NewStorage creates a new in memory storage 29 | func NewStorage() *Storage { 30 | return &Storage{ 31 | items: make(map[string]Item), 32 | mu: &sync.RWMutex{}, 33 | } 34 | } 35 | 36 | //Get a cached content by key 37 | func (s Storage) Get(key string) []byte { 38 | s.mu.RLock() 39 | defer s.mu.RUnlock() 40 | 41 | item := s.items[key] 42 | 43 | if len(item.Content) < 1 { 44 | return []byte("") 45 | } 46 | 47 | if item.Expired() { 48 | delete(s.items, key) 49 | return []byte("") 50 | } 51 | 52 | return item.Content 53 | } 54 | 55 | //Set a cached content by key 56 | func (s Storage) Set(key string, content []byte, duration time.Duration) { 57 | s.mu.Lock() 58 | defer s.mu.Unlock() 59 | 60 | s.items[key] = Item{ 61 | Content: content, 62 | Expiration: time.Now().Add(duration).UnixNano(), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/cache/memory/memory_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // todo write more tests 10 | func parse(s string) time.Duration { 11 | d, _ := time.ParseDuration(s) 12 | return d 13 | } 14 | 15 | func TestStorage_Memory_GetEmpty(t *testing.T) { 16 | storage := NewStorage() 17 | content := storage.Get("MY_KEY") 18 | 19 | assert.EqualValues(t, []byte(""), content) 20 | } 21 | 22 | func TestStorage_Memory_GetRateValue(t *testing.T) { 23 | storage := NewStorage() 24 | storage.Set("MY_KEY", []byte("123456"), parse("5s")) 25 | content := storage.Get("MY_KEY") 26 | 27 | assert.EqualValues(t, []byte("123456"), content) 28 | } 29 | 30 | func TestStorage_Memory_GetExpiredValue(t *testing.T) { 31 | storage := NewStorage() 32 | storage.Set("MY_KEY", []byte("123456"), parse("1s")) 33 | time.Sleep(parse("1s200ms")) 34 | content := storage.Get("MY_KEY") 35 | 36 | assert.EqualValues(t, []byte(""), content) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/cache/redis/doc.go: -------------------------------------------------------------------------------- 1 | package redis // github.com/me-io/go-swap/pkg/cache/redis 2 | -------------------------------------------------------------------------------- /pkg/cache/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | r "github.com/go-redis/redis" 5 | "time" 6 | ) 7 | 8 | var prefix = "_SWAP_CACHE_" 9 | 10 | //Storage mechanism for caching strings in memory 11 | type Storage struct { 12 | client *r.Client 13 | } 14 | 15 | //NewStorage creates a new redis storage 16 | func NewStorage(url string) (*Storage, error) { 17 | var ( 18 | opts *r.Options 19 | err error 20 | ) 21 | 22 | if opts, err = r.ParseURL(url); err != nil { 23 | return nil, err 24 | } 25 | 26 | return &Storage{ 27 | client: r.NewClient(opts), 28 | }, nil 29 | } 30 | 31 | //Get a cached content by key 32 | func (s Storage) Get(key string) []byte { 33 | val, _ := s.client.Get(prefix + key).Bytes() 34 | return val 35 | } 36 | 37 | //Set a cached content by key 38 | func (s Storage) Set(key string, content []byte, duration time.Duration) { 39 | s.client.Set(prefix+key, content, duration) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/cache/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // todo write more tests 11 | var redisURL = os.Getenv(`REDIS_URL`) 12 | 13 | func parse(s string) time.Duration { 14 | d, _ := time.ParseDuration(s) 15 | return d 16 | } 17 | 18 | func TestStorage_Redis_WrongURL(t *testing.T) { 19 | storage, err := NewStorage("wrong://wtf") 20 | if err == nil || storage != nil { 21 | t.Fail() 22 | } 23 | } 24 | 25 | func TestStorage_Redis_GetEmpty(t *testing.T) { 26 | storage, _ := NewStorage(redisURL) 27 | content := storage.Get("MY_KEY") 28 | 29 | assert.EqualValues(t, []byte(""), content) 30 | } 31 | 32 | func TestStorage_Redis_GetRateValue(t *testing.T) { 33 | storage, _ := NewStorage(redisURL) 34 | storage.Set("MY_KEY", []byte("123456"), parse("5s")) 35 | content := storage.Get("MY_KEY") 36 | 37 | assert.EqualValues(t, []byte("123456"), content) 38 | } 39 | 40 | func TestStorage_Redis_GetExpiredValue(t *testing.T) { 41 | storage, _ := NewStorage(redisURL) 42 | storage.Set("MY_KEY", []byte("123456"), parse("1s")) 43 | time.Sleep(parse("2s")) 44 | content := storage.Get("MY_KEY") 45 | 46 | assert.EqualValues(t, []byte(""), content) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/cache/types.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "time" 4 | 5 | // Storage ... Cache Storage Interface 6 | type Storage interface { 7 | Get(key string) []byte 8 | Set(key string, content []byte, duration time.Duration) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/exchanger/1forge.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type oneForgeApi struct { 14 | attributes 15 | } 16 | 17 | var ( 18 | oneForgeApiUrl = `https://forex.1forge.com/%s/convert?from=%s&to=%s&quantity=1&api_key=%s` 19 | oneForgeApiHeaders = map[string][]string{ 20 | `Accept`: {`text/html,application/xhtml+xml,application/xml,application/json`}, 21 | `Accept-Encoding`: {`text`}, 22 | } 23 | ) 24 | 25 | func (c *oneForgeApi) requestRate(from string, to string, opt ...interface{}) (*oneForgeApi, error) { 26 | 27 | // todo add option opt to add more headers or client configurations 28 | // free mem-leak 29 | // optimize for memory leak 30 | // todo optimize curl connection 31 | 32 | url := fmt.Sprintf(oneForgeApiUrl, c.apiVersion, from, to, c.apiKey) 33 | req, _ := http.NewRequest("GET", url, nil) 34 | 35 | oneForgeApiHeaders[`User-Agent`] = []string{c.userAgent} 36 | req.Header = oneForgeApiHeaders 37 | 38 | res, err := c.Client.Do(req) 39 | 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer res.Body.Close() 44 | 45 | body, err := ioutil.ReadAll(res.Body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // free mem-leak 51 | // todo discard data 52 | c.responseBody = string(body) 53 | return c, nil 54 | } 55 | 56 | // GetRateValue ... get exchange rate value 57 | func (c *oneForgeApi) GetRateValue() float64 { 58 | return c.rateValue 59 | } 60 | 61 | // GetExchangerName ... return exchanger name 62 | func (c *oneForgeApi) GetRateDateTime() string { 63 | return c.rateDate.Format(time.RFC3339) 64 | } 65 | 66 | // GetExchangerName ... return exchanger name 67 | func (c *oneForgeApi) GetExchangerName() string { 68 | return c.name 69 | } 70 | 71 | // Latest ... populate latest exchange rate 72 | func (c *oneForgeApi) Latest(from string, to string, opt ...interface{}) error { 73 | 74 | _, err := c.requestRate(from, to, opt) 75 | if err != nil { 76 | log.Print(err) 77 | return err 78 | } 79 | 80 | // if from currency is same as converted currency return value of 1 81 | if from == to { 82 | c.rateValue = 1 83 | return nil 84 | } 85 | 86 | json, err := simplejson.NewJson([]byte(c.responseBody)) 87 | 88 | if err != nil { 89 | log.Print(err) 90 | return err 91 | } 92 | 93 | // opening price 94 | value := json.GetPath(`value`). 95 | MustFloat64() 96 | // todo handle error 97 | if value <= 0 { 98 | return fmt.Errorf(`error in retrieving exhcange rate is 0`) 99 | } 100 | c.rateValue = value 101 | c.rateDate = time.Now() 102 | return nil 103 | } 104 | 105 | // NewOneForgeApi ... return new instance of oneForgeApi 106 | func NewOneForgeApi(opt map[string]string) *oneForgeApi { 107 | keepAliveTimeout := 600 * time.Second 108 | timeout := 5 * time.Second 109 | defaultTransport := &http.Transport{ 110 | DialContext: (&net.Dialer{ 111 | Timeout: 30 * time.Second, 112 | KeepAlive: keepAliveTimeout, 113 | DualStack: true, 114 | }).DialContext, 115 | MaxIdleConns: 100, 116 | MaxIdleConnsPerHost: 100, 117 | } 118 | 119 | client := &http.Client{ 120 | Transport: defaultTransport, 121 | Timeout: timeout, 122 | } 123 | 124 | attr := attributes{ 125 | name: `1forge`, 126 | Client: client, 127 | apiVersion: `1.0.3`, 128 | apiKey: opt[`apiKey`], 129 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 130 | } 131 | 132 | if opt[`userAgent`] != "" { 133 | attr.userAgent = opt[`userAgent`] 134 | } 135 | 136 | if opt[`apiVersion`] != "" { 137 | attr.apiVersion = opt[`apiVersion`] 138 | } 139 | 140 | r := &oneForgeApi{attr} 141 | return r 142 | } 143 | -------------------------------------------------------------------------------- /pkg/exchanger/1forge_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestOneForgeApi_Latest(t *testing.T) { 10 | rate := NewOneForgeApi(nil) 11 | assert.Equal(t, rate.name, `1forge`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`EUR`, `EUR`) 16 | assert.Equal(t, float64(1), rate.GetRateValue()) 17 | 18 | rate.Latest(`USD`, `AED`) 19 | assert.Equal(t, float64(3.675), rate.GetRateValue()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/exchanger/currency_list.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | // CurrencyList ... list of currencies in ISO Format 4 | // source @ https://raw.githubusercontent.com/umpirsky/currency-list/master/data/en_US/currency.json 5 | var CurrencyList = map[string]string{ 6 | "AFN": "Afghan Afghani", 7 | "AFA": "Afghan Afghani (1927–2002)", 8 | "ALL": "Albanian Lek", 9 | "ALK": "Albanian Lek (1946–1965)", 10 | "DZD": "Algerian Dinar", 11 | "ADP": "Andorran Peseta", 12 | "AOA": "Angolan Kwanza", 13 | "AOK": "Angolan Kwanza (1977–1991)", 14 | "AON": "Angolan New Kwanza (1990–2000)", 15 | "AOR": "Angolan Readjusted Kwanza (1995–1999)", 16 | "ARA": "Argentine Austral", 17 | "ARS": "Argentine Peso", 18 | "ARM": "Argentine Peso (1881–1970)", 19 | "ARP": "Argentine Peso (1983–1985)", 20 | "ARL": "Argentine Peso Ley (1970–1983)", 21 | "AMD": "Armenian Dram", 22 | "AWG": "Aruban Florin", 23 | "AUD": "Australian Dollar", 24 | "ATS": "Austrian Schilling", 25 | "AZN": "Azerbaijani Manat", 26 | "AZM": "Azerbaijani Manat (1993–2006)", 27 | "BSD": "Bahamian Dollar", 28 | "BHD": "Bahraini Dinar", 29 | "BDT": "Bangladeshi Taka", 30 | "BBD": "Barbadian Dollar", 31 | "BYN": "Belarusian Ruble", 32 | "BYB": "Belarusian Ruble (1994–1999)", 33 | "BYR": "Belarusian Ruble (2000–2016)", 34 | "BEF": "Belgian Franc", 35 | "BEC": "Belgian Franc (convertible)", 36 | "BEL": "Belgian Franc (financial)", 37 | "BZD": "Belize Dollar", 38 | "BMD": "Bermudan Dollar", 39 | "BTN": "Bhutanese Ngultrum", 40 | "BOB": "Bolivian Boliviano", 41 | "BOL": "Bolivian Boliviano (1863–1963)", 42 | "BOV": "Bolivian Mvdol", 43 | "BOP": "Bolivian Peso", 44 | "BAM": "Bosnia-Herzegovina Convertible Mark", 45 | "BAD": "Bosnia-Herzegovina Dinar (1992–1994)", 46 | "BAN": "Bosnia-Herzegovina New Dinar (1994–1997)", 47 | "BWP": "Botswanan Pula", 48 | "BRC": "Brazilian Cruzado (1986–1989)", 49 | "BRZ": "Brazilian Cruzeiro (1942–1967)", 50 | "BRE": "Brazilian Cruzeiro (1990–1993)", 51 | "BRR": "Brazilian Cruzeiro (1993–1994)", 52 | "BRN": "Brazilian New Cruzado (1989–1990)", 53 | "BRB": "Brazilian New Cruzeiro (1967–1986)", 54 | "BRL": "Brazilian Real", 55 | "GBP": "British Pound", 56 | "BND": "Brunei Dollar", 57 | "BGL": "Bulgarian Hard Lev", 58 | "BGN": "Bulgarian Lev", 59 | "BGO": "Bulgarian Lev (1879–1952)", 60 | "BGM": "Bulgarian Socialist Lev", 61 | "BUK": "Burmese Kyat", 62 | "BIF": "Burundian Franc", 63 | "XPF": "CFP Franc", 64 | "KHR": "Cambodian Riel", 65 | "CAD": "Canadian Dollar", 66 | "CVE": "Cape Verdean Escudo", 67 | "KYD": "Cayman Islands Dollar", 68 | "XAF": "Central African CFA Franc", 69 | "CLE": "Chilean Escudo", 70 | "CLP": "Chilean Peso", 71 | "CLF": "Chilean Unit of Account (UF)", 72 | "CNX": "Chinese People’s Bank Dollar", 73 | "CNY": "Chinese Yuan", 74 | "COP": "Colombian Peso", 75 | "COU": "Colombian Real Value Unit", 76 | "KMF": "Comorian Franc", 77 | "CDF": "Congolese Franc", 78 | "CRC": "Costa Rican Colón", 79 | "HRD": "Croatian Dinar", 80 | "HRK": "Croatian Kuna", 81 | "CUC": "Cuban Convertible Peso", 82 | "CUP": "Cuban Peso", 83 | "CYP": "Cypriot Pound", 84 | "CZK": "Czech Koruna", 85 | "CSK": "Czechoslovak Hard Koruna", 86 | "DKK": "Danish Krone", 87 | "DJF": "Djiboutian Franc", 88 | "DOP": "Dominican Peso", 89 | "NLG": "Dutch Guilder", 90 | "XCD": "East Caribbean Dollar", 91 | "DDM": "East German Mark", 92 | "ECS": "Ecuadorian Sucre", 93 | "ECV": "Ecuadorian Unit of Constant Value", 94 | "EGP": "Egyptian Pound", 95 | "GQE": "Equatorial Guinean Ekwele", 96 | "ERN": "Eritrean Nakfa", 97 | "EEK": "Estonian Kroon", 98 | "ETB": "Ethiopian Birr", 99 | "EUR": "Euro", 100 | "XEU": "European Currency Unit", 101 | "FKP": "Falkland Islands Pound", 102 | "FJD": "Fijian Dollar", 103 | "FIM": "Finnish Markka", 104 | "FRF": "French Franc", 105 | "XFO": "French Gold Franc", 106 | "XFU": "French UIC-Franc", 107 | "GMD": "Gambian Dalasi", 108 | "GEK": "Georgian Kupon Larit", 109 | "GEL": "Georgian Lari", 110 | "DEM": "German Mark", 111 | "GHS": "Ghanaian Cedi", 112 | "GHC": "Ghanaian Cedi (1979–2007)", 113 | "GIP": "Gibraltar Pound", 114 | "GRD": "Greek Drachma", 115 | "GTQ": "Guatemalan Quetzal", 116 | "GWP": "Guinea-Bissau Peso", 117 | "GNF": "Guinean Franc", 118 | "GNS": "Guinean Syli", 119 | "GYD": "Guyanaese Dollar", 120 | "HTG": "Haitian Gourde", 121 | "HNL": "Honduran Lempira", 122 | "HKD": "Hong Kong Dollar", 123 | "HUF": "Hungarian Forint", 124 | "ISK": "Icelandic Króna", 125 | "ISJ": "Icelandic Króna (1918–1981)", 126 | "INR": "Indian Rupee", 127 | "IDR": "Indonesian Rupiah", 128 | "IRR": "Iranian Rial", 129 | "IQD": "Iraqi Dinar", 130 | "IEP": "Irish Pound", 131 | "ILS": "Israeli New Shekel", 132 | "ILP": "Israeli Pound", 133 | "ILR": "Israeli Shekel (1980–1985)", 134 | "ITL": "Italian Lira", 135 | "JMD": "Jamaican Dollar", 136 | "JPY": "Japanese Yen", 137 | "JOD": "Jordanian Dinar", 138 | "KZT": "Kazakhstani Tenge", 139 | "KES": "Kenyan Shilling", 140 | "KWD": "Kuwaiti Dinar", 141 | "KGS": "Kyrgystani Som", 142 | "LAK": "Laotian Kip", 143 | "LVL": "Latvian Lats", 144 | "LVR": "Latvian Ruble", 145 | "LBP": "Lebanese Pound", 146 | "LSL": "Lesotho Loti", 147 | "LRD": "Liberian Dollar", 148 | "LYD": "Libyan Dinar", 149 | "LTL": "Lithuanian Litas", 150 | "LTT": "Lithuanian Talonas", 151 | "LUL": "Luxembourg Financial Franc", 152 | "LUC": "Luxembourgian Convertible Franc", 153 | "LUF": "Luxembourgian Franc", 154 | "MOP": "Macanese Pataca", 155 | "MKD": "Macedonian Denar", 156 | "MKN": "Macedonian Denar (1992–1993)", 157 | "MGA": "Malagasy Ariary", 158 | "MGF": "Malagasy Franc", 159 | "MWK": "Malawian Kwacha", 160 | "MYR": "Malaysian Ringgit", 161 | "MVR": "Maldivian Rufiyaa", 162 | "MVP": "Maldivian Rupee (1947–1981)", 163 | "MLF": "Malian Franc", 164 | "MTL": "Maltese Lira", 165 | "MTP": "Maltese Pound", 166 | "MRO": "Mauritanian Ouguiya", 167 | "MUR": "Mauritian Rupee", 168 | "MXV": "Mexican Investment Unit", 169 | "MXN": "Mexican Peso", 170 | "MXP": "Mexican Silver Peso (1861–1992)", 171 | "MDC": "Moldovan Cupon", 172 | "MDL": "Moldovan Leu", 173 | "MCF": "Monegasque Franc", 174 | "MNT": "Mongolian Tugrik", 175 | "MAD": "Moroccan Dirham", 176 | "MAF": "Moroccan Franc", 177 | "MZE": "Mozambican Escudo", 178 | "MZN": "Mozambican Metical", 179 | "MZM": "Mozambican Metical (1980–2006)", 180 | "MMK": "Myanmar Kyat", 181 | "NAD": "Namibian Dollar", 182 | "NPR": "Nepalese Rupee", 183 | "ANG": "Netherlands Antillean Guilder", 184 | "TWD": "New Taiwan Dollar", 185 | "NZD": "New Zealand Dollar", 186 | "NIO": "Nicaraguan Córdoba", 187 | "NIC": "Nicaraguan Córdoba (1988–1991)", 188 | "NGN": "Nigerian Naira", 189 | "KPW": "North Korean Won", 190 | "NOK": "Norwegian Krone", 191 | "OMR": "Omani Rial", 192 | "PKR": "Pakistani Rupee", 193 | "PAB": "Panamanian Balboa", 194 | "PGK": "Papua New Guinean Kina", 195 | "PYG": "Paraguayan Guarani", 196 | "PEI": "Peruvian Inti", 197 | "PEN": "Peruvian Sol", 198 | "PES": "Peruvian Sol (1863–1965)", 199 | "PHP": "Philippine Peso", 200 | "PLN": "Polish Zloty", 201 | "PLZ": "Polish Zloty (1950–1995)", 202 | "PTE": "Portuguese Escudo", 203 | "GWE": "Portuguese Guinea Escudo", 204 | "QAR": "Qatari Rial", 205 | "XRE": "RINET Funds", 206 | "RHD": "Rhodesian Dollar", 207 | "RON": "Romanian Leu", 208 | "ROL": "Romanian Leu (1952–2006)", 209 | "RUB": "Russian Ruble", 210 | "RUR": "Russian Ruble (1991–1998)", 211 | "RWF": "Rwandan Franc", 212 | "SVC": "Salvadoran Colón", 213 | "WST": "Samoan Tala", 214 | "SAR": "Saudi Riyal", 215 | "RSD": "Serbian Dinar", 216 | "CSD": "Serbian Dinar (2002–2006)", 217 | "SCR": "Seychellois Rupee", 218 | "SLL": "Sierra Leonean Leone", 219 | "SGD": "Singapore Dollar", 220 | "SKK": "Slovak Koruna", 221 | "SIT": "Slovenian Tolar", 222 | "SBD": "Solomon Islands Dollar", 223 | "SOS": "Somali Shilling", 224 | "ZAR": "South African Rand", 225 | "ZAL": "South African Rand (financial)", 226 | "KRH": "South Korean Hwan (1953–1962)", 227 | "KRW": "South Korean Won", 228 | "KRO": "South Korean Won (1945–1953)", 229 | "SSP": "South Sudanese Pound", 230 | "SUR": "Soviet Rouble", 231 | "ESP": "Spanish Peseta", 232 | "ESA": "Spanish Peseta (A account)", 233 | "ESB": "Spanish Peseta (convertible account)", 234 | "LKR": "Sri Lankan Rupee", 235 | "SHP": "St. Helena Pound", 236 | "SDD": "Sudanese Dinar (1992–2007)", 237 | "SDG": "Sudanese Pound", 238 | "SDP": "Sudanese Pound (1957–1998)", 239 | "SRD": "Surinamese Dollar", 240 | "SRG": "Surinamese Guilder", 241 | "SZL": "Swazi Lilangeni", 242 | "SEK": "Swedish Krona", 243 | "CHF": "Swiss Franc", 244 | "SYP": "Syrian Pound", 245 | "STD": "São Tomé & Príncipe Dobra", 246 | "TJR": "Tajikistani Ruble", 247 | "TJS": "Tajikistani Somoni", 248 | "TZS": "Tanzanian Shilling", 249 | "THB": "Thai Baht", 250 | "TPE": "Timorese Escudo", 251 | "TOP": "Tongan Paʻanga", 252 | "TTD": "Trinidad & Tobago Dollar", 253 | "TND": "Tunisian Dinar", 254 | "TRY": "Turkish Lira", 255 | "TRL": "Turkish Lira (1922–2005)", 256 | "TMT": "Turkmenistani Manat", 257 | "TMM": "Turkmenistani Manat (1993–2009)", 258 | "USD": "US Dollar", 259 | "USN": "US Dollar (Next day)", 260 | "USS": "US Dollar (Same day)", 261 | "UGX": "Ugandan Shilling", 262 | "UGS": "Ugandan Shilling (1966–1987)", 263 | "UAH": "Ukrainian Hryvnia", 264 | "UAK": "Ukrainian Karbovanets", 265 | "AED": "United Arab Emirates Dirham", 266 | "UYU": "Uruguayan Peso", 267 | "UYP": "Uruguayan Peso (1975–1993)", 268 | "UYI": "Uruguayan Peso (Indexed Units)", 269 | "UZS": "Uzbekistani Som", 270 | "VUV": "Vanuatu Vatu", 271 | "VEF": "Venezuelan Bolívar", 272 | "VEB": "Venezuelan Bolívar (1871–2008)", 273 | "VND": "Vietnamese Dong", 274 | "VNN": "Vietnamese Dong (1978–1985)", 275 | "CHE": "WIR Euro", 276 | "CHW": "WIR Franc", 277 | "XOF": "West African CFA Franc", 278 | "YDD": "Yemeni Dinar", 279 | "YER": "Yemeni Rial", 280 | "YUN": "Yugoslavian Convertible Dinar (1990–1992)", 281 | "YUD": "Yugoslavian Hard Dinar (1966–1990)", 282 | "YUM": "Yugoslavian New Dinar (1994–2002)", 283 | "YUR": "Yugoslavian Reformed Dinar (1992–1993)", 284 | "ZRN": "Zairean New Zaire (1993–1998)", 285 | "ZRZ": "Zairean Zaire (1971–1993)", 286 | "ZMW": "Zambian Kwacha", 287 | "ZMK": "Zambian Kwacha (1968–2012)", 288 | "ZWD": "Zimbabwean Dollar (1980–2008)", 289 | "ZWR": "Zimbabwean Dollar (2008)", 290 | "ZWL": "Zimbabwean Dollar (2009)", 291 | } 292 | 293 | // CurrencyListArr ... CurrencyList to array of int => interfaces to be used in validation 294 | var CurrencyListArr = MapKeyArrInterface(CurrencyList) 295 | -------------------------------------------------------------------------------- /pkg/exchanger/currencylayer.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type currencyLayerApi struct { 14 | attributes 15 | } 16 | 17 | var ( 18 | currencyLayerApiUrl = `https://apilayer.net/api/convert?access_key=%s&from=%s&to=%s&amount=1&format=1` 19 | currencyLayerApiHeaders = map[string][]string{ 20 | `Accept`: {`text/html,application/xhtml+xml,application/xml,application/json`}, 21 | `Accept-Encoding`: {`text`}, 22 | } 23 | ) 24 | 25 | func (c *currencyLayerApi) requestRate(from string, to string, opt ...interface{}) (*currencyLayerApi, error) { 26 | 27 | // todo add option opt to add more headers or client configurations 28 | // free mem-leak 29 | // optimize for memory leak 30 | // todo optimize curl connection 31 | 32 | url := fmt.Sprintf(currencyLayerApiUrl, c.apiKey, from, to) 33 | req, _ := http.NewRequest("GET", url, nil) 34 | 35 | currencyLayerApiHeaders[`User-Agent`] = []string{c.userAgent} 36 | req.Header = currencyLayerApiHeaders 37 | 38 | res, err := c.Client.Do(req) 39 | 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer res.Body.Close() 44 | 45 | body, err := ioutil.ReadAll(res.Body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // free mem-leak 51 | // todo discard data 52 | c.responseBody = string(body) 53 | return c, nil 54 | } 55 | 56 | // GetRateValue ... get exchange rate value 57 | func (c *currencyLayerApi) GetRateValue() float64 { 58 | return c.rateValue 59 | } 60 | 61 | // GetExchangerName ... return exchanger name 62 | func (c *currencyLayerApi) GetRateDateTime() string { 63 | return c.rateDate.Format(time.RFC3339) 64 | } 65 | 66 | // GetExchangerName ... return exchanger name 67 | func (c *currencyLayerApi) GetExchangerName() string { 68 | return c.name 69 | } 70 | 71 | // Latest ... populate latest exchange rate 72 | func (c *currencyLayerApi) Latest(from string, to string, opt ...interface{}) error { 73 | 74 | _, err := c.requestRate(from, to, opt) 75 | if err != nil { 76 | log.Print(err) 77 | return err 78 | } 79 | 80 | // if from currency is same as converted currency return value of 1 81 | if from == to { 82 | c.rateValue = 1 83 | return nil 84 | } 85 | 86 | json, err := simplejson.NewJson([]byte(c.responseBody)) 87 | 88 | if err != nil { 89 | log.Print(err) 90 | return err 91 | } 92 | 93 | // opening price 94 | value := json.GetPath(`result`). 95 | MustFloat64() 96 | // todo handle error 97 | if value <= 0 { 98 | return fmt.Errorf(`error in retrieving exhcange rate is 0`) 99 | } 100 | c.rateValue = value 101 | c.rateDate = time.Now() 102 | return nil 103 | } 104 | 105 | // NewCurrencyLayerApi ... return new instance of currencyLayerApi 106 | func NewCurrencyLayerApi(opt map[string]string) *currencyLayerApi { 107 | keepAliveTimeout := 600 * time.Second 108 | timeout := 5 * time.Second 109 | defaultTransport := &http.Transport{ 110 | DialContext: (&net.Dialer{ 111 | Timeout: 30 * time.Second, 112 | KeepAlive: keepAliveTimeout, 113 | DualStack: true, 114 | }).DialContext, 115 | MaxIdleConns: 100, 116 | MaxIdleConnsPerHost: 100, 117 | } 118 | 119 | client := &http.Client{ 120 | Transport: defaultTransport, 121 | Timeout: timeout, 122 | } 123 | 124 | attr := attributes{ 125 | name: `currencylayer`, 126 | Client: client, 127 | apiKey: opt[`apiKey`], 128 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 129 | } 130 | if opt[`userAgent`] != "" { 131 | attr.userAgent = opt[`userAgent`] 132 | } 133 | 134 | r := ¤cyLayerApi{attr} 135 | return r 136 | } 137 | -------------------------------------------------------------------------------- /pkg/exchanger/currencylayer_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestCurrencyLayerApi_Latest(t *testing.T) { 10 | rate := NewCurrencyLayerApi(nil) 11 | assert.Equal(t, rate.name, `currencylayer`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`EUR`, `EUR`) 16 | assert.Equal(t, float64(1), rate.GetRateValue()) 17 | 18 | rate.Latest(`EUR`, `USD`) 19 | assert.Equal(t, float64(6.8064), rate.GetRateValue()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/exchanger/doc.go: -------------------------------------------------------------------------------- 1 | package exchanger // import "github.com/me-io/go-swap/pkg/exchanger" 2 | -------------------------------------------------------------------------------- /pkg/exchanger/fixer.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type fixerApi struct { 14 | attributes 15 | } 16 | 17 | var ( 18 | fixerApiUrl = `https://data.fixer.io/api/convert?access_key=%s&from=%s&to=%s&amount=1&format=1` 19 | fixerApiHeaders = map[string][]string{ 20 | `Accept`: {`text/html,application/xhtml+xml,application/xml,application/json`}, 21 | `Accept-Encoding`: {`text`}, 22 | } 23 | ) 24 | 25 | func (c *fixerApi) requestRate(from string, to string, opt ...interface{}) (*fixerApi, error) { 26 | 27 | // todo add option opt to add more headers or client configurations 28 | // free mem-leak 29 | // optimize for memory leak 30 | // todo optimize curl connection 31 | 32 | url := fmt.Sprintf(fixerApiUrl, c.apiKey, from, to) 33 | req, _ := http.NewRequest("GET", url, nil) 34 | 35 | fixerApiHeaders[`User-Agent`] = []string{c.userAgent} 36 | req.Header = fixerApiHeaders 37 | 38 | res, err := c.Client.Do(req) 39 | 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer res.Body.Close() 44 | 45 | body, err := ioutil.ReadAll(res.Body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // free mem-leak 51 | // todo discard data 52 | c.responseBody = string(body) 53 | return c, nil 54 | } 55 | 56 | // GetRateValue ... get exchange rate value 57 | func (c *fixerApi) GetRateValue() float64 { 58 | return c.rateValue 59 | } 60 | 61 | // GetRateDateTime ... return rate datetime 62 | func (c *fixerApi) GetRateDateTime() string { 63 | return c.rateDate.Format(time.RFC3339) 64 | } 65 | 66 | // GetExchangerName ... return exchanger name 67 | func (c *fixerApi) GetExchangerName() string { 68 | return c.name 69 | } 70 | 71 | // Latest ... populate latest exchange rate 72 | func (c *fixerApi) Latest(from string, to string, opt ...interface{}) error { 73 | 74 | _, err := c.requestRate(from, to, opt) 75 | if err != nil { 76 | log.Print(err) 77 | return err 78 | } 79 | 80 | // if from currency is same as converted currency return value of 1 81 | if from == to { 82 | c.rateValue = 1 83 | return nil 84 | } 85 | 86 | json, err := simplejson.NewJson([]byte(c.responseBody)) 87 | 88 | if err != nil { 89 | log.Print(err) 90 | return err 91 | } 92 | 93 | // opening price 94 | value := json.GetPath(`result`). 95 | MustFloat64() 96 | // todo handle error 97 | if value <= 0 { 98 | return fmt.Errorf(`error in retrieving exhcange rate is 0`) 99 | } 100 | c.rateValue = value 101 | c.rateDate = time.Now() 102 | return nil 103 | } 104 | 105 | // NewFixerApi ... return new instance of fixerApi 106 | func NewFixerApi(opt map[string]string) *fixerApi { 107 | keepAliveTimeout := 600 * time.Second 108 | timeout := 5 * time.Second 109 | defaultTransport := &http.Transport{ 110 | DialContext: (&net.Dialer{ 111 | Timeout: 30 * time.Second, 112 | KeepAlive: keepAliveTimeout, 113 | DualStack: true, 114 | }).DialContext, 115 | MaxIdleConns: 100, 116 | MaxIdleConnsPerHost: 100, 117 | } 118 | 119 | client := &http.Client{ 120 | Transport: defaultTransport, 121 | Timeout: timeout, 122 | } 123 | 124 | attr := attributes{ 125 | name: `fixer`, 126 | Client: client, 127 | apiKey: opt[`apiKey`], 128 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 129 | } 130 | if opt[`userAgent`] != "" { 131 | attr.userAgent = opt[`userAgent`] 132 | } 133 | 134 | r := &fixerApi{attr} 135 | return r 136 | } 137 | -------------------------------------------------------------------------------- /pkg/exchanger/fixer_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestFixerApi_Latest(t *testing.T) { 10 | rate := NewFixerApi(nil) 11 | assert.Equal(t, rate.name, `fixer`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`EUR`, `EUR`) 16 | assert.Equal(t, float64(1), rate.GetRateValue()) 17 | 18 | rate.Latest(`EUR`, `USD`) 19 | assert.Equal(t, float64(3724.305775), rate.GetRateValue()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/exchanger/google.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "net/http" 9 | "regexp" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type googleApi struct { 15 | attributes 16 | } 17 | 18 | // ref @link https://github.com/florianv/exchanger/blob/master/src/Service/Google.php 19 | // example : https://www.google.com/search?q=1+USD+to+USD&ncr=1 20 | // example : https://www.google.com/search?q=1+USD+to+EGP&ncr=1 21 | // example : https://www.google.com/search?q=1+USD+to+AED&ncr=1 22 | var ( 23 | googleApiUrl = `https://www.google.com/search?q=1+%s+to+%s&ncr=1` 24 | googleApiHeaders = map[string][]string{ 25 | `Accept`: {`text/html`}, 26 | } 27 | ) 28 | 29 | func (c *googleApi) requestRate(from string, to string, opt ...interface{}) (*googleApi, error) { 30 | 31 | // todo add option opt to add more headers or client configurations 32 | // free mem-leak 33 | // optimize for memory leak 34 | // todo optimize curl connection 35 | 36 | // format the url and replace currency 37 | url := fmt.Sprintf(googleApiUrl, from, to) 38 | // prepare the request 39 | req, _ := http.NewRequest("GET", url, nil) 40 | // assign the request headers 41 | googleApiHeaders[`User-Agent`] = []string{c.userAgent} 42 | req.Header = googleApiHeaders 43 | 44 | // execute the request 45 | res, err := c.Client.Do(req) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | defer res.Body.Close() 51 | 52 | body, err := ioutil.ReadAll(res.Body) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // free mem-leak 58 | // todo discard data 59 | c.responseBody = string(body) 60 | return c, nil 61 | } 62 | 63 | // GetRateValue ... get exchange rate value 64 | func (c *googleApi) GetRateValue() float64 { 65 | return c.rateValue 66 | } 67 | 68 | // GetRateDateTime ... return rate datetime 69 | func (c *googleApi) GetRateDateTime() string { 70 | return c.rateDate.Format(time.RFC3339) 71 | } 72 | 73 | // GetExchangerName ... return exchanger name 74 | func (c *googleApi) GetExchangerName() string { 75 | return c.name 76 | } 77 | 78 | // Latest ... populate latest exchange rate 79 | func (c *googleApi) Latest(from string, to string, opt ...interface{}) error { 80 | 81 | // todo cache layer 82 | _, err := c.requestRate(from, to, opt) 83 | if err != nil { 84 | log.Print(err) 85 | return err 86 | } 87 | 88 | // if from currency is same as converted currency return value of 1 89 | if from == to { 90 | c.rateValue = 1 91 | return nil 92 | } 93 | 94 | validID := regexp.MustCompile(`(?s)knowledge-currency__tgt-input(.*)value="([0-9.]{0,12})"(.*)"`) 95 | stringMatches := validID.FindStringSubmatch(c.responseBody) 96 | 97 | c.rateValue, err = strconv.ParseFloat(stringMatches[2], 64) 98 | c.rateDate = time.Now() 99 | 100 | if err != nil { 101 | log.Print(err) 102 | return err 103 | } 104 | return nil 105 | } 106 | 107 | // NewGoogleApi ... return new instance of googleApi 108 | func NewGoogleApi(opt map[string]string) *googleApi { 109 | 110 | keepAliveTimeout := 600 * time.Second 111 | timeout := 5 * time.Second 112 | defaultTransport := &http.Transport{ 113 | DialContext: (&net.Dialer{ 114 | Timeout: 30 * time.Second, 115 | KeepAlive: keepAliveTimeout, 116 | DualStack: true, 117 | }).DialContext, 118 | MaxIdleConns: 100, 119 | MaxIdleConnsPerHost: 100, 120 | } 121 | 122 | client := &http.Client{ 123 | Transport: defaultTransport, 124 | Timeout: timeout, 125 | } 126 | 127 | attr := attributes{ 128 | name: `google`, 129 | Client: client, 130 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 131 | } 132 | if opt[`userAgent`] != "" { 133 | attr.userAgent = opt[`userAgent`] 134 | } 135 | 136 | r := &googleApi{attr} 137 | 138 | return r 139 | } 140 | -------------------------------------------------------------------------------- /pkg/exchanger/google_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestGoogleApi_Latest(t *testing.T) { 10 | rate := NewGoogleApi(nil) 11 | assert.Equal(t, rate.name, `google`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`EUR`, `EUR`) 16 | assert.Equal(t, float64(1), rate.GetRateValue()) 17 | 18 | rate.Latest(`EUR`, `USD`) 19 | assert.Equal(t, float64(3.67), rate.GetRateValue()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/exchanger/helpers.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | // ReverseMap ... Reverse a map in 4 | func ReverseMap(m map[string]string) map[string]string { 5 | n := make(map[string]string) 6 | for k, v := range m { 7 | n[v] = k 8 | } 9 | return n 10 | } 11 | 12 | // MapKeyArrInterface ... Reverse a map in 13 | func MapKeyArrInterface(m map[string]string) []interface{} { 14 | var n []interface{} 15 | for k := range m { 16 | n = append(n, k) 17 | } 18 | return n 19 | } 20 | -------------------------------------------------------------------------------- /pkg/exchanger/openexchangerates.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | type openExchangeRatesApi struct { 14 | attributes 15 | } 16 | 17 | var ( 18 | openExchangeRatesApiUrl = `https://openexchangerates.org/api/convert/1/%s/%s?app_id=%s` 19 | openExchangeRatesApiHeaders = map[string][]string{ 20 | `Accept`: {`text/html,application/xhtml+xml,application/xml,application/json`}, 21 | `Accept-Encoding`: {`text`}, 22 | } 23 | ) 24 | 25 | func (c *openExchangeRatesApi) requestRate(from string, to string, opt ...interface{}) (*openExchangeRatesApi, error) { 26 | 27 | // todo add option opt to add more headers or client configurations 28 | // free mem-leak 29 | // optimize for memory leak 30 | // todo optimize curl connection 31 | 32 | url := fmt.Sprintf(openExchangeRatesApiUrl, c.apiKey, from, to) 33 | req, _ := http.NewRequest("GET", url, nil) 34 | 35 | openExchangeRatesApiHeaders[`User-Agent`] = []string{c.userAgent} 36 | req.Header = openExchangeRatesApiHeaders 37 | 38 | res, err := c.Client.Do(req) 39 | 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer res.Body.Close() 44 | 45 | body, err := ioutil.ReadAll(res.Body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // free mem-leak 51 | // todo discard data 52 | c.responseBody = string(body) 53 | return c, nil 54 | } 55 | 56 | // GetRateValue ... get exchange rate value 57 | func (c *openExchangeRatesApi) GetRateValue() float64 { 58 | return c.rateValue 59 | } 60 | 61 | // GetRateDateTime ... return rate datetime 62 | func (c *openExchangeRatesApi) GetRateDateTime() string { 63 | return c.rateDate.Format(time.RFC3339) 64 | } 65 | 66 | // GetExchangerName ... return exchanger name 67 | func (c *openExchangeRatesApi) GetExchangerName() string { 68 | return c.name 69 | } 70 | 71 | // Latest ... populate latest exchange rate 72 | func (c *openExchangeRatesApi) Latest(from string, to string, opt ...interface{}) error { 73 | 74 | _, err := c.requestRate(from, to, opt) 75 | if err != nil { 76 | log.Print(err) 77 | return err 78 | } 79 | 80 | // if from currency is same as converted currency return value of 1 81 | if from == to { 82 | c.rateValue = 1 83 | return nil 84 | } 85 | 86 | json, err := simplejson.NewJson([]byte(c.responseBody)) 87 | 88 | if err != nil { 89 | log.Print(err) 90 | return err 91 | } 92 | 93 | // opening price 94 | value := json.GetPath(`response`). 95 | MustFloat64() 96 | // todo handle error 97 | if value <= 0 { 98 | return fmt.Errorf(`error in retrieving exhcange rate is 0`) 99 | } 100 | c.rateValue = value 101 | c.rateDate = time.Now() 102 | return nil 103 | } 104 | 105 | // NewOpenExchangeRatesApi ... return new instance of openExchangeRatesApi 106 | func NewOpenExchangeRatesApi(opt map[string]string) *openExchangeRatesApi { 107 | keepAliveTimeout := 600 * time.Second 108 | timeout := 5 * time.Second 109 | defaultTransport := &http.Transport{ 110 | DialContext: (&net.Dialer{ 111 | Timeout: 30 * time.Second, 112 | KeepAlive: keepAliveTimeout, 113 | DualStack: true, 114 | }).DialContext, 115 | MaxIdleConns: 100, 116 | MaxIdleConnsPerHost: 100, 117 | } 118 | 119 | client := &http.Client{ 120 | Transport: defaultTransport, 121 | Timeout: timeout, 122 | } 123 | 124 | attr := attributes{ 125 | name: `openexchangerates`, 126 | Client: client, 127 | apiKey: opt[`apiKey`], 128 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 129 | } 130 | if opt[`userAgent`] != "" { 131 | attr.userAgent = opt[`userAgent`] 132 | } 133 | 134 | r := &openExchangeRatesApi{attr} 135 | return r 136 | } 137 | -------------------------------------------------------------------------------- /pkg/exchanger/openexchangerates_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestOpenExchangeRatesApi_Latest(t *testing.T) { 10 | rate := NewOpenExchangeRatesApi(nil) 11 | assert.Equal(t, rate.name, `openexchangerates`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`EUR`, `EUR`) 16 | assert.Equal(t, float64(1), rate.GetRateValue()) 17 | 18 | rate.Latest(`USD`, `AED`) 19 | assert.Equal(t, float64(3.6571), rate.GetRateValue()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/exchanger/themoneyconverter.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "net/http" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // @link : https://themoneyconverter.com/AED/EUR.aspx?amount=1 16 | 17 | type theMoneyConverterApi struct { 18 | attributes 19 | } 20 | 21 | var ( 22 | theMoneyConverterApiUrl = `https://themoneyconverter.com/%s/%s.aspx?amount=1` 23 | theMoneyConverterApiHeaders = map[string][]string{ 24 | `Accept`: {`text/html`}, 25 | } 26 | ) 27 | 28 | func (c *theMoneyConverterApi) requestRate(from string, to string, opt ...interface{}) (*theMoneyConverterApi, error) { 29 | 30 | // todo add option opt to add more headers or client configurations 31 | // free mem-leak 32 | // optimize for memory leak 33 | // todo optimize curl connection 34 | 35 | // format the url and replace currency 36 | url := fmt.Sprintf(theMoneyConverterApiUrl, from, to) 37 | // prepare the request 38 | req, _ := http.NewRequest("GET", url, nil) 39 | // assign the request headers 40 | theMoneyConverterApiHeaders[`User-Agent`] = []string{c.userAgent} 41 | req.Header = theMoneyConverterApiHeaders 42 | 43 | // execute the request 44 | res, err := c.Client.Do(req) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer res.Body.Close() 50 | 51 | body, err := ioutil.ReadAll(res.Body) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // free mem-leak 57 | // todo discard data 58 | c.responseBody = string(body) 59 | return c, nil 60 | } 61 | 62 | // GetRateValue ... get exchange rate value 63 | func (c *theMoneyConverterApi) GetRateValue() float64 { 64 | return c.rateValue 65 | } 66 | 67 | // GetRateDateTime ... return rate datetime 68 | func (c *theMoneyConverterApi) GetRateDateTime() string { 69 | return c.rateDate.Format(time.RFC3339) 70 | } 71 | 72 | // GetExchangerName ... return exchanger name 73 | func (c *theMoneyConverterApi) GetExchangerName() string { 74 | return c.name 75 | } 76 | 77 | // Latest ... populate latest exchange rate 78 | func (c *theMoneyConverterApi) Latest(from string, to string, opt ...interface{}) error { 79 | 80 | // todo cache layer 81 | _, err := c.requestRate(from, to, opt) 82 | if err != nil { 83 | log.Print(err) 84 | return err 85 | } 86 | 87 | // if from currency is same as converted currency return value of 1 88 | if from == to { 89 | c.rateValue = 1 90 | return nil 91 | } 92 | 93 | validID := regexp.MustCompile(`(?s)output(.*)>(.*)`) 94 | stringMatches := validID.FindStringSubmatch(c.responseBody) 95 | 96 | stringMatch := strings.TrimSpace(strings.Replace(stringMatches[2], "\n", "", -1)) 97 | stringMatch = strings.Replace(stringMatch, fmt.Sprintf("%d %s = ", 1, from), "", -1) 98 | stringMatch = strings.Replace(stringMatch, fmt.Sprintf(" %s", to), "", -1) 99 | 100 | c.rateValue, err = strconv.ParseFloat(stringMatch, 64) 101 | c.rateDate = time.Now() 102 | 103 | if err != nil { 104 | log.Print(err) 105 | return err 106 | } 107 | return nil 108 | } 109 | 110 | // NewTheMoneyConverterApi ... return new instance of theMoneyConverterApi 111 | func NewTheMoneyConverterApi(opt map[string]string) *theMoneyConverterApi { 112 | 113 | keepAliveTimeout := 600 * time.Second 114 | timeout := 5 * time.Second 115 | defaultTransport := &http.Transport{ 116 | DialContext: (&net.Dialer{ 117 | Timeout: 30 * time.Second, 118 | KeepAlive: keepAliveTimeout, 119 | DualStack: true, 120 | }).DialContext, 121 | MaxIdleConns: 100, 122 | MaxIdleConnsPerHost: 100, 123 | } 124 | 125 | client := &http.Client{ 126 | Transport: defaultTransport, 127 | Timeout: timeout, 128 | } 129 | 130 | attr := attributes{ 131 | name: `themoneyconverter`, 132 | Client: client, 133 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 134 | } 135 | if opt[`userAgent`] != "" { 136 | attr.userAgent = opt[`userAgent`] 137 | } 138 | 139 | r := &theMoneyConverterApi{attr} 140 | 141 | return r 142 | } 143 | -------------------------------------------------------------------------------- /pkg/exchanger/themoneyconverter_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestTheMoneyConverterApi_Latest(t *testing.T) { 10 | rate := NewTheMoneyConverterApi(nil) 11 | assert.Equal(t, rate.name, `themoneyconverter`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`EUR`, `EUR`) 16 | assert.Equal(t, float64(1), rate.GetRateValue()) 17 | 18 | rate.Latest(`USD`, `AED`) 19 | assert.Equal(t, float64(3.6725), rate.GetRateValue()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/exchanger/types.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // Rate ... Rate interface 9 | type Rate interface { 10 | GetRateValue() float64 11 | GetRateDateTime() string 12 | GetExchangerName() string 13 | } 14 | 15 | // Exchanger ... Exchanger interface 16 | type Exchanger interface { 17 | Latest(string, string, ...interface{}) error 18 | Rate 19 | } 20 | 21 | // attributes ... Exchanger attributes 22 | type attributes struct { 23 | apiVersion string 24 | apiKey string 25 | userAgent string 26 | responseBody string 27 | rateValue float64 28 | rateDate time.Time 29 | name string 30 | Client *http.Client // exposed for custom http clients or testing 31 | } 32 | -------------------------------------------------------------------------------- /pkg/exchanger/yahoo.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bitly/go-simplejson" 6 | "io/ioutil" 7 | "log" 8 | "math" 9 | "net" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | type yahooApi struct { 15 | attributes 16 | } 17 | 18 | var ( 19 | yahooApiUrl = `https://query1.finance.yahoo.com/v8/finance/chart/%s%s=X?region=US&lang=en-US&includePrePost=false&interval=1d&range=1d&corsDomain=finance.yahoo.com&.tsrc=finance` 20 | yahooApiHeaders = map[string][]string{ 21 | `Accept`: {`text/html,application/xhtml+xml,application/xml,application/json`}, 22 | `Accept-Encoding`: {`text`}, 23 | } 24 | ) 25 | 26 | func (c *yahooApi) requestRate(from string, to string, opt ...interface{}) (*yahooApi, error) { 27 | 28 | // todo add option opt to add more headers or client configurations 29 | // free mem-leak 30 | // optimize for memory leak 31 | // todo optimize curl connection 32 | 33 | url := fmt.Sprintf(yahooApiUrl, from, to) 34 | req, _ := http.NewRequest("GET", url, nil) 35 | 36 | yahooApiHeaders[`User-Agent`] = []string{c.userAgent} 37 | req.Header = yahooApiHeaders 38 | 39 | res, err := c.Client.Do(req) 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | defer res.Body.Close() 45 | 46 | body, err := ioutil.ReadAll(res.Body) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // free mem-leak 52 | // todo discard data 53 | c.responseBody = string(body) 54 | return c, nil 55 | } 56 | 57 | // GetRateValue ... get exchange rate value 58 | func (c *yahooApi) GetRateValue() float64 { 59 | return c.rateValue 60 | } 61 | 62 | // GetRateDateTime ... return rate datetime 63 | func (c *yahooApi) GetRateDateTime() string { 64 | return c.rateDate.Format(time.RFC3339) 65 | } 66 | 67 | // GetExchangerName ... return exchanger name 68 | func (c *yahooApi) GetExchangerName() string { 69 | return c.name 70 | } 71 | 72 | // Latest ... populate latest exchange rate 73 | func (c *yahooApi) Latest(from string, to string, opt ...interface{}) error { 74 | 75 | _, err := c.requestRate(from, to, opt) 76 | if err != nil { 77 | log.Print(err) 78 | return err 79 | } 80 | 81 | json, err := simplejson.NewJson([]byte(c.responseBody)) 82 | 83 | if err != nil { 84 | log.Print(err) 85 | return err 86 | } 87 | 88 | // opening price 89 | value := json.GetPath(`chart`, `result`). 90 | GetIndex(0). 91 | //GetPath(`indicators`, `adjclose`). 92 | //GetIndex(0). 93 | //GetPath(`adjclose`). 94 | //GetIndex(0). 95 | GetPath(`indicators`, `quote`). 96 | GetIndex(0). 97 | GetPath(`open`). 98 | GetIndex(0). 99 | MustFloat64() 100 | // todo handle error 101 | if value <= 0 { 102 | return fmt.Errorf(`error in retrieving exhcange rate is 0`) 103 | } 104 | c.rateValue = math.Round(value*1000000) / 1000000 105 | c.rateDate = time.Now() 106 | return nil 107 | } 108 | 109 | // NewYahooApi ... return new instance of yahooApi 110 | func NewYahooApi(opt map[string]string) *yahooApi { 111 | keepAliveTimeout := 600 * time.Second 112 | timeout := 5 * time.Second 113 | defaultTransport := &http.Transport{ 114 | DialContext: (&net.Dialer{ 115 | Timeout: 30 * time.Second, 116 | KeepAlive: keepAliveTimeout, 117 | DualStack: true, 118 | }).DialContext, 119 | MaxIdleConns: 100, 120 | MaxIdleConnsPerHost: 100, 121 | } 122 | 123 | client := &http.Client{ 124 | Transport: defaultTransport, 125 | Timeout: timeout, 126 | } 127 | 128 | attr := attributes{ 129 | name: `yahoo`, 130 | Client: client, 131 | userAgent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0`, 132 | } 133 | if opt[`userAgent`] != "" { 134 | attr.userAgent = opt[`userAgent`] 135 | } 136 | 137 | r := &yahooApi{attr} 138 | return r 139 | } 140 | -------------------------------------------------------------------------------- /pkg/exchanger/yahoo_test.go: -------------------------------------------------------------------------------- 1 | package exchanger 2 | 3 | import ( 4 | "github.com/me-io/go-swap/test/staticMock" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestYahooApi_Latest(t *testing.T) { 10 | rate := NewYahooApi(nil) 11 | assert.Equal(t, rate.name, `yahoo`) 12 | 13 | rate.Client.Transport = staticMock.NewTestMT() 14 | 15 | rate.Latest(`USD`, `EUR`) 16 | assert.Equal(t, float64(0.272272), rate.GetRateValue()) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/swap/doc.go: -------------------------------------------------------------------------------- 1 | package swap // import "github.com/me-io/go-swap/pkg/swap" 2 | -------------------------------------------------------------------------------- /pkg/swap/swap.go: -------------------------------------------------------------------------------- 1 | package swap 2 | 3 | import ( 4 | "fmt" 5 | ex "github.com/me-io/go-swap/pkg/exchanger" 6 | "log" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | // NewSwap ... configure new swap instance 12 | func NewSwap(opt ...string) *Swap { 13 | // todo add options 14 | // cache 15 | // timeout etc ... 16 | return &Swap{} 17 | } 18 | 19 | // AddExchanger ... add service to the swap stack 20 | func (b *Swap) AddExchanger(interfaceClass ex.Exchanger) *Swap { 21 | if interfaceClass != nil { 22 | b.exchangers = append(b.exchangers, interfaceClass) 23 | } 24 | return b 25 | } 26 | 27 | // Build ... build and init swap object 28 | func (b *Swap) Build() *Swap { 29 | return b 30 | } 31 | 32 | // Latest ... get latest rate exchange from the first api that respond from the swap stack 33 | func (b *Swap) Latest(currencyPair string) ex.Exchanger { 34 | if len(b.exchangers) < 1 { 35 | // configure at least one service 36 | log.Panic(400) 37 | } 38 | 39 | // todo 40 | var currentSrc ex.Exchanger 41 | errArr := map[string]string{} 42 | 43 | args := strings.Split(currencyPair, "/") 44 | for _, srv := range b.exchangers { 45 | err := srv.Latest(args[0], args[1], nil) 46 | 47 | if err != nil { 48 | // add errors to array so we can log them 49 | errArr[reflect.TypeOf(srv).String()] = fmt.Sprint(err) 50 | continue 51 | } 52 | // assign the service after first working service and break the loop 53 | currentSrc = srv 54 | break 55 | } 56 | 57 | if currentSrc == nil { 58 | log.Panic(errArr) 59 | } 60 | return currentSrc 61 | } 62 | -------------------------------------------------------------------------------- /pkg/swap/swap_test.go: -------------------------------------------------------------------------------- 1 | package swap 2 | 3 | import ( 4 | ex "github.com/me-io/go-swap/pkg/exchanger" 5 | "github.com/me-io/go-swap/test/staticMock" 6 | "github.com/stretchr/testify/assert" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestSwap_New(t *testing.T) { 12 | SwapTest := NewSwap() 13 | assert.Equal(t, "*swap.Swap", reflect.TypeOf(SwapTest).String()) 14 | } 15 | 16 | //func TestSwap_Latest_Error(t *testing.T) { 17 | // SwapTest := NewSwap() 18 | // assert.Equal(t, "*swap.Swap", reflect.TypeOf(SwapTest).String()) 19 | // SwapTest.Build() 20 | // SwapTest.Latest("EUR/USD") 21 | //} 22 | 23 | func TestSwap_AddExchanger(t *testing.T) { 24 | SwapTest := NewSwap() 25 | 26 | g := ex.NewGoogleApi(nil) 27 | g.Client.Transport = staticMock.NewTestMT() 28 | y := ex.NewYahooApi(nil) 29 | y.Client.Transport = staticMock.NewTestMT() 30 | 31 | SwapTest. 32 | AddExchanger(g). 33 | AddExchanger(y). 34 | Build() 35 | assert.Equal(t, "*swap.Swap", reflect.TypeOf(SwapTest).String()) 36 | } 37 | 38 | func TestSwap_Build_Google(t *testing.T) { 39 | SwapTest := NewSwap() 40 | 41 | g := ex.NewGoogleApi(nil) 42 | g.Client.Transport = staticMock.NewTestMT() 43 | 44 | SwapTest. 45 | AddExchanger(g). 46 | Build() 47 | 48 | euroToUsdRate := SwapTest.Latest("EUR/USD") 49 | assert.Equal(t, float64(3.67), euroToUsdRate.GetRateValue()) 50 | assert.Equal(t, `google`, euroToUsdRate.GetExchangerName()) 51 | 52 | usdToUsdRate := SwapTest.Latest("USD/USD") 53 | assert.Equal(t, float64(1), usdToUsdRate.GetRateValue()) 54 | assert.Equal(t, `google`, euroToUsdRate.GetExchangerName()) 55 | } 56 | 57 | func TestSwap_Build_Yahoo(t *testing.T) { 58 | SwapTest := NewSwap() 59 | 60 | y := ex.NewYahooApi(nil) 61 | y.Client.Transport = staticMock.NewTestMT() 62 | 63 | SwapTest. 64 | AddExchanger(y). 65 | Build() 66 | 67 | euroToUsdRate := SwapTest.Latest("EUR/USD") 68 | assert.Equal(t, float64(0.272272), euroToUsdRate.GetRateValue()) 69 | assert.Equal(t, `yahoo`, euroToUsdRate.GetExchangerName()) 70 | } 71 | 72 | func TestSwap_Build_Stack_Google_Yahoo(t *testing.T) { 73 | SwapTest := NewSwap() 74 | g := ex.NewGoogleApi(nil) 75 | g.Client.Transport = staticMock.NewTestMT() 76 | y := ex.NewYahooApi(nil) 77 | y.Client.Transport = staticMock.NewTestMT() 78 | 79 | SwapTest. 80 | AddExchanger(g). 81 | AddExchanger(y). 82 | Build() 83 | 84 | euroToUsdRate := SwapTest.Latest("EUR/USD") 85 | assert.Equal(t, float64(3.67), euroToUsdRate.GetRateValue()) 86 | assert.Equal(t, `google`, euroToUsdRate.GetExchangerName()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/swap/types.go: -------------------------------------------------------------------------------- 1 | package swap 2 | 3 | import ( 4 | ex "github.com/me-io/go-swap/pkg/exchanger" 5 | ) 6 | 7 | // Swap ... main struct 8 | type Swap struct { 9 | exchangers []ex.Exchanger 10 | cache interface{} 11 | } 12 | -------------------------------------------------------------------------------- /scripts/build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REPO_NAME="meio/go-swap-server" 4 | GIT_TAG=`git describe --tags --always --dirty` 5 | GO_VER=`go version` 6 | OS="linux" 7 | ARCH="amd64" 8 | DOCKER_TAG="${OS}-${ARCH}-${GIT_TAG}" 9 | 10 | # build only tag branch in this format 0.0.0 11 | if [[ ${GIT_TAG} =~ ^[[:digit:].[:digit:].[:digit:]]+$ ]]; then 12 | true 13 | echo "TAG: ${GIT_TAG} - start build" 14 | else 15 | echo "TAG: ${GIT_TAG} - skip build" 16 | exit 0 17 | fi 18 | 19 | # build only with go version 1.11 20 | if [[ ${GO_VER} =~ 1\.11 ]]; then 21 | echo "GO_VER: ${GO_VER} - start build" 22 | else 23 | echo "GO_VER: ${GO_VER} - skip build" 24 | exit 0 25 | fi 26 | 27 | if [[ ! -z "${DOCKER_PASSWORD}" && ! -z "${DOCKER_USERNAME}" ]] 28 | then 29 | echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin 30 | fi 31 | 32 | TAG_EXIST=`curl -s "https://hub.docker.com/v2/repositories/${REPO_NAME}/tags/${DOCKER_TAG}/" | grep '"id":'` 33 | 34 | if [[ ! -z ${TAG_EXIST} ]]; then 35 | echo "${REPO_NAME}:${DOCKER_TAG} already exist" 36 | exit 0 37 | fi 38 | 39 | 40 | docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ 41 | --build-arg VCS_REF=`git rev-parse --short HEAD` \ 42 | --build-arg DOCKER_TAG="${DOCKER_TAG}" \ 43 | --build-arg VERSION="${GIT_TAG}" \ 44 | -t ${REPO_NAME}:${DOCKER_TAG} -f .dockerfile-${OS}-${ARCH} . 45 | 46 | if [[ $? != 0 ]]; then 47 | echo "${REPO_NAME}:${DOCKER_TAG} build failed" 48 | exit 1 49 | fi 50 | 51 | 52 | if [[ -z ${TAG_EXIST} ]]; then 53 | docker push ${REPO_NAME}:${DOCKER_TAG} 54 | echo "${REPO_NAME}:${DOCKER_TAG} pushed successfully" 55 | 56 | fi 57 | 58 | # push latest 59 | docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ 60 | --build-arg VCS_REF=`git rev-parse --short HEAD` \ 61 | --build-arg DOCKER_TAG="${DOCKER_TAG}" \ 62 | --build-arg VERSION="${GIT_TAG}" \ 63 | -t ${REPO_NAME}:latest -f .dockerfile-${OS}-${ARCH} . 64 | 65 | docker push ${REPO_NAME}:latest 66 | echo "${REPO_NAME}:latest pushed successfully" 67 | 68 | # update microbadger.com 69 | curl -XPOST "https://hooks.microbadger.com/images/meio/go-swap-server/TOoBKgNqzCZH6dNBlAopouqsLF0=" 70 | -------------------------------------------------------------------------------- /scripts/build-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | export CGO_ENABLED=0 8 | my_dir="$(dirname "$0")" 9 | 10 | go build -o ${my_dir}/../bin/server cmd/server/*.go 11 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if [ -z "${PKG}" ]; then 8 | echo "PKG must be set" 9 | exit 1 10 | fi 11 | if [ -z "${ARCH}" ]; then 12 | echo "ARCH must be set" 13 | exit 1 14 | fi 15 | if [ -z "${VERSION}" ]; then 16 | echo "VERSION must be set" 17 | exit 1 18 | fi 19 | 20 | if [ -z "${OS}" ]; then 21 | echo "OS must be set" 22 | exit 1 23 | fi 24 | 25 | export CGO_ENABLED=0 26 | export GOARCH="${ARCH}" 27 | export GOOS="${OS}" 28 | 29 | go install \ 30 | -installsuffix "static" \ 31 | -ldflags "-X ${PKG}/pkg/version.VERSION=${VERSION}" \ 32 | ./... 33 | -------------------------------------------------------------------------------- /scripts/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec ${BINSRC_ENV} "$@" 4 | 5 | -------------------------------------------------------------------------------- /scripts/server-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | my_dir="$(dirname "$0")" 8 | 9 | docker pull meio/go-swap-server:latest 10 | docker run --rm --name go-swap-server -u 0 -p 5000:5000 -it meio/go-swap-server:latest 11 | -------------------------------------------------------------------------------- /scripts/server-redis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | my_dir="$(dirname "$0")" 8 | 9 | GO_FILES=`find ${my_dir}/../cmd/server/. -type f \( -iname "*.go" ! -iname "*_test.go" \)` 10 | cmd="go run ${GO_FILES} -CACHE=redis -REDIS_URL=redis://localhost:6379" 11 | ${cmd} 12 | -------------------------------------------------------------------------------- /scripts/server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | my_dir="$(dirname "$0")" 8 | 9 | GO_FILES=`find ${my_dir}/../cmd/server/. -type f \( -iname "*.go" ! -iname "*_test.go" \)` 10 | go run ${GO_FILES} 11 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | export CGO_ENABLED=0 8 | 9 | TARGETS=$(for d in "$@"; do echo ./$d/...; done) 10 | 11 | echo "Running tests: Install packages: " 12 | go test -v -i -installsuffix "static" ${TARGETS} # Install packages that are dependencies of the test. Do not run the test.. 13 | echo "Running tests: Run: " 14 | go test -covermode=count -coverprofile=profile.cov -v -installsuffix "static" ${TARGETS} 15 | echo 16 | 17 | echo -n "Checking gofmt: " 18 | ERRS=$(find "$@" -type f -name \*.go | xargs gofmt -l 2>&1 || true) 19 | if [ -n "${ERRS}" ]; then 20 | echo "FAIL - the following files need to be gofmt'ed:" 21 | for e in ${ERRS}; do 22 | echo " $e" 23 | done 24 | echo 25 | exit 1 26 | fi 27 | echo "PASS" 28 | echo 29 | 30 | echo -n "Checking go vet: " 31 | ERRS=$(go vet ${TARGETS} 2>&1 || true) 32 | if [ -n "${ERRS}" ]; then 33 | echo "FAIL" 34 | echo "${ERRS}" 35 | echo 36 | exit 1 37 | fi 38 | echo "PASS" 39 | echo 40 | -------------------------------------------------------------------------------- /test/scripts/google.json: -------------------------------------------------------------------------------- 1 | { 2 | "amount": 3, 3 | "exchanger": [ 4 | { 5 | "name": "google", 6 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 7 | } 8 | ], 9 | "from": "USD", 10 | "to": "AED", 11 | "decimalPoints": 4 12 | } 13 | -------------------------------------------------------------------------------- /test/scripts/multi_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "amount": 3, 3 | "exchanger": [ 4 | { 5 | "name": "currencyLayer", 6 | "apiKey": "12312", 7 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 8 | }, 9 | { 10 | "name": "yahoo", 11 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 12 | }, 13 | { 14 | "name": "google", 15 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 16 | }, 17 | { 18 | "name": "fixer", 19 | "apiKey": "12312", 20 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 21 | }, 22 | { 23 | "name": "google", 24 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 25 | } 26 | ], 27 | "from": "USD", 28 | "to": "AED", 29 | "decimalPoints": 5 30 | } 31 | -------------------------------------------------------------------------------- /test/scripts/request.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | my_dir="$(dirname "$0")" 4 | filename=${1-multi_1} 5 | file=${my_dir}/${filename}.json 6 | host=${2-localhost} 7 | port=${3-5000} 8 | echo "file: ${file}" 9 | 10 | curl -X POST -H "Content-Type: application/json" -d @${file} http://${host}:${port}/convert 11 | -------------------------------------------------------------------------------- /test/scripts/yahoo.json: -------------------------------------------------------------------------------- 1 | { 2 | "amount": 3, 3 | "exchanger": [ 4 | { 5 | "name": "yahoo", 6 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0" 7 | } 8 | ], 9 | "from": "USD", 10 | "to": "AED", 11 | "cacheTime": "10s", 12 | "decimalPoints": 6 13 | } 14 | -------------------------------------------------------------------------------- /test/staticMock/1forge_json_aed_usd.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": 3.675, 3 | "text": "1 USD is worth 3.675 AED", 4 | "timestamp": 1538254496 5 | } 6 | -------------------------------------------------------------------------------- /test/staticMock/currencylayer_json_aed_usd.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "terms": "https:\/\/currencylayer.com\/terms", 4 | "privacy": "https:\/\/currencylayer.com\/privacy", 5 | "query": { 6 | "from": "AED", 7 | "to": "USD", 8 | "amount": 25 9 | }, 10 | "info": { 11 | "timestamp": 1537250111, 12 | "quote": 0.272256 13 | }, 14 | "result": 6.8064 15 | } 16 | -------------------------------------------------------------------------------- /test/staticMock/doc.go: -------------------------------------------------------------------------------- 1 | package staticMock // import "github.com/me-io/go-swap/test/staticMock" 2 | -------------------------------------------------------------------------------- /test/staticMock/fixer_json_aed_usd.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "query": { 4 | "from": "GBP", 5 | "to": "JPY", 6 | "amount": 25 7 | }, 8 | "info": { 9 | "timestamp": 1519328414, 10 | "rate": 148.972231 11 | }, 12 | "historical": "", 13 | "date": "2018-02-22", 14 | "result": 3724.305775 15 | } 16 | -------------------------------------------------------------------------------- /test/staticMock/google_html_aed_usd.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /test/staticMock/openexchangerates_json_aed_usd.json: -------------------------------------------------------------------------------- 1 | { 2 | "disclaimer": "https://openexchangerates.org/terms/", 3 | "license": "https://openexchangerates.org/license/", 4 | "request": { 5 | "query": "/convert/1/USD/AED", 6 | "amount": 1, 7 | "from": "USD", 8 | "to": "AED" 9 | }, 10 | "meta": { 11 | "timestamp": 1449885661, 12 | "rate": 3.6571 13 | }, 14 | "response": 3.6571 15 | } 16 | -------------------------------------------------------------------------------- /test/staticMock/themoneyconverter_html_aed_usd.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 1 USD = 3.67250 AED 4 | 5 |
6 |
7 | invert currencies 8 | ↓↑ 9 |
10 | -------------------------------------------------------------------------------- /test/staticMock/transport.go: -------------------------------------------------------------------------------- 1 | package staticMock 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | type mT struct{} 12 | 13 | // NewTestMT ... return new RoundTripper for mocking http response 14 | func NewTestMT() http.RoundTripper { 15 | return &mT{} 16 | } 17 | 18 | // Implement http.RoundTripper 19 | func (t *mT) RoundTrip(req *http.Request) (*http.Response, error) { 20 | // Create mocked http.Response 21 | response := &http.Response{ 22 | Header: make(http.Header), 23 | Request: req, 24 | StatusCode: http.StatusOK, 25 | } 26 | // url := req.URL.RequestURI() 27 | host := req.URL.Host 28 | 29 | _, filename, _, _ := runtime.Caller(0) 30 | tPath := filepath.Dir(filename) 31 | 32 | responseBody := `` 33 | response.Header.Set("Content-Type", "application/json") 34 | 35 | switch { 36 | case host == `www.google.com`: 37 | response.Header.Set("Content-Type", "text/html") 38 | fc, _ := ioutil.ReadFile(tPath + `/google_html_aed_usd.html`) 39 | responseBody = string(fc) 40 | break 41 | case host == `query1.finance.yahoo.com`: 42 | fp, _ := filepath.Abs(tPath + `/yahoo_json_aed_usd.json`) 43 | fc, _ := ioutil.ReadFile(fp) 44 | responseBody = string(fc) 45 | break 46 | case host == `apilayer.net`: 47 | fp, _ := filepath.Abs(tPath + `/currencylayer_json_aed_usd.json`) 48 | fc, _ := ioutil.ReadFile(fp) 49 | responseBody = string(fc) 50 | break 51 | case host == `data.fixer.io`: 52 | fp, _ := filepath.Abs(tPath + `/fixer_json_aed_usd.json`) 53 | fc, _ := ioutil.ReadFile(fp) 54 | responseBody = string(fc) 55 | break 56 | case host == `forex.1forge.com`: 57 | fp, _ := filepath.Abs(tPath + `/1forge_json_aed_usd.json`) 58 | fc, _ := ioutil.ReadFile(fp) 59 | responseBody = string(fc) 60 | break 61 | case host == `themoneyconverter.com`: 62 | fp, _ := filepath.Abs(tPath + `/themoneyconverter_html_aed_usd.html`) 63 | fc, _ := ioutil.ReadFile(fp) 64 | responseBody = string(fc) 65 | break 66 | case host == `openexchangerates.org`: 67 | fp, _ := filepath.Abs(tPath + `/openexchangerates_json_aed_usd.json`) 68 | fc, _ := ioutil.ReadFile(fp) 69 | responseBody = string(fc) 70 | break 71 | default: 72 | 73 | } 74 | 75 | response.Body = ioutil.NopCloser(strings.NewReader(responseBody)) 76 | return response, nil 77 | } 78 | -------------------------------------------------------------------------------- /test/staticMock/yahoo_json_aed_usd.json: -------------------------------------------------------------------------------- 1 | { 2 | "chart": { 3 | "result": [ 4 | { 5 | "meta": { 6 | "currency": "USD", 7 | "symbol": "AEDUSD=X", 8 | "exchangeName": "CCY", 9 | "instrumentType": "CURRENCY", 10 | "firstTradeDate": 1070236800, 11 | "gmtoffset": 3600, 12 | "timezone": "BST", 13 | "exchangeTimezoneName": "Europe/London", 14 | "chartPreviousClose": 0.2723, 15 | "priceHint": 4, 16 | "currentTradingPeriod": { 17 | "pre": { 18 | "timezone": "BST", 19 | "start": 1537138800, 20 | "end": 1537138800, 21 | "gmtoffset": 3600 22 | }, 23 | "regular": { 24 | "timezone": "BST", 25 | "start": 1537138800, 26 | "end": 1537225140, 27 | "gmtoffset": 3600 28 | }, 29 | "post": { 30 | "timezone": "BST", 31 | "start": 1537225140, 32 | "end": 1537225140, 33 | "gmtoffset": 3600 34 | } 35 | }, 36 | "dataGranularity": "1d", 37 | "validRanges": [ 38 | "1d", 39 | "5d", 40 | "1mo", 41 | "3mo", 42 | "6mo", 43 | "1y", 44 | "2y", 45 | "5y", 46 | "10y", 47 | "ytd", 48 | "max" 49 | ] 50 | }, 51 | "timestamp": [ 52 | 1537186345 53 | ], 54 | "indicators": { 55 | "quote": [ 56 | { 57 | "open": [ 58 | 0.2722718417644501 59 | ], 60 | "volume": [ 61 | 0 62 | ], 63 | "high": [ 64 | 0.27238309383392334 65 | ], 66 | "low": [ 67 | 0.27226442098617554 68 | ], 69 | "close": [ 70 | 0.2722792625427246 71 | ] 72 | } 73 | ], 74 | "adjclose": [ 75 | { 76 | "adjclose": [ 77 | 0.2722792625427246 78 | ] 79 | } 80 | ] 81 | } 82 | } 83 | ], 84 | "error": null 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package go_swap 2 | 3 | // VERSION is the app-global version string, which should be substituted with a 4 | // real value during build. 5 | var VERSION = "0.0.1" 6 | --------------------------------------------------------------------------------