diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..d2961b4 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,38 @@ +name: tests + +on: + push: + branches: + - '**' + pull_request: + +jobs: + test: + strategy: + matrix: + go-version: [ 1.17.x ] + os: [ ubuntu-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/cache@v2 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + ~/Library/Caches/go-build + %LocalAppData%\go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Test + run: go test ./... + - name: Run coverage + run: go test -race -coverprofile=coverage.out -covermode=atomic + - name: Upload coverage to Codecov + run: bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/.github/workflows/code_quality.yaml b/.github/workflows/code_quality.yaml new file mode 100644 index 0000000..c27b82b --- /dev/null +++ b/.github/workflows/code_quality.yaml @@ -0,0 +1,24 @@ +name: golangci-lint + +on: + push: + branches: + - '**' + pull_request: + +jobs: + golangci: + strategy: + matrix: + go-version: [ 1.17.x ] + os: [ ubuntu-latest ] + name: lint + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: latest + skip-build-cache: true + skip-pkg-cache: true \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f103d0b --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,28 @@ +name: release + +on: + push: + tags: + - v* + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - name: Unshallow + run: git fetch --prune --unshallow + + - name: Create release + uses: goreleaser/goreleaser-action@v2 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb24029 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +.idea +dist/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..2070fad --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,44 @@ +--- +######################### +######################### +## Golang Linter rules ## +######################### +######################### + +# configure golangci-lint +# see https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml +issues: + exclude-rules: + - path: _test\.go + linters: + - dupl + - gosec + - goconst +linters: + enable: + - gosec + - unconvert + - gocyclo + - goconst + - goimports + - gocritic + disable: + - maligned + - golint +linters-settings: + errcheck: + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: true + govet: + # report about shadowed variables + check-shadowing: true + enable: + - fieldalignment + - revive + gocyclo: + # minimal code complexity to report, 30 by default + min-complexity: 15 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..07c161c --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,23 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + - go mod tidy +project_name : apidq-client-go + +build: + skip: true + +release: + github: + prerelease: auto + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..6ceb7b5 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: golangci-lint + name: golangci-lint + description: Fast linters runner for Go. + entry: golangci-lint run --fix + types: [go] + language: golang + pass_filenames: false \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/address.go b/address.go new file mode 100644 index 0000000..c66db59 --- /dev/null +++ b/address.go @@ -0,0 +1,42 @@ +package apidq + +import ( + "context" + "net/http" + + "github.com/nikitaksv/apidq-client-go/dto/address" +) + +type AddressService struct { + *service +} + +// Clean Стандартизация адреса +func (s AddressService) Clean(ctx context.Context, req *address.CleanRequest) (rsp *address.CleanResponse, httpRsp *http.Response, err error) { + rsp = &address.CleanResponse{} + httpRsp, err = s.post(ctx, ServiceAddress, "/v1/clean/address", req, rsp) + if err != nil { + return + } + return +} + +// CleanIqdq Стандартизация адреса в формате старого API +func (s AddressService) CleanIqdq(ctx context.Context, req *address.CleanRequest) (rsp *address.CleanIqdqResponse, httpRsp *http.Response, err error) { + rsp = &address.CleanIqdqResponse{} + httpRsp, err = s.post(ctx, ServiceAddress, "/v1/clean/address/iqdq", req, rsp) + if err != nil { + return + } + return +} + +// Suggest Подсказки адреса +func (s AddressService) Suggest(ctx context.Context, req *address.SuggestRequest) (rsp *address.SuggestResponse, httpRsp *http.Response, err error) { + rsp = &address.SuggestResponse{} + httpRsp, err = s.post(ctx, ServiceAddress, "/v1/suggest/address", req, rsp) + if err != nil { + return + } + return +} diff --git a/address_test.go b/address_test.go new file mode 100644 index 0000000..b9726e8 --- /dev/null +++ b/address_test.go @@ -0,0 +1,43 @@ +package apidq + +import ( + "context" + "net/http" + "testing" + + "github.com/nikitaksv/apidq-client-go/dto/address" +) + +func TestAddressClean(t *testing.T) { + reqBs := []byte(`{"query":"москва спартаковская 10с12","countryCode":"RU"}`) + rspBs := []byte(`{"original":"москва спартаковская 10с12","address":"г Москва, пл Спартаковская","postcodeIn":"","postcode":"105082","region":{"fullName":"г Москва","name":"Москва","type":"г","codes":{"fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","ga":"RU0770000000000000000000000","osm":""}},"area":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"city":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"cityArea":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"settlement":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"planStructure":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"street":{"fullName":"пл Спартаковская","name":"Спартаковская","type":"пл","codes":{"fias":"cd6717bf-1b64-4004-a042-ff1164313e7c","ga":"RU0770000000000000000002733","osm":""}},"houseDetails":{"fullName":"дом 10, строение 12","floor":"","house":"10","case":"","build":"12","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"coordinates":{"latitude":55.777322,"longitude":37.677688},"country":{"name":"Россия","alpha2":"RU","alpha3":"RUS","numeric":643},"valid":true,"quality":{"unique":0,"actuality":0,"undefined":0,"level":8,"house":3,"geo":8}}`) + testEndpointCall(t, reqBs, rspBs, func(client *Client) (interface{}, *http.Response, error) { + return client.Address.Clean(context.Background(), &address.CleanRequest{ + Query: "москва спартаковская 10с12", + CountryCode: "RU", + }) + }) +} + +func TestAddressCleanIqdq(t *testing.T) { + reqBs := []byte(`{"query":"москва спартаковская 10с12","countryCode":"RU"}`) + rspBs := []byte(`{"c_ischeck":"1","c_index_in":"","c_zipcode":"105082","c_address_original":"москва спартаковская 10с12","c_address_full":"г Москва, пл Спартаковская","c_kladr":"","c_gaCode":"","c_country":"Россия","c_country_iso_code":"RU","c_region_name":"Москва","c_region_abbr":"г","c_region_fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","c_district_name":"","c_district_abbr":"","c_district_fias":"","c_city_name":"","c_city_abbr":"","c_city_fias":"","c_community_name":"","c_community_abbr":"","c_community_fias":"","c_street_name":"Спартаковская","c_street_abbr":"пл","c_street_fias":"cd6717bf-1b64-4004-a042-ff1164313e7c","c_json_kvant":{"fullName":"дом 10, строение 12","floor":"","house":"10","case":"","build":"12","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"c_house_str":"дом 10, строение 12","c_addr_lost":"","c_status_error":"000038","c_house_error":"3","c_house_error_desc":"","c_kladr19":"","c_gninmb":"","c_okato":"","c_oktmo":"","c_aoguid":"","c_aolevel":"8","c_houseguid":"","c_timezone":"","c_coordinate":{"c_lon":37.677688,"c_lat":55.777322,"c_level":8}}`) + testEndpointCall(t, reqBs, rspBs, func(client *Client) (interface{}, *http.Response, error) { + return client.Address.CleanIqdq(context.Background(), &address.CleanRequest{ + Query: "москва спартаковская 10с12", + CountryCode: "RU", + }) + }) +} + +func TestAddressSuggest(t *testing.T) { + reqBs := []byte(`{"query": "москва варш","countryCode": "RU", "count": 5}`) + rspBs := []byte(`{"suggestions":[{"address":"г Москва, Варшавское ш","postcode":"117105","region":{"fullName":"г Москва","name":"Москва","type":"г","codes":{"fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","ga":"RU0770000000000000000000000","osm":""}},"area":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"city":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"cityArea":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"settlement":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"planStructure":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"street":{"fullName":"Варшавское ш","name":"Варшавское","type":"ш","codes":{"fias":"8fc06b0b-5de3-4a72-9e6f-9e0647a37a66","ga":"RU0770000000000000000000476","osm":""}},"houseDetails":{"fullName":"","floor":"","house":"","case":"","build":"","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"coordinates":{"latitude":55.646,"longitude":37.6203},"country":{"name":"Россия","alpha2":"RU","alpha3":"RUS","numeric":643}},{"address":"г Москва, 2-й Варшавский проезд","postcode":"115201","region":{"fullName":"г Москва","name":"Москва","type":"г","codes":{"fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","ga":"RU0770000000000000000000000","osm":""}},"area":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"city":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"cityArea":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"settlement":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"planStructure":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"street":{"fullName":"2-й Варшавский проезд","name":"2-й Варшавский","type":"проезд","codes":{"fias":"b89718e1-8b56-4ba8-8383-5c7b596aee6c","ga":"RU0770000000000000000000475","osm":""}},"houseDetails":{"fullName":"","floor":"","house":"","case":"","build":"","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"coordinates":{"latitude":55.6442,"longitude":37.63},"country":{"name":"Россия","alpha2":"RU","alpha3":"RUS","numeric":643}},{"address":"г Москва, 1-й Варшавский проезд","postcode":"115201","region":{"fullName":"г Москва","name":"Москва","type":"г","codes":{"fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","ga":"RU0770000000000000000000000","osm":""}},"area":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"city":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"cityArea":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"settlement":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"planStructure":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"street":{"fullName":"1-й Варшавский проезд","name":"1-й Варшавский","type":"проезд","codes":{"fias":"09ffd474-1ca8-42e1-8217-876300fd7c2c","ga":"RU0770000000000000000000474","osm":""}},"houseDetails":{"fullName":"","floor":"","house":"","case":"","build":"","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"coordinates":{"latitude":55.6501,"longitude":37.6264},"country":{"name":"Россия","alpha2":"RU","alpha3":"RUS","numeric":643}},{"address":"г Москва, п Вороновское, Варшавское 64-й км ш","postcode":"108830","region":{"fullName":"г Москва","name":"Москва","type":"г","codes":{"fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","ga":"RU0770000000000000000000000","osm":""}},"area":{"fullName":"п Вороновское","name":"Вороновское","type":"п","codes":{"fias":"10409e98-eb2d-4a52-acdd-7166ca7e0e48","ga":"RU0770020000000000000000000","osm":""}},"city":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"cityArea":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"settlement":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"planStructure":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"street":{"fullName":"Варшавское 64-й км ш","name":"Варшавское 64-й км","type":"ш","codes":{"fias":"dc6cb90e-fe77-44c7-93f7-ec39909489e1","ga":"RU0770020000000000000000015","osm":""}},"houseDetails":{"fullName":"","floor":"","house":"","case":"","build":"","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"coordinates":{"latitude":55.2921,"longitude":37.1821},"country":{"name":"Россия","alpha2":"RU","alpha3":"RUS","numeric":643}},{"address":"г Москва, Варшавское шоссе 28-й км (п Воскресенско км","postcode":"117148","region":{"fullName":"г Москва","name":"Москва","type":"г","codes":{"fias":"0c5b2444-70a0-4932-980c-b4dc0d3f02b5","ga":"RU0770000000000000000000000","osm":""}},"area":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"city":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"cityArea":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"settlement":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"planStructure":{"fullName":"","name":"","type":"","codes":{"fias":"","ga":"","osm":""}},"street":{"fullName":"Варшавское шоссе 28-й км (п Воскресенско км","name":"Варшавское шоссе 28-й км (п Воскресенско","type":"км","codes":{"fias":"b4a45703-7ca1-4dff-9f9d-8e34deadbf33","ga":"RU0770000000000000000007569","osm":""}},"houseDetails":{"fullName":"","floor":"","house":"","case":"","build":"","liter":"","lend":"","block":"","pav":"","flat":"","office":"","kab":"","abon":"","plot":"","sek":"","entr":"","room":"","hostel":"","munit":""},"coordinates":{"latitude":55.4926,"longitude":37.5928},"country":{"name":"Россия","alpha2":"RU","alpha3":"RUS","numeric":643}}]}`) + testEndpointCall(t, reqBs, rspBs, func(client *Client) (interface{}, *http.Response, error) { + return client.Address.Suggest(context.Background(), &address.SuggestRequest{ + Query: "москва варш", + CountryCode: "RU", + Count: 5, + }) + }) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..2092681 --- /dev/null +++ b/client.go @@ -0,0 +1,209 @@ +package apidq + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + BaseURL = "https://api.apidq.io/" + contentTypeJSON = "application/json" + acceptJSON = "application/json" + authorization = "Authorization" + + ServiceAddress = "address" + ServicePhone = "phone" + ServiceName = "name" + + ctxKeyService ctxKey = iota +) + +type ctxKey int + +type service struct { + client *Client +} + +func (s *service) prepareCtx(ctx context.Context, service string) context.Context { + return context.WithValue(ctx, ctxKeyService, service) +} + +func (s *service) post(ctx context.Context, service, url string, req, rsp interface{}) (*http.Response, error) { + ctx = s.prepareCtx(ctx, service) + r, err := s.client.newRequest(ctx, http.MethodPost, url, contentTypeJSON, req) + if err != nil { + return nil, err + } + + httpRsp, err := s.client.do(ctx, r, rsp) + if err != nil { + return httpRsp, err + } + return httpRsp, nil +} + +type RequestOptionFunc func(r *http.Request) error + +type Client struct { + baseURL *url.URL + client *http.Client + + common service + Address *AddressService + Phone *PhoneService + Name *NameService + + requestOptions []RequestOptionFunc +} + +func NewClient(httpClient *http.Client, baseURL string, reqOpts ...RequestOptionFunc) (*Client, error) { + if httpClient == nil { + httpClient = &http.Client{ + Transport: http.DefaultTransport, + Timeout: 15 * time.Second, + } + } + if baseURL == "" { + baseURL = BaseURL + } + + pBaseURL, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + c := &Client{client: httpClient, baseURL: pBaseURL, requestOptions: reqOpts} + + c.common.client = c + c.Address = &AddressService{&c.common} + c.Phone = &PhoneService{&c.common} + c.Name = &NameService{&c.common} + + return c, nil +} + +// WithAuthService ApiDQ дает возможность генерировать отдельные токены для всех сервисов. +// Допустимые значения service = ["address","phone","name"] +func (c *Client) WithAuthService(apiKey, service string) *Client { + c.requestOptions = append(c.requestOptions, func(r *http.Request) error { + if strings.Contains(fmt.Sprintf("%v", r.Context().Value(ctxKeyService)), service) { + r.Header.Set(authorization, apiKey) + } + return nil + }) + return c +} + +// WithAuth Auth for all services +func (c *Client) WithAuth(apiKey string) *Client { + c.requestOptions = append(c.requestOptions, func(r *http.Request) error { + r.Header.Set(authorization, apiKey) + return nil + }) + return c +} + +// WithReqOptions Add request options +func (c *Client) WithReqOptions(reqOpts ...RequestOptionFunc) *Client { + c.requestOptions = append(c.requestOptions, reqOpts...) + return c +} + +func (c *Client) newRequest(ctx context.Context, method, url, contentType string, body interface{}) (*http.Request, error) { + u, err := c.baseURL.Parse(url) + if err != nil { + return nil, err + } + + var buf io.ReadWriter + if body != nil { + if method == http.MethodPost { + buf = &bytes.Buffer{} + enc := json.NewEncoder(buf) + errEnc := enc.Encode(body) + if errEnc != nil { + return nil, errEnc + } + } else { + return nil, fmt.Errorf("request Method \"%q\" is unknown", method) + } + } + + req, err := http.NewRequestWithContext(ctx, method, u.String(), buf) + if err != nil { + return nil, err + } + + if body != nil { + req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept", acceptJSON) + } + + for _, opt := range c.requestOptions { + if errOpt := opt(req); errOpt != nil { + return nil, errOpt + } + } + + return req, nil +} + +// Отправка запроса +func (c *Client) do(_ context.Context, req *http.Request, v interface{}) (rsp *http.Response, err error) { + rsp, err = c.client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + if rsp != nil && rsp.Body != nil { + if e := rsp.Body.Close(); e != nil && err == nil { + err = e // if body not close, return err + } + } + }() + + if v != nil && rsp.ContentLength != 0 { + body, e := ioutil.ReadAll(rsp.Body) + if e != nil { + return nil, e + } + + if rsp.StatusCode != 200 { + errRsp := &ErrorResponse{} + decErr := json.Unmarshal(body, errRsp) + if decErr == nil { + return nil, errRsp + } + return nil, decErr + } + + decErr := json.Unmarshal(body, v) + if decErr == nil { + return rsp, nil + } + + return nil, decErr + } + + return rsp, nil +} + +type ErrorResponse struct { + Message string `json:"message"` + // https://grpc.github.io/grpc/core/md_doc_statuscodes.html + // https://developers.google.com/maps-booking/reference/grpc-api/status_codes + Code int `json:"code"` +} + +func (e ErrorResponse) Error() string { + return fmt.Sprintf("[%d] %s", e.Code, e.Message) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..6daf06e --- /dev/null +++ b/client_test.go @@ -0,0 +1,103 @@ +package apidq + +import ( + "context" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/nikitaksv/apidq-client-go/dto/address" + "github.com/stretchr/testify/require" +) + +const TestAPIKey = "testApiKey123" + +func NewTestClient(h http.HandlerFunc) (*Client, *httptest.Server) { + s := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + authKey := r.Header.Get(authorization) + if authKey == "" { + w.WriteHeader(http.StatusUnauthorized) + if _, err := w.Write([]byte(`{"code": 16, "message": "Ключ API обязателен"}`)); err != nil { + panic(err) + } + return + } else if authKey != TestAPIKey { + if _, err := w.Write([]byte(`{"code":16,"message":"Ошибка авторизации"}`)); err != nil { + panic(err) + } + w.WriteHeader(http.StatusUnauthorized) + return + } + h(w, r) + }, + ), + ) + c, err := NewClient(http.DefaultClient, s.URL) + if err != nil { + panic(err) + } + return c.WithAuth(TestAPIKey), s +} + +func TestAuth(t *testing.T) { + client, tS := NewTestClient(func(w http.ResponseWriter, r *http.Request) {}) + defer tS.Close() + + _, _, err := client.WithReqOptions(func(r *http.Request) error { + r.Header.Del(authorization) + return nil + }).Address.Clean(context.Background(), &address.CleanRequest{}) + if err == nil { + t.Fatal(errors.New("need ErrorResponse")) + } + if err.Error() != "[16] Ключ API обязателен" { + t.Fatal(err) + } + + _, _, err = client.WithAuthService(TestAPIKey, "address").Address.Clean(context.Background(), &address.CleanRequest{}) + if err != nil { + t.Fatal(err) + } + + client, tS = NewTestClient(func(w http.ResponseWriter, r *http.Request) {}) + defer tS.Close() + + _, _, err = client.Address.Clean(context.Background(), &address.CleanRequest{}) + if err != nil { + t.Fatal(err) + } +} + +func testEndpointCall(t *testing.T, reqBs, rspBs []byte, endpointCall func(client *Client) (interface{}, *http.Response, error)) { + h := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + bs, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + + require.JSONEq(t, string(bs), string(reqBs)) + + if _, err = w.Write(rspBs); err != nil { + panic(err) + } + } + client, tS := NewTestClient(h) + defer tS.Close() + + cleanRsp, _, err := endpointCall(client) + if err != nil { + t.Fatal(err) + } + bs, err := json.Marshal(cleanRsp) + if err != nil { + t.Fatal(err) + } + + require.JSONEq(t, string(bs), string(rspBs)) +} diff --git a/dto/address/address.go b/dto/address/address.go new file mode 100644 index 0000000..c9c8b13 --- /dev/null +++ b/dto/address/address.go @@ -0,0 +1,142 @@ +package address + +import ( + "fmt" +) + +type Codes struct { + Fias string `json:"fias"` + Ga string `json:"ga"` + Osm string `json:"osm"` +} + +type Part struct { + Codes *Codes `json:"codes"` + FullName string `json:"fullName"` + Name string `json:"name"` + Type string `json:"type"` +} + +type HouseDetails struct { + FullName string `json:"fullName"` + House string `json:"house"` + Case string `json:"case"` + Build string `json:"build"` + Liter string `json:"liter"` + Lend string `json:"lend"` + Block string `json:"block"` + Pav string `json:"pav"` + Floor string `json:"floor"` + Flat string `json:"flat"` + Office string `json:"office"` + Kab string `json:"kab"` + Abon string `json:"abon"` + Plot string `json:"plot"` + Sek string `json:"sek"` + Entr string `json:"entr"` + Room string `json:"room"` + Hostel string `json:"hostel"` + Munit string `json:"munit"` +} + +type Coordinates struct { + // Широта в градусах. Значение должно быть в диапазоне [-90.0, +90.0]. + Latitude float64 `json:"latitude"` + // Долгота в градусах. Значение должно быть в диапазоне[-180.0, +180.0]. + Longitude float64 `json:"longitude"` +} + +func (c Coordinates) String() string { + return fmt.Sprintf("%f,%f", c.Latitude, c.Longitude) +} + +type Country struct { + Name string `json:"name"` + Alpha2 string `json:"alpha2"` + Alpha3 string `json:"alpha3"` + Numeric int `json:"numeric"` +} + +type CleanRequest struct { + Query string `json:"query"` + CountryCode string `json:"countryCode"` +} + +type CleanResponse struct { + Address + Quality *Quality `json:"quality"` + Original string `json:"original"` + PostcodeIn string `json:"postcodeIn"` + Valid bool `json:"valid"` +} + +type SuggestRequest struct { + Query string `json:"query"` + CountryCode string `json:"countryCode"` + Count int `json:"count"` +} + +type SuggestResponse struct { + Suggestions []*Address `json:"suggestions"` +} + +type Address struct { + Region *Part `json:"region"` + Area *Part `json:"area"` + City *Part `json:"city"` + CityArea *Part `json:"cityArea"` + Settlement *Part `json:"settlement"` + PlanStructure *Part `json:"planStructure"` + Street *Part `json:"street"` + HouseDetails *HouseDetails `json:"houseDetails"` + Coordinates *Coordinates `json:"coordinates"` + Country *Country `json:"country"` + Address string `json:"address"` + Postcode string `json:"postcode"` +} + +type CleanIqdqResponse struct { + CJsonKvant *HouseDetails `json:"c_json_kvant"` + CCoordinate *struct { + CLon float64 `json:"c_lon"` + CLat float64 `json:"c_lat"` + CLevel int `json:"c_level"` + } `json:"c_coordinate"` + CIscheck string `json:"c_ischeck"` + CIndexIn string `json:"c_index_in"` + CZipcode string `json:"c_zipcode"` + CAddressOriginal string `json:"c_address_original"` + CAddressFull string `json:"c_address_full"` + CKladr string `json:"c_kladr"` + CGaCode string `json:"c_gaCode"` + CCountry string `json:"c_country"` + CCountryIsoCode string `json:"c_country_iso_code"` + CRegionName string `json:"c_region_name"` + CRegionAbbr string `json:"c_region_abbr"` + CRegionFias string `json:"c_region_fias"` + CDistrictName string `json:"c_district_name"` + CDistrictAbbr string `json:"c_district_abbr"` + CDistrictFias string `json:"c_district_fias"` + CCityName string `json:"c_city_name"` + CCityAbbr string `json:"c_city_abbr"` + CCityFias string `json:"c_city_fias"` + CCommunityName string `json:"c_community_name"` + CCommunityAbbr string `json:"c_community_abbr"` + CCommunityFias string `json:"c_community_fias"` + CStreetName string `json:"c_street_name"` + CStreetAbbr string `json:"c_street_abbr"` + CStreetFias string `json:"c_street_fias"` + CHouseStr string `json:"c_house_str"` + CAddrLost string `json:"c_addr_lost"` + CStatusError string `json:"c_status_error"` + CHouseError string `json:"c_house_error"` + CHouseErrorDesc string `json:"c_house_error_desc"` + CKladr19 string `json:"c_kladr19"` + CGninmb string `json:"c_gninmb"` + COkato string `json:"c_okato"` + COktmo string `json:"c_oktmo"` + CAoguid string `json:"c_aoguid"` + CAolevel string `json:"c_aolevel"` + CHouseguid string `json:"c_houseguid"` + CTimezone string `json:"c_timezone"` +} diff --git a/dto/address/quality.go b/dto/address/quality.go new file mode 100644 index 0000000..f4495bf --- /dev/null +++ b/dto/address/quality.go @@ -0,0 +1,148 @@ +package address + +const ( + // QltUniqUnique Uniq/Уникален: предлагается 1 эталонный адрес + QltUniqUnique QltUniq = iota + // QltUniqDoubtful Doubtful/Сомнителен: предлагается несколько эталонных адресов близких по написанию (возможен выбор) + QltUniqDoubtful + // QltUniqNotUniq Not unique/Неуникален: есть несколько эталонных записей, в равной степени соответствующих исходному адресу + QltUniqNotUniq + + // QltActualityActual Actual/Найдено по актуальной записи: название и административное подчинение, указанные в разбираемом адресе, соответствуют эталонному + QltActualityActual QltActuality = iota + // QltActualityRename Rename/Переименование: устаревшее название одного из адресных элементов, указанных в разбираемом адресе + QltActualityRename + // QltActualityReassignment Reassignment/Переподчинение: административное подчинение, указанное в разбираемом адресе, устарело + QltActualityReassignment + + // QltUndefNo No/Нет + QltUndefNo QltUndef = iota + // QltUndefInsignificant Insignificant/Малозначимый: информация, не влияющая на результаты распознавания при ручной проверке + QltUndefInsignificant + // QltUndefSignificant Significant/Значимый: информация, которая при ручной проверке может повлиять на результат сравнения разбираемого адреса с эталоном + QltUndefSignificant + + // QltLvlRegion To the region(state)/До региона + QltLvlRegion QltLvl = iota + 1 + // QltLvlDistrict To the district/До района + QltLvlDistrict + // QltLvlCity To the city/До города + QltLvlCity + // QltLvlCityArea To the district in the city/До района в городе + QltLvlCityArea + // QltLvlSettlement To the settlement/До населенного пункта + QltLvlSettlement + // QltLvlPlanStruct To the planning structure/До планировочной структуры + QltLvlPlanStruct + // QltLvlStreet To the street/До улицы + QltLvlStreet + // QltLvlHouse To the house/До дома + QltLvlHouse + + // QltHouseNotFound Not found variants/Не найдено вариантов + QltHouseNotFound QltHouse = 0 + // QltHouseExact Exact match of the house by reference/Точное определение дома по эталону + QltHouseExact QltHouse = 3 + // QltHousePartial Partial house match by reference/Частичное определение дома по эталону + QltHousePartial QltHouse = 4 + // QltHouseNonHouse The parsed address is missing a house number/В разбираемом адресе отсутствует номер дома + QltHouseNonHouse QltHouse = 9 + + // QltGeoRegion To the region(state)/До региона + QltGeoRegion QltGeo = iota + 1 + // QltGeoDistrict To the district/До района + QltGeoDistrict + // QltGeoCity To the city/До города + QltGeoCity + // QltGeoCityArea To the district in the city/До района в городе + QltGeoCityArea + // QltGeoSettlement To the settlement/До населенного пункта + QltGeoSettlement + // QltGeoPlanStruct To the planning structure/До планировочной структуры + QltGeoPlanStruct + // QltGeoStreet To the street/До улицы + QltGeoStreet + // QltGeoHouse To the house/До дома + QltGeoHouse +) + +type Quality struct { + // Уровень уникальности найденного адреса + Unique QltUniq `json:"unique"` + // Статус актуальности исходного адреса + Actuality QltActuality `json:"actuality"` + // Разбор неадресной информации в исходном адресе + Undefined QltUndef `json:"undefined"` + // Уровень, до которого произведено сравнение исходного адреса с эталоном + Level QltLvl `json:"level"` + // Степень совпадения номера дома в исходном адресе с эталоном + House QltHouse `json:"house"` + // Уровень, до которого разобраны координаты адреса + Geo QltGeo `json:"geo"` +} + +// QltUniq Уровень уникальности найденного адреса +type QltUniq int + +// Values Все возможные значения +func (q QltUniq) Values() []QltUniq { + return []QltUniq{QltUniqUnique, QltUniqDoubtful, QltUniqNotUniq} +} + +// QltActuality Статус актуальности исходного адреса +type QltActuality int + +// Values Все возможные значения +func (q QltActuality) Values() []QltActuality { + return []QltActuality{QltActualityActual, QltActualityRename, QltActualityReassignment} +} + +// QltUndef Разбор неадресной информации в исходном адресе +type QltUndef int + +// Values Все возможные значения +func (q QltUndef) Values() []QltUndef { + return []QltUndef{QltUndefNo, QltUndefInsignificant, QltUndefSignificant} +} + +// QltLvl Уровень, до которого произведено сравнение исходного адреса с эталоном +type QltLvl int + +// Values Все возможные значения +func (q QltLvl) Values() []QltLvl { + return []QltLvl{ + QltLvlRegion, + QltLvlDistrict, + QltLvlCity, + QltLvlCityArea, + QltLvlSettlement, + QltLvlPlanStruct, + QltLvlStreet, + QltLvlHouse, + } +} + +// QltHouse Степень совпадения номера дома в исходном адресе с эталоном +type QltHouse int + +// Values Все возможные значения +func (q QltHouse) Values() []QltHouse { + return []QltHouse{QltHouseNotFound, QltHouseExact, QltHousePartial, QltHouseNonHouse} +} + +// QltGeo Уровень, до которого разобраны координаты адреса +type QltGeo int + +// Values Все возможные значения +func (q QltGeo) Values() []QltGeo { + return []QltGeo{ + QltGeoRegion, + QltGeoDistrict, + QltGeoCity, + QltGeoCityArea, + QltGeoSettlement, + QltGeoPlanStruct, + QltGeoStreet, + QltGeoHouse, + } +} diff --git a/dto/name/name.go b/dto/name/name.go new file mode 100644 index 0000000..81c34a1 --- /dev/null +++ b/dto/name/name.go @@ -0,0 +1,65 @@ +package name + +// Type Тип/Часть ФИО +type Type string + +func (t Type) Values() []Type { + return []Type{TypeUnknown, TypeSurname, TypeName, TypePatronymic} +} + +// Gender Пол +type Gender string + +func (g Gender) Values() []Gender { + return []Gender{GenderUnknown, GenderMale, GenderFemale} +} + +const ( + // TypeUnknown Неизвестно + TypeUnknown Type = "UNKNOWN" + // TypeSurname Фамилия + TypeSurname Type = "SURNAME" + // TypeName мя + TypeName Type = "NAME" + // TypePatronymic Отчество + TypePatronymic Type = "NAME" + + // GenderUnknown Неизвестно + GenderUnknown = "UNKNOWN" + // GenderMale Мужской пол + GenderMale = "MALE" + // GenderFemale Женский пол + GenderFemale = "FEMALE" +) + +type CleanRequest struct { + Query string `json:"query"` +} + +type CleanResponse struct { + Gender Gender `json:"gender"` + Original string `json:"original"` + Result string `json:"result"` + LastName string `json:"lastName"` + FirstName string `json:"firstName"` + MiddleName string `json:"middleName"` + UnparsedParts []string `json:"unparsedParts"` + Possible bool `json:"possible"` + Valid bool `json:"valid"` +} + +type SuggestRequest struct { + Type Type `json:"type"` + Query string `json:"query"` + Count int `json:"count"` +} + +type SuggestResponse struct { + Suggestions []*Suggest `json:"suggestions"` +} + +type Suggest struct { + Result string `json:"result"` + Lang string `json:"lang"` + Gender string `json:"gender"` +} diff --git a/dto/phone/phone.go b/dto/phone/phone.go new file mode 100644 index 0000000..cdfedfb --- /dev/null +++ b/dto/phone/phone.go @@ -0,0 +1,24 @@ +package phone + +type CleanRequest struct { + Query string `json:"query"` + CountryCode string `json:"countryCode"` +} + +type CleanResponse struct { + Original string `json:"original"` + International string `json:"international"` + National string `json:"national"` + E164 string `json:"E164"` + RFC3966 string `json:"RFC3966"` + Carrier string `json:"carrier"` + Country string `json:"country"` + AreaCode string `json:"areaCode"` + Geocoding string `json:"geocoding"` + SubscriberNumber string `json:"subscriberNumber"` + Type string `json:"type"` + Timezones []string `json:"timezones"` + CountryCode int `json:"countryCode"` + Possible bool `json:"possible"` + Valid bool `json:"valid"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0a52019 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/nikitaksv/apidq-client-go + +go 1.15 + +require github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb88a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/name.go b/name.go new file mode 100644 index 0000000..ac1612f --- /dev/null +++ b/name.go @@ -0,0 +1,32 @@ +package apidq + +import ( + "context" + "net/http" + + "github.com/nikitaksv/apidq-client-go/dto/name" +) + +type NameService struct { + *service +} + +// Clean Стандартизация ФИО +func (s NameService) Clean(ctx context.Context, req *name.CleanRequest) (rsp *name.CleanResponse, httpRsp *http.Response, err error) { + rsp = &name.CleanResponse{} + httpRsp, err = s.post(ctx, ServiceName, "/v1/clean/name", req, rsp) + if err != nil { + return + } + return +} + +// Suggest Подсказки ФИО +func (s NameService) Suggest(ctx context.Context, req *name.SuggestRequest) (rsp *name.SuggestResponse, httpRsp *http.Response, err error) { + rsp = &name.SuggestResponse{} + httpRsp, err = s.post(ctx, ServiceName, "/v1/suggest/name", req, rsp) + if err != nil { + return + } + return +} diff --git a/name_test.go b/name_test.go new file mode 100644 index 0000000..674fc53 --- /dev/null +++ b/name_test.go @@ -0,0 +1,31 @@ +package apidq + +import ( + "context" + "net/http" + "testing" + + "github.com/nikitaksv/apidq-client-go/dto/name" +) + +func TestNameClean(t *testing.T) { + reqBs := []byte(`{"query":"Андрей Ильич Петров"}`) + rspBs := []byte(`{"original":"string","result":"string","lastName":"string","firstName":"string","middleName":"string","gender":"UNKNOWN","unparsedParts":["string"],"possible":true,"valid":true}`) + testEndpointCall(t, reqBs, rspBs, func(client *Client) (interface{}, *http.Response, error) { + return client.Name.Clean(context.Background(), &name.CleanRequest{ + Query: "Андрей Ильич Петров", + }) + }) +} + +func TestNameSuggest(t *testing.T) { + reqBs := []byte(`{"query":"Андре","type":"SURNAME","count":5}`) + rspBs := []byte(`{"suggestions":[{"result":"string","lang":"string","gender":"UNKNOWN"},{"result":"string","lang":"string","gender":"UNKNOWN"},{"result":"string","lang":"string","gender":"UNKNOWN"},{"result":"string","lang":"string","gender":"UNKNOWN"},{"result":"string","lang":"string","gender":"UNKNOWN"}]}`) + testEndpointCall(t, reqBs, rspBs, func(client *Client) (interface{}, *http.Response, error) { + return client.Name.Suggest(context.Background(), &name.SuggestRequest{ + Type: name.TypeSurname, + Count: 5, + Query: "Андре", + }) + }) +} diff --git a/phone.go b/phone.go new file mode 100644 index 0000000..0e7855d --- /dev/null +++ b/phone.go @@ -0,0 +1,22 @@ +package apidq + +import ( + "context" + "net/http" + + "github.com/nikitaksv/apidq-client-go/dto/phone" +) + +type PhoneService struct { + *service +} + +// Clean Стандартизация телефонного номера +func (s PhoneService) Clean(ctx context.Context, req *phone.CleanRequest) (rsp *phone.CleanResponse, httpRsp *http.Response, err error) { + rsp = &phone.CleanResponse{} + httpRsp, err = s.post(ctx, ServicePhone, "/v1/clean/phone", req, rsp) + if err != nil { + return + } + return +} diff --git a/phone_test.go b/phone_test.go new file mode 100644 index 0000000..0c87fcf --- /dev/null +++ b/phone_test.go @@ -0,0 +1,20 @@ +package apidq + +import ( + "context" + "net/http" + "testing" + + "github.com/nikitaksv/apidq-client-go/dto/phone" +) + +func TestPhoneClean(t *testing.T) { + reqBs := []byte(`{"query":"89611122333","countryCode":"RU"}`) + rspBs := []byte(`{"original":"89611122333","international":"+7 961 112-23-33","national":"8 (961) 112-23-33","E164":"+79611122333","RFC3966":"tel:+7-961-112-23-33","carrier":"Beeline","countryCode":7,"country":"RU","areaCode":"","timezones":["Europe/Moscow"],"geocoding":"","subscriberNumber":"9611122333","type":"MOBILE","possible":true,"valid":true}`) + testEndpointCall(t, reqBs, rspBs, func(client *Client) (interface{}, *http.Response, error) { + return client.Phone.Clean(context.Background(), &phone.CleanRequest{ + Query: "89611122333", + CountryCode: "RU", + }) + }) +}