├── .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 | [](https://travis-ci.org/me-io/go-swap)
5 | [](https://goreportcard.com/report/github.com/me-io/go-swap)
6 | [](https://coveralls.io/github/me-io/go-swap?branch=master)
7 | [](https://godoc.org/github.com/me-io/go-swap)
8 | [](https://github.com/me-io/go-swap/releases)
9 |
10 |
11 | [](https://meabed.com)
12 | [](https://microbadger.com/images/meio/go-swap-server)
13 | [](https://microbadger.com/images/meio/go-swap-server)
14 | [](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 |
22 |
23 |
24 |
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 | - [](https://app.getpostman.com/run-collection/5f8445ef9a390fd3faa1)
73 |
74 | ## QuickStart
75 |
76 |
77 |
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 |
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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------