diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a55730 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Created by .ignore support plugin (hsz.mobi) + +/.idea diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8659518 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: go + +go: + - "1.10.x" + - "1.11.x" + - "1.12.x" + - tip + +#before_script: +# # coverage reporter +# - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter +# - chmod +x ./cc-test-reporter +# - ./cc-test-reporter before-build + +script: +# - go test -v -race -coverprofile=coverage.out -covermode=atomic + - go test -v -race + +#after_script: +# - ./cc-test-reporter format-coverage -t gocov coverage.out +# - ./cc-test-reporter upload-coverage diff --git a/README.md b/README.md index 536c7ad..5224922 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ -# dadata -Golang client for DaData.ru +# Client for DaData.ru + +Forked from https://github.com/webdeskltd/dadata. + + +[![Build Status](https://travis-ci.org/ekomobile/dadata.svg)](https://travis-ci.org/ekomobile/dadata) +[![GitHub release](https://img.shields.io/github/release/ekomobile/dadata.svg)](https://github.com/ekomobile/dadata/releases) +[![Go Report Card](https://goreportcard.com/badge/github.com/ekomobile/dadata)](https://goreportcard.com/report/github.com/ekomobile/dadata) +[![GoDoc](https://godoc.org/github.com/ekomobile/dadata?status.svg)](https://godoc.org/github.com/ekomobile/dadata) + +DaData API v2 + +Implemented [Clean](https://dadata.ru/api/clean/) and [Suggest](https://dadata.ru/api/suggest/) methods. + +## Installation + +`go get github.com/ekomobile/dadata` + +## Usage +```go +package main + +import ( + "fmt" + + "github.com/ekomobile/dadata" +) + +func main() { + // By default client gets keys from `DADATA_API_KEY` and `DADATA_SECRET_KEY` environment variables. + daData := dadata.NewClient() + + banks, err := daData.SuggestBanks(dadata.SuggestRequestParams{Query: "Кредитный", Count: 3}) + if nil != err { + fmt.Println(err) + } + + for _, bank := range banks { + fmt.Println(bank.Data.Name.Full) + fmt.Println(bank.Data.Bic) + } + + // Output: + // "МОСКОВСКИЙ КРЕДИТНЫЙ БАНК" (ПУБЛИЧНОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО) + // 044525659 + // КОММЕРЧЕСКИЙ БАНК "РЕСПУБЛИКАНСКИЙ КРЕДИТНЫЙ АЛЬЯНС" (ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ) + // 044525860 + // ЖИЛИЩНО-КРЕДИТНЫЙ КОММЕРЧЕСКИЙ БАНК "ЖИЛКРЕДИТ" ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ + // 044525325 +} +``` + +## Licence +MIT see [LICENSE](LICENSE) diff --git a/balance.go b/balance.go new file mode 100644 index 0000000..481b819 --- /dev/null +++ b/balance.go @@ -0,0 +1,21 @@ +package dadata + +import ( + "context" +) + +// ProfileBalance return daily statistics +// see documentation https://dadata.ru/api/stat/ +func (c *Client) ProfileBalance() (*BalanceResponse, error) { + return c.ProfileBalanceWithCtx(context.Background()) +} + +// ProfileBalanceWithCtx return daily statistics +// see documentation https://dadata.ru/api/stat/ +func (c *Client) ProfileBalanceWithCtx(ctx context.Context) (result *BalanceResponse, err error) { + result = new(BalanceResponse) + if err = c.sendRequestToURL(ctx, "GET", c.options.baseURL+"profile/balance", nil, result); err != nil { + result = nil + } + return +} diff --git a/clean.go b/clean.go new file mode 100644 index 0000000..59f9207 --- /dev/null +++ b/clean.go @@ -0,0 +1,112 @@ +package dadata + +import "context" + +func (c *Client) sendCleanRequest(ctx context.Context, lastURLPart string, source, result interface{}) error { + return c.sendRequest(ctx, "clean/"+lastURLPart, source, result) +} + +// CleanAddresses clean all provided addresses +// Call https://dadata.ru/api/v2/clean/address +func (c *Client) CleanAddresses(sourceAddresses ...string) ([]Address, error) { + return c.CleanAddressesWithCtx(context.Background(), sourceAddresses...) +} + +// CleanAddressesWithCtx clean all provided addresses +// Call https://dadata.ru/api/v2/clean/address +func (c *Client) CleanAddressesWithCtx(ctx context.Context, sourceAddresses ...string) (addresses []Address, err error) { + if err = c.sendCleanRequest(ctx, "address", &sourceAddresses, &addresses); err != nil { + addresses = nil + } + return +} + +// CleanPhones clean all provided phones +// Call https://dadata.ru/api/v2/clean/phone +func (c *Client) CleanPhones(sourcePhones ...string) ([]Phone, error) { + return c.CleanPhonesWithCtx(context.Background(), sourcePhones...) +} + +// CleanPhonesWithCtx clean all provided phones +// Call https://dadata.ru/api/v2/clean/phone +func (c *Client) CleanPhonesWithCtx(ctx context.Context, sourcePhones ...string) (phones []Phone, err error) { + if err = c.sendCleanRequest(ctx, "phone", &sourcePhones, &phones); err != nil { + phones = nil + } + return +} + +// CleanNames clean all provided names +// Call https://dadata.ru/api/v2/clean/name +func (c *Client) CleanNames(sourceNames ...string) ([]Name, error) { + return c.CleanNamesWithCtx(context.Background(), sourceNames...) +} + +// CleanNamesWithCtx clean all provided names +// Call https://dadata.ru/api/v2/clean/name +func (c *Client) CleanNamesWithCtx(ctx context.Context, sourceNames ...string) (names []Name, err error) { + if err = c.sendCleanRequest(ctx, "name", &sourceNames, &names); err != nil { + names = nil + } + return +} + +// CleanEmails clean all provided emails +// Call https://dadata.ru/api/v2/clean/email +func (c *Client) CleanEmails(sourceEmails ...string) ([]Email, error) { + return c.CleanEmailsWithCtx(context.Background(), sourceEmails...) +} + +// CleanEmailsWithCtx clean all provided emails +// Call https://dadata.ru/api/v2/clean/email +func (c *Client) CleanEmailsWithCtx(ctx context.Context, sourceEmails ...string) (emails []Email, err error) { + if err = c.sendCleanRequest(ctx, "email", &sourceEmails, &emails); err != nil { + emails = nil + } + return +} + +// CleanBirthdates clean all provided birthdates +// Call https://dadata.ru/api/v2/clean/birthdate +func (c *Client) CleanBirthdates(sourceBirthdates ...string) ([]Birthdate, error) { + return c.CleanBirthdatesWithCtx(context.Background(), sourceBirthdates...) +} + +// CleanBirthdatesWithCtx clean all provided birthdates +// Call https://dadata.ru/api/v2/clean/birthdate +func (c *Client) CleanBirthdatesWithCtx(ctx context.Context, sourceBirthdates ...string) (birthdates []Birthdate, err error) { + if err = c.sendCleanRequest(ctx, "birthdate", &sourceBirthdates, &birthdates); err != nil { + birthdates = nil + } + return +} + +// CleanVehicles clean all provided vehicles +// Call https://dadata.ru/api/v2/clean/vehicle +func (c *Client) CleanVehicles(sourceVehicles ...string) ([]Vehicle, error) { + return c.CleanVehiclesWithCtx(context.Background(), sourceVehicles...) +} + +// CleanVehiclesWithCtx clean all provided vehicles +// Call https://dadata.ru/api/v2/clean/vehicle +func (c *Client) CleanVehiclesWithCtx(ctx context.Context, sourceVehicles ...string) (vehicles []Vehicle, err error) { + if err = c.sendCleanRequest(ctx, "vehicle", &sourceVehicles, &vehicles); err != nil { + vehicles = nil + } + return +} + +// CleanPassports clean all provided passports +// Call https://dadata.ru/api/v2/clean/passport +func (c *Client) CleanPassports(sourcePassports ...string) ([]Passport, error) { + return c.CleanPassportsWithCtx(context.Background(), sourcePassports...) +} + +// CleanPassportsWithCtx clean all provided passports +// Call https://dadata.ru/api/v2/clean/passport +func (c *Client) CleanPassportsWithCtx(ctx context.Context, sourcePassports ...string) (passports []Passport, err error) { + if err = c.sendCleanRequest(ctx, "passport", &sourcePassports, &passports); err != nil { + passports = nil + } + return +} diff --git a/clean_test.go b/clean_test.go new file mode 100644 index 0000000..359dd55 --- /dev/null +++ b/clean_test.go @@ -0,0 +1,38 @@ +package dadata + +import ( + "fmt" +) + +func ExampleClient_CleanAddresses() { + // By default client uses `DADATA_API_KEY` and `DADATA_SECRET_KEY` environment variables. + daData := NewClient() + // Or credentials may be passed as client option. + // daData := NewClient(WithCredentialProvider("API_KEY", "SECRET_KEY")) + + addresses, err := daData.CleanAddresses("ул.Правды 26", "пер.Расковой 5") + + if nil != err { + fmt.Println(err) + } + + for _, address := range addresses { + fmt.Println(address.StreetTypeFull) + fmt.Println(address.Street) + fmt.Println(address.House) + } +} + +func ExampleClient_CleanNames() { + daData := NewClient() + + names, err := daData.CleanNames("Алексей Иванов", "Иван Алексеев") + if nil != err { + fmt.Println(err) + } + + for _, name := range names { + fmt.Println(name.Surname) + fmt.Println(name.Name) + } +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..1a9b7dc --- /dev/null +++ b/client.go @@ -0,0 +1,164 @@ +// Golang client library for DaData.ru (https://dadata.ru/). + +// Package dadata implemented cleaning (https://dadata.ru/api/clean/) and suggesting (https://dadata.ru/api/suggest/) +package dadata + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" +) + +const ( + defaultBaseURL = "https://dadata.ru/api/v2/" + defaultBaseSuggestURL = "https://suggestions.dadata.ru/suggestions/api/4_1/rs/" +) + +type ( + Client struct { + options clientOptions + } + + CredentialProvider interface { + ApiKey() string + SecretKey() string + } + + Credentials struct { + ApiKeyValue string + SecretKeyValue string + } + + EnvironmentCredentials struct { + ApiKeyName string + SecretKeyName string + } + + clientOptions struct { + httpClient *http.Client + credentialProvider CredentialProvider + baseURL string + baseSuggestURL string + } + + ClientOption func(opts *clientOptions) +) + +// NewClient Create new client of DaData. +// Api and secret keys see on profile page (https://dadata.ru/profile/). +// By default client uses `DADATA_API_KEY` and `DADATA_SECRET_KEY` environment variables. +func NewClient(opts ...ClientOption) *Client { + options := clientOptions{ + httpClient: http.DefaultClient, + credentialProvider: &EnvironmentCredentials{ + ApiKeyName: "DADATA_API_KEY", + SecretKeyName: "DADATA_SECRET_KEY", + }, + baseURL: defaultBaseURL, + baseSuggestURL: defaultBaseSuggestURL, + } + + applyOptions(&options, opts...) + + return &Client{ + options: options, + } +} + +func (c *Client) sendRequestToURL(ctx context.Context, method, url string, source interface{}, result interface{}) error { + if err := ctx.Err(); err != nil { + return fmt.Errorf("sendRequestToURL: ctx.Err return err=%v", err) + } + + buffer := &bytes.Buffer{} + + if err := json.NewEncoder(buffer).Encode(source); err != nil { + return fmt.Errorf("sendRequestToURL: json.Encode return err = %v", err) + } + + request, err := http.NewRequest(method, url, buffer) + + if err != nil { + return fmt.Errorf("sendRequestToURL: http.NewRequest return err = %v", err) + } + + request = request.WithContext(ctx) + + request.Header.Add("Authorization", fmt.Sprintf("Token %s", c.options.credentialProvider.ApiKey())) + request.Header.Add("X-Secret", c.options.credentialProvider.SecretKey()) + request.Header.Add("Content-Type", "application/json") + request.Header.Add("Accept", "application/json") + + response, err := c.options.httpClient.Do(request) + if err != nil { + return fmt.Errorf("sendRequestToURL: httpClient.Do return err = %v", err) + } + + defer response.Body.Close() + + if http.StatusOK != response.StatusCode { + return fmt.Errorf("sendRequestToURL: Request error %v", response.Status) + } + + if err = json.NewDecoder(response.Body).Decode(&result); err != nil { + return fmt.Errorf("sendRequestToURL: json.Decode return err = %v", err) + } + + return nil +} + +// sendRequest +func (c *Client) sendRequest(ctx context.Context, lastURLPart string, source interface{}, result interface{}) error { + return c.sendRequestToURL(ctx, "POST", c.options.baseURL+lastURLPart, source, result) +} + +func WithHttpClient(c *http.Client) ClientOption { + return func(opts *clientOptions) { + opts.httpClient = c + } +} + +// WithCredentialProvider sets credential provider. +func WithCredentialProvider(c CredentialProvider) ClientOption { + return func(opts *clientOptions) { + opts.credentialProvider = c + } +} + +func WithBaseURL(url string) ClientOption { + return func(opts *clientOptions) { + opts.baseURL = url + } +} + +func WithBaseSuggestURL(url string) ClientOption { + return func(opts *clientOptions) { + opts.baseSuggestURL = url + } +} + +func applyOptions(options *clientOptions, opts ...ClientOption) { + options = &clientOptions{} + for _, o := range opts { + o(options) + } +} + +func (c *Credentials) ApiKey() string { + return c.ApiKeyValue +} + +func (c *Credentials) SecretKey() string { + return c.SecretKeyValue +} + +func (c *EnvironmentCredentials) ApiKey() string { + return os.Getenv(c.ApiKeyName) +} + +func (c *EnvironmentCredentials) SecretKey() string { + return os.Getenv(c.SecretKeyName) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..6c6a0ba --- /dev/null +++ b/client_test.go @@ -0,0 +1,13 @@ +package dadata + +import ( + "testing" +) + +func TestNewDaData(t *testing.T) { + daData := NewClient() + + if daData == nil { + t.Errorf(`NewClient return nil`) + } +} diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..e5599ac --- /dev/null +++ b/constants.go @@ -0,0 +1,59 @@ +package dadata + +// Определите, нужна ли дополнительная проверка оператором, используя код качества (qc): +const ( + QcSuccess = 0 // Исходное значение распознано уверенно. Не требуется ручная проверка. + QcFailure = 1 // Исходное значение распознано с допущениями или не распознано. Требуется ручная проверка. +) + +// Определите пригодность к рассылке, используя код полноты адреса (qc_complete): +const ( + QcCompleteSuitable = 0 // Пригоден для почтовой рассылки + QcCompleteNoRegion = 1 // Не пригоден, нет региона + QcCompleteNoCity = 2 // Не пригоден, нет города + QcCompleteNoStreet = 3 // Не пригоден, нет улицы + QcCompleteNotHome = 4 // Не пригоден, нет дома + QcCompleteNoApartment = 5 // Пригоден для юридических лиц или частных владений (нет квартиры) + QcCompleteNotSuitable = 6 // Не пригоден + QcCompleteCompleteForeignAddress = 7 // Иностранный адрес + QcCompleteCompleteNoKLADR = 10 // Пригоден, но низкая вероятность успешной доставки (дом не найден в КЛАДР) +) + +// Определите вероятность успешной доставки письма по адресу, используя код проверки дома (qc_house): +const ( + QcHouseExactMatch = 2 // Дом найден по точному совпадению (КЛАДР) Высокая + QcHouseNotExpansionMatch = 3 // Различие в расширении дома (КЛАДР) Средняя + QcHouseRangeMatch = 4 // Дом найден по диапазону (КЛАДР) Средняя + QcHouseNotFound = 10 // Дом не найден (КЛАДР) Низкая +) + +// Определите точность координат адреса доставки с помощью кода qc_geo: +const ( + QcGeoExactCoordinates = 0 // Точные координаты + QcGeoNearestHouse = 1 // Ближайший дом + QcGeoStreet = 2 // Улица + QcGeoLocality = 3 // Населенный пункт + QcGeoCity = 4 // Город + QcGeoNotDetermined = 5 // Координаты не определены +) + +// Проверьте, указал ли клиент телефон, соответствующий его адресу, с помощью кода qc_conflict (удобно для проверки уровня риска): +const ( + QcConflictFullMath = 0 // Телефон соответствует адресу + QcConflictCityMath = 2 // Города адреса и телефона отличаются + QcConflictRegionMath = 3 // Регионы адреса и телефона отличаются +) + +// BoundValue type wrapper for suggest bounds +// full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=222888017 +type BoundValue string + +// const for SuggestBound +const ( + SuggestBoundRegion BoundValue = "region" // Регион + SuggestBoundArea BoundValue = "area" // Район + SuggestBoundCity BoundValue = "city" // Город + SuggestBoundSettlement BoundValue = "settlement" // Населенный пункт + SuggestBoundStreet BoundValue = "street" // Улица + SuggestBoundHouse BoundValue = "house" // Дом +) diff --git a/find_by_id.go b/find_by_id.go new file mode 100644 index 0000000..73009c5 --- /dev/null +++ b/find_by_id.go @@ -0,0 +1,84 @@ +package dadata + +import ( + "context" + "fmt" +) + +// AddressByID find address by Fias or Kladr +// see full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=312016944 +func (c *Client) AddressByID(id string) (*ResponseAddress, error) { + return c.AddressByIDWithCtx(context.Background(), id) +} + +// AddressByIDWithCtx find address by Fias or Kladr +// see full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=312016944 +func (c *Client) AddressByIDWithCtx(ctx context.Context, id string) (address *ResponseAddress, err error) { + var result []ResponseAddress + if result, err = c.AddressesByIDWithCtx(ctx, id); err != nil { + return + } + address = &result[0] + return +} + +// AddressesByID find addresses by Fias or Kladr +// see full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=312016944 +func (c *Client) AddressesByID(id string) ([]ResponseAddress, error) { + return c.AddressesByIDWithCtx(context.Background(), id) +} + +// AddressesByIDWithCtx find addresses by Fias or Kladr +// see full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=312016944 +func (c *Client) AddressesByIDWithCtx(ctx context.Context, id string) (addresses []ResponseAddress, err error) { + var result = &SuggestAddressResponse{} + var req = SuggestRequestParams{Query: id} + + if err = c.sendRequestToURL(ctx, "POST", c.options.baseSuggestURL+"findById/address", req, result); err != nil { + return + } + if len(result.Suggestions) == 0 { + err = fmt.Errorf("dadata.AddressByID: cannot detect address by id %s", id) + return + } + addresses = result.Suggestions + + return +} + +// CountryByID find country by ID +func (c *Client) CountryByID(id string) (*ResponseCountry, error) { + return c.CountryByIDWithCtx(context.Background(), id) +} + +// CountryByIDWithCtx find country by ID +func (c *Client) CountryByIDWithCtx(ctx context.Context, id string) (country *ResponseCountry, err error) { + var result []ResponseCountry + if result, err = c.CountriesByIDWithCtx(ctx, id); err != nil { + return + } + country = &result[0] + return +} + +// CountriesByID find countries by ID +func (c *Client) CountriesByID(id string) ([]ResponseCountry, error) { + return c.CountriesByIDWithCtx(context.Background(), id) +} + +// CountriesByIDWithCtx find countries by ID +func (c *Client) CountriesByIDWithCtx(ctx context.Context, id string) (addresses []ResponseCountry, err error) { + var result = &SuggestCountryResponse{} + var req = SuggestRequestParams{Query: id} + + if err = c.sendRequestToURL(ctx, "POST", c.options.baseSuggestURL+"findById/country", req, result); err != nil { + return + } + if len(result.Suggestions) == 0 { + err = fmt.Errorf("dadata.CountryByID: cannot detect country by id %s", id) + return + } + addresses = result.Suggestions + + return +} diff --git a/find_by_id_test.go b/find_by_id_test.go new file mode 100644 index 0000000..f886dec --- /dev/null +++ b/find_by_id_test.go @@ -0,0 +1,16 @@ +package dadata + +import ( + "fmt" +) + +func ExampleClient_AddressByID() { + daData := NewClient() + addr, err := daData.AddressByID("6300000100000") + if err != nil { + fmt.Println(err) + return + } + + fmt.Printf("City: %s,\nFiasID: %s,\nKladr: %s\n", addr.Data.City, addr.Data.FiasID, addr.Data.KladrID) +} diff --git a/geoip.go b/geoip.go new file mode 100644 index 0000000..5229ec8 --- /dev/null +++ b/geoip.go @@ -0,0 +1,32 @@ +package dadata + +import ( + "context" + "fmt" +) + +// GeoIP try to find address by IP +// see documentation on: +// https://dadata.ru/api/detect_address_by_ip/ +// https://confluence.hflabs.ru/pages/viewpage.action?pageId=715096277 +// ip string representation of ip-address (example 10.12.44.23) +// if ip=="" then dadata try to get ip-address from X-Forwarded-For header +func (c *Client) GeoIP(ip string) (*GeoIPResponse, error) { + return c.GeoIPWithCtx(context.Background(), ip) +} + +// GeoIPWithCtx try to find address by IP +// see documentation on: +// https://dadata.ru/api/detect_address_by_ip/ +// https://confluence.hflabs.ru/pages/viewpage.action?pageId=715096277 +// ip string representation of ip-address (example 10.12.44.23) +// if ip=="" then dadata try to get ip-address from X-Forwarded-For header +func (c *Client) GeoIPWithCtx(ctx context.Context, ip string) (result *GeoIPResponse, err error) { + result = &GeoIPResponse{} + if err = c.sendRequestToURL(ctx, "GET", c.options.baseSuggestURL+"detectAddressByIp?ip="+ip, nil, &result); err != nil { + result = nil + } else if result.Location == nil { + result, err = nil, fmt.Errorf("dadata.GeoIP: cannot detect address by ip %s", ip) + } + return +} diff --git a/geoip_test.go b/geoip_test.go new file mode 100644 index 0000000..e1dc732 --- /dev/null +++ b/geoip_test.go @@ -0,0 +1,23 @@ +package dadata + +import ( + "fmt" +) + +func ExampleDaData_GeoIP() { + daData := NewClient() + + geoIPResponse, err := daData.GeoIP("83.220.54.223") + if nil != err { + fmt.Println(err) + return + } + if geoIPResponse.Location == nil { + fmt.Println("empty result from GeoIP") + return + } + address := geoIPResponse.Location.Data + fmt.Println(address.Country) + fmt.Println(address.City) + fmt.Printf("see on https://www.google.com/maps/@%s,%sf,14z\n", address.GeoLat, address.GeoLon) +} diff --git a/geoip_types.go b/geoip_types.go new file mode 100644 index 0000000..60f5b56 --- /dev/null +++ b/geoip_types.go @@ -0,0 +1,6 @@ +package dadata + +// GeoIPResponse response for GeoIP +type GeoIPResponse struct { + Location *ResponseAddress `json:"location"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..abcc03f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ekomobile/dadata + +go 1.12 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/model.go b/model.go new file mode 100644 index 0000000..0c7259f --- /dev/null +++ b/model.go @@ -0,0 +1,308 @@ +package dadata + +// Address base struct for datdata.Address +type Address struct { + Source string `json:"source"` // Исходный адрес одной строкой + Result string `json:"result"` // Стандартизованный адрес одной строкой + PostalCode string `json:"postal_code"` // Индекс + Country string `json:"country"` // Страна + RegionFiasID string `json:"region_fias_id"` // Код ФИАС региона + RegionKladrID string `json:"region_kladr_id"` // Код КЛАДР региона + RegionWithType string `json:"region_with_type"` // Регион с типом + RegionType string `json:"region_type"` // Тип региона (сокращенный) + RegionTypeFull string `json:"region_type_full"` // Тип региона + Region string `json:"region"` // Регион + AreaFiasID string `json:"area_fias_id"` // Код ФИАС района в регионе + AreaKladrID string `json:"area_kladr_id"` // Код КЛАДР района в регионе + AreaWithType string `json:"area_with_type"` // Район в регионе с типом + AreaType string `json:"area_type"` // Тип района в регионе (сокращенный) + AreaTypeFull string `json:"area_type_full"` // Тип района в регионе + Area string `json:"area"` // Район в регионе + CityFiasID string `json:"city_fias_id"` // Код ФИАС города + CityKladrID string `json:"city_kladr_id"` // Код КЛАДР города + CityWithType string `json:"city_with_type"` // Город с типом + CityType string `json:"city_type"` // Тип города (сокращенный) + CityTypeFull string `json:"city_type_full"` // Тип города + City string `json:"city"` // Город + CityArea string `json:"city_area"` // Административный округ (только для Москвы) + CityDistrictFiasID string `json:"city_district_fias_id"` // Код ФИАС района города (заполняется, только если район есть в ФИАС) + CityDistrictKladrID string `json:"city_district_kladr_id"` // Код КЛАДР района города (не заполняется) + CityDistrictWithType string `json:"city_district_with_type"` // Район города с типом + CityDistrictType string `json:"city_district_type"` // Тип района города (сокращенный) + CityDistrictTypeFull string `json:"city_district_type_full"` // Тип района города + CityDistrict string `json:"city_district"` // Район города + SettlementFiasID string `json:"settlement_fias_id"` // Код ФИАС нас. пункта + SettlementKladrID string `json:"settlement_kladr_id"` // Код КЛАДР нас. пункта + SettlementWithType string `json:"settlement_with_type"` // Населенный пункт с типом + SettlementType string `json:"settlement_type"` // Тип населенного пункта (сокращенный) + SettlementTypeFull string `json:"settlement_type_full"` // Тип населенного пункта + Settlement string `json:"settlement"` // Населенный пункт + StreetFiasID string `json:"street_fias_id"` // Код ФИАС улицы + StreetKladrID string `json:"street_kladr_id"` // Код КЛАДР улицы + StreetWithType string `json:"street_with_type"` // Улица с типом + StreetType string `json:"street_type"` // Тип улицы (сокращенный) + StreetTypeFull string `json:"street_type_full"` // Тип улицы + Street string `json:"street"` // Улица + HouseFiasID string `json:"house_fias_id"` // Код ФИАС дома + HouseKladrID string `json:"house_kladr_id"` // Код КЛАДР дома + HouseType string `json:"house_type"` // Тип дома (сокращенный) + HouseTypeFull string `json:"house_type_full"` // Тип дома + House string `json:"house"` // Дом + BlockType string `json:"block_type"` // Тип корпуса/строения (сокращенный) + BlockTypeFull string `json:"block_type_full"` // Тип корпуса/строения + Block string `json:"block"` // Корпус/строение + FlatType string `json:"flat_type"` // Тип квартиры (сокращенный) + FlatTypeFull string `json:"flat_type_full"` // Тип квартиры + Flat string `json:"flat"` // Квартира + FlatArea string `json:"flat_area"` // Площадь квартиры + SquareMeterPrice string `json:"square_meter_price"` // Рыночная стоимость м² + FlatPrice string `json:"flat_price"` // Рыночная стоимость квартиры + PostalBox string `json:"postal_box"` // Абонентский ящик + FiasID string `json:"fias_id"` // Код ФИАС + FiasLevel string `json:"fias_level"` // Уровень детализации, до которого адрес найден в ФИАС + KladrID string `json:"kladr_id"` // Код КЛАДР + CapitalMarker string `json:"capital_marker"` // Статус центра + Okato string `json:"okato"` // Код ОКАТО + Oktmo string `json:"oktmo"` // Код ОКТМО + TaxOffice string `json:"tax_office"` // Код ИФНС для физических лиц + Timezone string `json:"timezone"` // Часовой пояс + GeoLat string `json:"geo_lat"` // Координаты: широта + GeoLon string `json:"geo_lon"` // Координаты: долгота + BeltwayHit string `json:"beltway_hit"` // Внутри кольцевой? + BeltwayDistance string `json:"beltway_distance"` // Расстояние от кольцевой в км. + + // QualityCodeGeo для clean вызовов он int для suggest в адресе банков он string поэтому в поле поставил interface{} чтобы работало и там и там)\ + QualityCodeGeo interface{} `json:"qc_geo,string"` // Код точности координат + QualityCodeComplete interface{} `json:"qc_complete,string"` // Код полноты + QualityCodeHouse interface{} `json:"qc_house,string"` // Код проверки дома + QualityCode interface{} `json:"qc,string"` // Код качества + UnparsedParts string `json:"unparsed_parts"` // Нераспознанная часть адреса. Для адреса + Metro []Metro `json:"metro"` +} + +// ResponseAddress api response for address +type ResponseAddress struct { + Value string `json:"value"` + UnrestrictedValue string `json:"unrestricted_value"` + Data Address `json:"data"` +} + +// Metro base struct for dadata.Metro +type Metro struct { + Name string `json:"name"` + Line string `json:"line"` + Distance float64 `json:"distance"` +} + +// Phone base struct for dadata.Phone +type Phone struct { + Source string `json:"source"` // Исходный телефон одной строкой + Type string `json:"type"` // Тип телефона + Phone string `json:"phone"` // Стандартизованный телефон одной строкой + CountryCode string `json:"country_code"` // Код страны + CityCode string `json:"city_code"` // Код города / DEF-код + Number string `json:"number"` // Локальный номер телефона + Extension string `json:"extension"` // Добавочный номер + Provider string `json:"provider"` // Оператор связи + Region string `json:"region"` // Регион + Timezone string `json:"timezone"` // Часовой пояс + QualityCodeConflict int `json:"qc_conflict"` // Признак конфликта телефона с адресом + QualityCode int `json:"qc"` // Код качества +} + +// Name base struct for dadata.Name +type Name struct { + Source string `json:"source"` // Исходное ФИО одной строкой + Result string `json:"result"` // Стандартизованное ФИО одной строкой + ResultGenitive string `json:"result_genitive"` // ФИО в родительном падеже (кого?) + ResultDative string `json:"result_dative"` // ФИО в дательном падеже (кому?) + ResultAblative string `json:"result_ablative"` // ФИО в творительном падеже (кем?) + Surname string `json:"surname"` // Фамилия + Name string `json:"name"` // Имя + Patronymic string `json:"patronymic"` // Отчество + Gender string `json:"gender"` // Пол + QualityCode interface{} `json:"qc"` // Код качества +} + +// ResponseName api response for name +type ResponseName struct { + Value string `json:"value"` + UnrestrictedValue string `json:"unrestricted_value"` + Data Name `json:"data"` +} + +// Email base struct for dadata.Email +type Email struct { + Source string `json:"source"` // Исходный e-mail + Email string `json:"email"` // Стандартизованный e-mail + QualityCode int `json:"qc"` // Код качества +} + +// ResponseEmail api response for email +type ResponseEmail struct { + Value string `json:"value"` + UnrestrictedValue string `json:"unrestricted_value"` + Data Email `json:"data"` +} + +// Birthdate base struct for dadata.Birthdate +type Birthdate struct { + Source string `json:"source"` // Исходная дата + Birthdate string `json:"birthdate"` // Стандартизованная дата + QualityCode int `json:"qc"` // Код качества +} + +// Vehicle base struct for dadata.Vehicle +type Vehicle struct { + Source string `json:"source"` // Исходное значение + Result string `json:"result"` // Стандартизованное значение + Brand string `json:"brand"` // Марка + Model string `json:"model"` // Модель + QualityCode int `json:"qc"` // Код проверки +} + +// Passport base struct for dadata.Passport +type Passport struct { + Source string `json:"source"` // Исходная серия и номер одной строкой + Series string `json:"series"` // Серия + Number string `json:"number"` // Номер + QualityCode int `json:"qc"` // Код проверки +} + +// Bank base struct for dadata.Bank +type Bank struct { + Opf OrganizationOPF `json:"opf"` + Name BankName `json:"name"` + Bic string `json:"bic"` // Банковский идентификационный код (БИК) ЦБ РФ + Swift string `json:"swift"` // Банковский идентификационный код в системе SWIFT + Okpo string `json:"okpo"` // Код ОКПО + CorrespondentAccount string `json:"correspondent_account"` // Корреспондентский счет в ЦБ РФ + RegistrationNumber string `json:"registration_number"` // Регистрационный номер в ЦБ РФ + // Go cannot use recursive types - thats why we need copy paste all fields to Rks + Rkc struct { + Opf OrganizationOPF `json:"opf"` + Name BankName `json:"name"` + Bic string `json:"bic"` + Swift string `json:"swift"` + Okpo string `json:"okpo"` + CorrespondentAccount string `json:"correspondent_account"` + RegistrationNumber string `json:"registration_number"` + Rkc interface{} `json:"rkc"` + Address ResponseAddress `json:"address"` + Phone string `json:"phone"` + State OrganizationState `json:"state"` + } `json:"rkc"` // Расчетно-кассовый центр. Объект такой же структуры, как сам банк. + Address ResponseAddress `json:"address"` // см ResponseAddress + Phone string `json:"phone"` // Не заполняется + State OrganizationState `json:"state"` +} + +// ResponseBank api response for bank +type ResponseBank struct { + Value string `json:"value"` + UnrestrictedValue string `json:"unrestricted_value"` + Data Bank `json:"data"` +} + +// OrganizationOPF Тип Кредитной организации +type OrganizationOPF struct { + Type string `json:"type"` // Тип кредитной организации + Full string `json:"full"` // Тип кредитной организации (на русском) + Short string `json:"short"` // Тип кредитной организации (на русском, сокращенный) +} + +// BankName наименование банка +type BankName struct { + Payment string `json:"payment"` // Платежное наименование + Full string `json:"full"` // Полное наименование + Short string `json:"short"` // Краткое наименование +} + +// OrganizationState Статус организации +type OrganizationState struct { + Status string `json:"status"` // Статус организации: + // ACTIVE — действующая + // LIQUIDATING — ликвидируется + // LIQUIDATED — ликвидирована + ActualityDate int64 `json:"actuality_date"` // Дата актуальности сведений + RegistrationDate int64 `json:"registration_date"` // Дата регистрации + LiquidationDate string `json:"liquidation_date"` // Дата ликвидации +} + +// Party base struct for dadata.Party (rus Организация) +type Party struct { + Kpp string `json:"kpp"` + Capital string `json:"capital"` + Management struct { + Name string `json:"name"` + Post string `json:"post"` + } `json:"management"` + Founders string `json:"founders"` + Managers string `json:"managers"` + BranchType string `json:"branch_type"` + BranchCount int `json:"branch_count"` + Source string `json:"source"` + Qc string `json:"qc"` + Hid string `json:"hid"` + Type string `json:"type"` + State OrganizationState `json:"state"` + Opf OrganizationOPF `json:"opf"` + Name struct { + FullWithOpf string `json:"full_with_opf"` + ShortWithOpf string `json:"short_with_opf"` + Latin string `json:"latin"` + Full string `json:"full"` + Short string `json:"short"` + } `json:"name"` + Inn string `json:"inn"` + Ogrn string `json:"ogrn"` + Okpo string `json:"okpo"` + Okved string `json:"okved"` + Okveds string `json:"okveds"` + Authorities string `json:"authorities"` + Documents string `json:"documents"` + Licenses string `json:"licenses"` + Address ResponseAddress `json:"address"` + Phones string `json:"phones"` + Emails string `json:"emails"` + OgrnDate int64 `json:"ogrn_date"` + OkvedType string `json:"okved_type"` +} + +// ResponseParty api response for party +type ResponseParty struct { + Value string `json:"value"` + UnrestrictedValue string `json:"unrestricted_value"` + Data Party `json:"data"` +} + +// StatResponse usage statitics +// see docs https://dadata.ru/api/stat/ +type StatResponse struct { + Date string `json:"date"` + Services struct { + Merging int `json:"merging"` + Suggestions int `json:"suggestions"` + Clean int `json:"clean"` + } `json:"services"` +} + +// BalanceResponse Response of API request +type BalanceResponse struct { + Balance float64 `json:"balance"` +} + +// Country base struct for dadata.Country +type Country struct { + Code string `json:"code"` + Alfa2 string `json:"alfa2"` + Alfa3 string `json:"alfa3"` + NameShort string `json:"name_short"` + Name string `json:"name"` +} + +// ResponseCountry api response for country +type ResponseCountry struct { + Value string `json:"value"` + Data Country `json:"data"` +} diff --git a/stat.go b/stat.go new file mode 100644 index 0000000..0512ecd --- /dev/null +++ b/stat.go @@ -0,0 +1,25 @@ +package dadata + +import ( + "context" + "time" +) + +// DailyStat return daily statistics +// see documentation https://dadata.ru/api/stat/ +func (c *Client) DailyStat(date time.Time) (*StatResponse, error) { + return c.DailyStatWithCtx(context.Background(), date) +} + +// DailyStatWithCtx return daily statistics +// see documentation https://dadata.ru/api/stat/ +func (c *Client) DailyStatWithCtx(ctx context.Context, date time.Time) (result *StatResponse, err error) { + var dateStr string + + result, dateStr = &StatResponse{}, date.Format("2006-01-02") + if err = c.sendRequestToURL(ctx, "GET", c.options.baseURL+"stat/daily?date="+dateStr, nil, result); err != nil { + result = nil + } + + return +} diff --git a/suggest.go b/suggest.go new file mode 100644 index 0000000..4006049 --- /dev/null +++ b/suggest.go @@ -0,0 +1,97 @@ +package dadata + +import "context" + +func (c *Client) sendSuggestRequest(ctx context.Context, lastURLPart string, requestParams SuggestRequestParams, result interface{}) error { + return c.sendRequest(ctx, "suggest/"+lastURLPart, requestParams, result) +} + +// SuggestAddresses try to return suggest addresses by requestParams +func (c *Client) SuggestAddresses(requestParams SuggestRequestParams) ([]ResponseAddress, error) { + return c.SuggestAddressesWithCtx(context.Background(), requestParams) +} + +// SuggestAddressesWithCtx try to return suggest addresses by requestParams +func (c *Client) SuggestAddressesWithCtx(ctx context.Context, requestParams SuggestRequestParams) (ret []ResponseAddress, err error) { + var result = &SuggestAddressResponse{} + if err = c.sendSuggestRequest(ctx, "address", requestParams, result); err != nil { + return + } + ret = result.Suggestions + return +} + +// SuggestNames try to return suggest names by requestParams +func (c *Client) SuggestNames(requestParams SuggestRequestParams) ([]ResponseName, error) { + return c.SuggestNamesWithCtx(context.Background(), requestParams) +} + +// SuggestNamesWithCtx try to return suggest names by requestParams +func (c *Client) SuggestNamesWithCtx(ctx context.Context, requestParams SuggestRequestParams) (ret []ResponseName, err error) { + var result = &SuggestNameResponse{} + if err = c.sendSuggestRequest(ctx, "fio", requestParams, result); err != nil { + return + } + ret = result.Suggestions + return +} + +// SuggestBanks try to return suggest banks by requestParams +func (c *Client) SuggestBanks(requestParams SuggestRequestParams) ([]ResponseBank, error) { + return c.SuggestBanksWithCtx(context.Background(), requestParams) +} + +// SuggestBanksWithCtx try to return suggest banks by requestParams +func (c *Client) SuggestBanksWithCtx(ctx context.Context, requestParams SuggestRequestParams) (ret []ResponseBank, err error) { + var result = &SuggestBankResponse{} + if err = c.sendSuggestRequest(ctx, "bank", requestParams, result); err != nil { + return + } + ret = result.Suggestions + return +} + +// SuggestParties try to return suggest parties by requestParams +func (c *Client) SuggestParties(requestParams SuggestRequestParams) ([]ResponseParty, error) { + return c.SuggestPartiesWithCtx(context.Background(), requestParams) +} + +// SuggestPartiesWithCtx try to return suggest parties by requestParams +func (c *Client) SuggestPartiesWithCtx(ctx context.Context, requestParams SuggestRequestParams) (ret []ResponseParty, err error) { + var result = &SuggestPartyResponse{} + if err = c.sendSuggestRequest(ctx, "party", requestParams, result); err != nil { + return + } + ret = result.Suggestions + return +} + +// SuggestEmails try to return suggest emails by requestParams +func (c *Client) SuggestEmails(requestParams SuggestRequestParams) ([]ResponseEmail, error) { + return c.SuggestEmailsWithCtx(context.Background(), requestParams) +} + +// SuggestEmailsWithCtx try to return suggest emails by requestParams +func (c *Client) SuggestEmailsWithCtx(ctx context.Context, requestParams SuggestRequestParams) (ret []ResponseEmail, err error) { + var result = &SuggestEmailResponse{} + if err = c.sendSuggestRequest(ctx, "email", requestParams, result); err != nil { + return + } + ret = result.Suggestions + return +} + +// SuggestCountries try to return suggest countries by requestParams +func (c *Client) SuggestCountries(requestParams SuggestRequestParams) ([]ResponseCountry, error) { + return c.SuggestCountriesWithCtx(context.Background(), requestParams) +} + +// SuggestCountriesWithCtx try to return suggest countries by requestParams +func (c *Client) SuggestCountriesWithCtx(ctx context.Context, requestParams SuggestRequestParams) (ret []ResponseCountry, err error) { + var result = &SuggestCountryResponse{} + if err = c.sendSuggestRequest(ctx, "country", requestParams, result); err != nil { + return + } + ret = result.Suggestions + return +} diff --git a/suggest_test.go b/suggest_test.go new file mode 100644 index 0000000..ac8c1e0 --- /dev/null +++ b/suggest_test.go @@ -0,0 +1,114 @@ +package dadata + +import ( + "context" + "fmt" + "time" +) + +func ExampleClient_SuggestAddresses() { + daData := NewClient() + + addresses, err := daData.SuggestAddresses(SuggestRequestParams{Query: "Преснен", Count: 2}) + if nil != err { + fmt.Println(err) + } + + for _, address := range addresses { + fmt.Println(address.UnrestrictedValue) + fmt.Println(address.Data.Street) + fmt.Println(address.Data.FiasLevel) + } +} + +func ExampleClient_SuggestAddresses_granular() { + daData := NewClient() + + var req SuggestRequestParams + + req.Query = "лен" + + req.Locations = append(req.Locations, SuggestRequestParamsLocation{ + RegionFiasID: "df3d7359-afa9-4aaa-8ff9-197e73906b1c", + CityFiasID: "e9e684ce-7d60-4480-ba14-ca6da658188b", + }) + + req.FromBound = SuggestBound{SuggestBoundStreet} + req.ToBound = SuggestBound{SuggestBoundStreet} + + req.RestrictValue = true + req.Count = 2 + + addresses, err := daData.SuggestAddresses(req) + if nil != err { + fmt.Println(err) + } + + for _, address := range addresses { + fmt.Println(address.UnrestrictedValue) + fmt.Println(address.Data.Street) + } +} + +func ExampleClient_SuggestAddressesWithCtx() { + daData := NewClient() + + var req SuggestRequestParams + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + + req.Query = "лен" + + req.Locations = append(req.Locations, SuggestRequestParamsLocation{ + RegionFiasID: "df3d7359-afa9-4aaa-8ff9-197e73906b1c", + CityFiasID: "e9e684ce-7d60-4480-ba14-ca6da658188b", + }) + + req.FromBound = SuggestBound{SuggestBoundStreet} + req.ToBound = SuggestBound{SuggestBoundStreet} + + req.RestrictValue = true + req.Count = 2 + + addresses, err := daData.SuggestAddressesWithCtx(ctx, req) + if nil != err { + fmt.Println(err) + } + + for _, address := range addresses { + fmt.Println(address.UnrestrictedValue) + } + + cancel() + // if ctx is exited (by cancel or timeout) we must catch err + _, err = daData.SuggestAddressesWithCtx(ctx, req) + fmt.Println(err) +} + +func ExampleClient_SuggestBanks() { + daData := NewClient() + + banks, err := daData.SuggestBanks(SuggestRequestParams{Query: "Кредитный", Count: 3}) + if nil != err { + fmt.Println(err) + } + + for _, bank := range banks { + fmt.Println(bank.Data.Name.Full) + fmt.Println(bank.Data.Bic) + } +} + +func ExampleClient_SuggestParties() { + daData := NewClient() + + parties, err := daData.SuggestParties(SuggestRequestParams{Query: "Агрохолд", Count: 3}) + if nil != err { + fmt.Println(err) + } + + for _, party := range parties { + fmt.Println(party.Data.Name.Full) + fmt.Println(party.Data.Ogrn) + } +} diff --git a/suggest_types.go b/suggest_types.go new file mode 100644 index 0000000..7d5a3c6 --- /dev/null +++ b/suggest_types.go @@ -0,0 +1,73 @@ +package dadata + +// SuggestRequestParamsLocation constraints for suggestion +// full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=204669108 +type SuggestRequestParamsLocation struct { + FiasID string `json:"fias_id,omitempty"` + KladrID string `json:"kladr_id,omitempty"` + Region string `json:"region,omitempty"` + RegionFiasID string `json:"region_fias_id,omitempty"` + RegionKladrID string `json:"region_kladr_id,omitempty"` + RegionTypeFull string `json:"region_type_full,omitempty"` + City string `json:"city,omitempty"` + CityFiasID string `json:"city_fias_id,omitempty"` // search only in this area + CityKladrID string `json:"city_kladr_id,omitempty"` + CityTypeFull string `json:"city_type_full,omitempty"` + CityDistrictTypeFull string `json:"city_district_type_full,omitempty"` + Settlement string `json:"settlement,omitempty"` + SettlementFiasID string `json:"settlement_fias_id,omitempty"` + SettlementKladrID string `json:"settlement_kladr_id,omitempty"` + SettlementTypeFull string `json:"settlement_type_full,omitempty"` + Street string `json:"street,omitempty"` + StreetFiasID string `json:"street_fias_id,omitempty"` + StreetKladrID string `json:"street_kladr_id,omitempty"` + StreetTypeFull string `json:"street_type_full,omitempty"` + AreaTypeFull string `json:"area_type_full,omitempty"` +} + +// SuggestBound for granular sugestion +// full documentation https://confluence.hflabs.ru/pages/viewpage.action?pageId=222888017 +type SuggestBound struct { + Value BoundValue `json:"value"` +} + +// SuggestRequestParams Request struct +type SuggestRequestParams struct { + Query string `json:"query"` // user input for suggestion + Count int `json:"count"` // ligmit for results + Locations []SuggestRequestParamsLocation `json:"locations"` + RestrictValue bool `json:"restrict_value"` // don't show restricts (region) on results + + FromBound SuggestBound `json:"from_bound"` + ToBound SuggestBound `json:"to_bound"` +} + +// SuggestAddressResponse result slice for address suggestions +type SuggestAddressResponse struct { + Suggestions []ResponseAddress `json:"suggestions"` +} + +// SuggestNameResponse result slice for name suggestions +type SuggestNameResponse struct { + Suggestions []ResponseName `json:"suggestions"` +} + +// SuggestBankResponse result slice for bank suggestions +type SuggestBankResponse struct { + Suggestions []ResponseBank `json:"suggestions"` +} + +// SuggestPartyResponse result slice for party suggestions +type SuggestPartyResponse struct { + Suggestions []ResponseParty `json:"suggestions"` +} + +// SuggestEmailResponse result slice for email suggestions +type SuggestEmailResponse struct { + Suggestions []ResponseEmail `json:"suggestions"` +} + +// SuggestCountryResponse result slice for country suggestions +type SuggestCountryResponse struct { + Suggestions []ResponseCountry `json:"suggestions"` +}