From 2e8fb54ecb7b43ca5c6f1dd2ddc285bbd776a2cc Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 22:12:49 +0100 Subject: [PATCH 01/11] Fix response body leak and wrap errors in lookup Close response body on non-200 error paths to prevent resource leaks. Wrap errors with the original error using %w for better debugging. --- api.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index 1a50d67..1216f9d 100644 --- a/api.go +++ b/api.go @@ -135,7 +135,7 @@ func (api *API) lookup(namespace string, search map[string]interface{}) (*http.R // everything is in the URL request, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, ErrBuildingRequest + return nil, fmt.Errorf("%w: %w", ErrBuildingRequest, err) } if api.apiKey != "" { @@ -146,16 +146,18 @@ func (api *API) lookup(namespace string, search map[string]interface{}) (*http.R client := &http.Client{} response, err := client.Do(request) if err != nil { - return nil, ErrQueryingAPI + return nil, fmt.Errorf("%w: %w", ErrQueryingAPI, err) } // Special handling for PeeringDB rate limit if response.StatusCode == http.StatusTooManyRequests { + response.Body.Close() return nil, ErrRateLimitExceeded } // Generic handling for non-OK responses if response.StatusCode != http.StatusOK { body, _ := io.ReadAll(response.Body) + response.Body.Close() return nil, fmt.Errorf("%s: %s", response.Status, body) } From 5c92fec9d94423ed2277b7fb7c217409aa27c4a5 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 22:13:41 +0100 Subject: [PATCH 02/11] Fix typos and remove dead code Fix constant name networkInternetExchangeLANNamepsace typo, fix NewAPIWithAPIKey doc comment, remove unreachable ErrBuildingURL check. --- api.go | 10 ++-------- api_test.go | 2 +- net.go | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/api.go b/api.go index 1216f9d..73b065c 100644 --- a/api.go +++ b/api.go @@ -21,15 +21,12 @@ const ( internetExchangePrefixNamespace = "ixpfx" networkNamespace = "net" networkFacilityNamespace = "netfac" - networkInternetExchangeLANNamepsace = "netixlan" + networkInternetExchangeLANNamespace = "netixlan" organizationNamespace = "org" networkContactNamespace = "poc" ) var ( - // ErrBuildingURL is the error that will be returned if the URL to call the - // API cannot be built as expected. - ErrBuildingURL = errors.New("error while building the URL to call the peeringdb api") // ErrBuildingRequest is the error that will be returned if the HTTP // request to call the API cannot be built as expected. ErrBuildingRequest = errors.New("error while building the request to send to the peeringdb api") @@ -55,7 +52,7 @@ func NewAPI() *API { return &API{url: baseAPI} } -// NewAPIWithAuth returns a pointer to a new API structure. The API will point +// NewAPIWithAPIKey returns a pointer to a new API structure. The API will point // to the publicly known PeeringDB API endpoint and will use the provided API // key for authentication while making API calls. func NewAPIWithAPIKey(apiKey string) *API { @@ -127,9 +124,6 @@ func formatURL(base, namespace string, search map[string]interface{}) string { // decode with a JSON decoder. func (api *API) lookup(namespace string, search map[string]interface{}) (*http.Response, error) { url := formatURL(api.url, namespace, search) - if url == "" { - return nil, ErrBuildingURL - } // Prepare the GET request to the API, no need to set a body since // everything is in the URL diff --git a/api_test.go b/api_test.go index 5878079..5d474d9 100644 --- a/api_test.go +++ b/api_test.go @@ -105,7 +105,7 @@ func TestFormatURL(t *testing.T) { // Test netixlan namespace with search parameter expected = "https://www.peeringdb.com/api/netixlan?depth=1&id=10" - url = formatURL(base, networkInternetExchangeLANNamepsace, searchMap) + url = formatURL(base, networkInternetExchangeLANNamespace, searchMap) if url != expected { t.Errorf("formatURL, want '%s' got '%s'", expected, url) } diff --git a/net.go b/net.go index d37139c..15d0668 100644 --- a/net.go +++ b/net.go @@ -306,7 +306,7 @@ type NetworkInternetExchangeLAN struct { // response. An error can be returned if something went wrong. func (api *API) getNetworkInternetExchangeLANResource(search map[string]interface{}) (*networkInternetExchangeLANResource, error) { // Get the NetworkInternetExchangeLANResource from the API - response, err := api.lookup(networkInternetExchangeLANNamepsace, search) + response, err := api.lookup(networkInternetExchangeLANNamespace, search) if err != nil { return nil, err } From 843de29ea9e1c181f65f2f9aeed7571493b26807 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 22:13:57 +0100 Subject: [PATCH 03/11] Add missing CarrierCount field to Facility The carrier_count field exists in the API schema but was missing from the struct. --- facility.go | 1 + 1 file changed, 1 insertion(+) diff --git a/facility.go b/facility.go index 1bebfa8..8a1ce43 100644 --- a/facility.go +++ b/facility.go @@ -36,6 +36,7 @@ type Facility struct { Notes string `json:"notes"` NetCount int `json:"net_count"` IXCount int `json:"ix_count"` + CarrierCount int `json:"carrier_count"` SalesEmail string `json:"sales_email"` SalesPhone string `json:"sales_phone"` TechEmail string `json:"tech_email"` From ee3bba1932247b54d13b3d0bc247c5d4ddbe5721 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 22:14:46 +0100 Subject: [PATCH 04/11] Extract SocialMedia as a named type Replace the anonymous struct repeated in every entity with a shared SocialMedia type defined in api.go. --- api.go | 6 ++++++ campus.go | 5 +---- carrier.go | 5 +---- facility.go | 5 +---- ix.go | 5 +---- net.go | 5 +---- organization.go | 5 +---- 7 files changed, 12 insertions(+), 24 deletions(-) diff --git a/api.go b/api.go index 73b065c..c8f19f0 100644 --- a/api.go +++ b/api.go @@ -38,6 +38,12 @@ var ( ErrRateLimitExceeded = errors.New("rate limit exceeded") ) +// SocialMedia represents a social media link for a PeeringDB entity. +type SocialMedia struct { + Service string `json:"service"` + Identifier string `json:"identifier"` +} + // API is the structure used to interact with the PeeringDB API. This is the // main structure of this package. All functions to make API calls are // associated to this structure. diff --git a/campus.go b/campus.go index 226deda..2a2f573 100644 --- a/campus.go +++ b/campus.go @@ -35,10 +35,7 @@ type Campus struct { State string `json:"state"` Zipcode string `json:"zipcode"` FacilitySet []int `json:"fac_set"` - SocialMedia []struct { - Service string `json:"service"` - Identifier string `json:"identifier"` - } `json:"social_media"` + SocialMedia []SocialMedia `json:"social_media"` } // getCampusResource returns a pointer to a campusResource structure diff --git a/carrier.go b/carrier.go index 4a7b799..9a5e34e 100644 --- a/carrier.go +++ b/carrier.go @@ -31,10 +31,7 @@ type Carrier struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` Status string `json:"status"` - SocialMedia []struct { - Service string `json:"service"` - Identifier string `json:"identifier"` - } `json:"social_media"` + SocialMedia []SocialMedia `json:"social_media"` } // getCarrierResource returns a pointer to a carrierResource structure diff --git a/facility.go b/facility.go index 8a1ce43..1580856 100644 --- a/facility.go +++ b/facility.go @@ -59,10 +59,7 @@ type Facility struct { Suite string `json:"suite"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` - SocialMedia []struct { - Service string `json:"service"` - Identifier string `json:"identifier"` - } `json:"social_media"` + SocialMedia []SocialMedia `json:"social_media"` } // getFacilityResource returns a pointer to a facilityResource structure diff --git a/ix.go b/ix.go index ae96318..3ce01a1 100644 --- a/ix.go +++ b/ix.go @@ -55,10 +55,7 @@ type InternetExchange struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` Status string `json:"status"` - SocialMedia []struct { - Service string `json:"service"` - Identifier string `json:"identifier"` - } `json:"social_media"` + SocialMedia []SocialMedia `json:"social_media"` } // getInternetExchangeResource returns a pointer to an internetExchangeResource diff --git a/net.go b/net.go index 15d0668..2236a03 100644 --- a/net.go +++ b/net.go @@ -64,10 +64,7 @@ type Network struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` Status string `json:"status"` - SocialMedia []struct { - Service string `json:"service"` - Identifier string `json:"identifier"` - } `json:"social_media"` + SocialMedia []SocialMedia `json:"social_media"` } // getNetworkResource returns a pointer to an networkResource structure diff --git a/organization.go b/organization.go index 762b35a..0657a23 100644 --- a/organization.go +++ b/organization.go @@ -45,10 +45,7 @@ type Organization struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` Status string `json:"status"` - SocialMedia []struct { - Service string `json:"service"` - Identifier string `json:"identifier"` - } `json:"social_media"` + SocialMedia []SocialMedia `json:"social_media"` } // getOrganizationResource returns a pointer to an organizationResource From e2a389ff38d4bc2780eece12c1a35b601a554294 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 22:15:50 +0100 Subject: [PATCH 05/11] Replace constructors with functional options and reuse http.Client Replace four constructors with a single NewAPI(opts...) using functional options (WithURL, WithAPIKey, WithHTTPClient). Store http.Client on the API struct for connection pooling and configurable timeouts. --- api.go | 61 +++++++++++++--------------- api_test.go | 112 ++++++++++++++++++++-------------------------------- 2 files changed, 69 insertions(+), 104 deletions(-) diff --git a/api.go b/api.go index c8f19f0..0efa1c0 100644 --- a/api.go +++ b/api.go @@ -44,52 +44,47 @@ type SocialMedia struct { Identifier string `json:"identifier"` } -// API is the structure used to interact with the PeeringDB API. This is the -// main structure of this package. All functions to make API calls are -// associated to this structure. +// API is the structure used to interact with the PeeringDB API. type API struct { url string apiKey string + client *http.Client } -// NewAPI returns a pointer to a new API structure. It uses the publicly known -// PeeringDB API endpoint. -func NewAPI() *API { - return &API{url: baseAPI} -} +// Option configures an API instance. +type Option func(*API) -// NewAPIWithAPIKey returns a pointer to a new API structure. The API will point -// to the publicly known PeeringDB API endpoint and will use the provided API -// key for authentication while making API calls. -func NewAPIWithAPIKey(apiKey string) *API { - return &API{ - url: baseAPI, - apiKey: apiKey, +// WithURL sets a custom API endpoint URL. +func WithURL(u string) Option { + return func(api *API) { + api.url = u } } -// NewAPIFromURL returns a pointer to a new API structure from a given URL. If -// the given URL is empty it will use the default PeeringDB API URL. -func NewAPIFromURL(url string) *API { - if url == "" { - return NewAPI() +// WithAPIKey sets the API key for authentication. +func WithAPIKey(apiKey string) Option { + return func(api *API) { + api.apiKey = apiKey } - - return &API{url: url} } -// NewAPIFromURLWithAPIKey returns a pointer to a new API structure from a given -// URL. If the given URL is empty it will use the default PeeringDB API URL. It -// will use the provided API key for authentication while making API calls. -func NewAPIFromURLWithAPIKey(url, apiKey string) *API { - if url == "" { - return NewAPIWithAPIKey(apiKey) +// WithHTTPClient sets a custom HTTP client. +func WithHTTPClient(client *http.Client) Option { + return func(api *API) { + api.client = client } +} - return &API{ - url: url, - apiKey: apiKey, +// NewAPI returns a new API instance configured with the given options. +func NewAPI(opts ...Option) *API { + api := &API{ + url: baseAPI, + client: &http.Client{}, + } + for _, opt := range opts { + opt(api) } + return api } // formatSearchParameters is used to format parameters for a request. When @@ -142,9 +137,7 @@ func (api *API) lookup(namespace string, search map[string]interface{}) (*http.R request.Header.Add("Authorization", fmt.Sprintf("Api-Key %s", api.apiKey)) } - // Send the request to the API using a simple HTTP client - client := &http.Client{} - response, err := client.Do(request) + response, err := api.client.Do(request) if err != nil { return nil, fmt.Errorf("%w: %w", ErrQueryingAPI, err) } diff --git a/api_test.go b/api_test.go index 5d474d9..5cfe140 100644 --- a/api_test.go +++ b/api_test.go @@ -126,76 +126,48 @@ func TestFormatURL(t *testing.T) { } func TestNewAPI(t *testing.T) { - var expectedURL string - - // Test to use the public PeeringDB API - api := NewAPI() - expectedURL = "https://www.peeringdb.com/api/" - if api.url != expectedURL { - t.Errorf("formatURL, want '%s' got '%s'", expectedURL, api.url) - } -} - -func TestNewAPIWithAPIKey(t *testing.T) { - var expectedURL, expectedApiKey string - - // Test to use the public PeeringDB API with authentication - api := NewAPIWithAPIKey("test123") - expectedURL = "https://www.peeringdb.com/api/" - expectedApiKey = "test123" - if api.url != expectedURL { - t.Errorf("formatURL, want '%s' got '%s'", expectedURL, api.url) - } - if api.apiKey != expectedApiKey { - t.Errorf("formatURL, want '%s' got '%s'", expectedApiKey, api.apiKey) - } -} - -func TestNewAPIFromURL(t *testing.T) { - var expectedURL string - var api *API - - // Test to see if an empty string parameter will force to use the public - // PeeringDB API. - api = NewAPIFromURL("") - expectedURL = "https://www.peeringdb.com/api/" - if api.url != expectedURL { - t.Errorf("formatURL, want '%s' got '%s'", expectedURL, api.url) - } - - // Test with - api = NewAPIFromURL("http://localhost/api/") - expectedURL = "http://localhost/api/" - if api.url != expectedURL { - t.Errorf("formatURL, want '%s' got '%s'", expectedURL, api.url) - } -} - -func TestNewAPIFromURLWithAPIKey(t *testing.T) { - var expectedURL, expectedApiKey string - var api *API - - // Test to see if an empty string parameter will force to use the public - // PeeringDB API. - api = NewAPIFromURLWithAPIKey("", "test123") - expectedURL = "https://www.peeringdb.com/api/" - expectedApiKey = "test123" - if api.url != expectedURL { - t.Errorf("formatURL, want '%s' got '%s'", expectedURL, api.url) - } - if api.apiKey != expectedApiKey { - t.Errorf("formatURL, want '%s' got '%s'", expectedApiKey, api.apiKey) - } - - // Test with - api = NewAPIFromURLWithAPIKey("http://localhost/api/", "test123") - expectedURL = "http://localhost/api/" - expectedApiKey = "test123" - if api.url != expectedURL { - t.Errorf("formatURL, want '%s' got '%s'", expectedURL, api.url) - } - if api.apiKey != expectedApiKey { - t.Errorf("formatURL, want '%s' got '%s'", expectedApiKey, api.apiKey) + tests := []struct { + name string + opts []Option + wantURL string + wantAPIKey string + }{ + { + name: "default", + wantURL: baseAPI, + }, + { + name: "with API key", + opts: []Option{WithAPIKey("test123")}, + wantURL: baseAPI, + wantAPIKey: "test123", + }, + { + name: "with custom URL", + opts: []Option{WithURL("http://localhost/api/")}, + wantURL: "http://localhost/api/", + }, + { + name: "with URL and API key", + opts: []Option{WithURL("http://localhost/api/"), WithAPIKey("test123")}, + wantURL: "http://localhost/api/", + wantAPIKey: "test123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := NewAPI(tt.opts...) + if api.url != tt.wantURL { + t.Errorf("url: want %q, got %q", tt.wantURL, api.url) + } + if api.apiKey != tt.wantAPIKey { + t.Errorf("apiKey: want %q, got %q", tt.wantAPIKey, api.apiKey) + } + if api.client == nil { + t.Error("client should not be nil") + } + }) } } From 9e8778b558005370a8eaf8b804a70737b1e55d21 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 22:21:40 +0100 Subject: [PATCH 06/11] Eliminate boilerplate with generics, context.Context, and url.Values Replace per-entity Resource structs, getResource methods, and repetitive Get/GetAll/GetByID functions with generic fetch[T] and fetchByID[T] helpers. All public methods now accept context.Context for cancellation and url.Values for type-safe query parameters, and return []T instead of *[]T. Tests rewritten with httptest to avoid live API calls. --- api.go | 130 ++++---- api_test.go | 821 +++++++++++++++++++++++++++++++++++++++-------- campus.go | 135 ++------ carrier.go | 232 +++---------- contact.go | 102 +----- doc.go | 13 - examples_test.go | 52 ++- facility.go | 178 +++------- ix.go | 496 ++++++---------------------- net.go | 406 +++++------------------ organization.go | 157 +++------ 11 files changed, 1141 insertions(+), 1581 deletions(-) diff --git a/api.go b/api.go index 0efa1c0..0af5f63 100644 --- a/api.go +++ b/api.go @@ -1,12 +1,14 @@ package peeringdb import ( + "context" + "encoding/json" "errors" "fmt" "io" "net/http" "net/url" - "sort" + "strconv" ) const ( @@ -27,15 +29,16 @@ const ( ) var ( - // ErrBuildingRequest is the error that will be returned if the HTTP - // request to call the API cannot be built as expected. - ErrBuildingRequest = errors.New("error while building the request to send to the peeringdb api") - // ErrQueryingAPI is the error that will be returned if there is an issue - // while making the request to the API. - ErrQueryingAPI = errors.New("error while querying peeringdb api") - // ErrRateLimitExceeded is the error that will be returned if the API rate - // limit is exceeded. + // ErrBuildingRequest is returned if the HTTP request to call the API + // cannot be built as expected. + ErrBuildingRequest = errors.New("error building request for peeringdb api") + // ErrQueryingAPI is returned if there is an issue while making the + // request to the API. + ErrQueryingAPI = errors.New("error querying peeringdb api") + // ErrRateLimitExceeded is returned if the API rate limit is exceeded. ErrRateLimitExceeded = errors.New("rate limit exceeded") + // ErrInvalidID is returned when a resource ID is invalid (e.g. <= 0). + ErrInvalidID = errors.New("invalid resource ID") ) // SocialMedia represents a social media link for a PeeringDB entity. @@ -44,6 +47,15 @@ type SocialMedia struct { Identifier string `json:"identifier"` } +// resource is a generic top-level structure for parsing JSON responses from +// the PeeringDB API. +type resource[T any] struct { + Meta struct { + Generated float64 `json:"generated,omitempty"` + } `json:"meta"` + Data []T `json:"data"` +} + // API is the structure used to interact with the PeeringDB API. type API struct { url string @@ -87,93 +99,79 @@ func NewAPI(opts ...Option) *API { return api } -// formatSearchParameters is used to format parameters for a request. When -// building the search string the keys will be used in the alphabetic order. -func formatSearchParameters(parameters map[string]interface{}) string { - // Nothing in slice, just return empty string - if parameters == nil { - return "" +// fetch queries the PeeringDB API for a given namespace and returns a slice +// of results. It uses generics to avoid per-entity boilerplate. +func fetch[T any](ctx context.Context, api *API, namespace string, search url.Values) ([]T, error) { + u := api.url + namespace + "?depth=1" + if len(search) > 0 { + u += "&" + search.Encode() } - var search string - var keys []string - - // Get all map keys - for i := range parameters { - keys = append(keys, i) - } - - // Sort the keys slice - sort.Strings(keys) - - // For each element, append it to the request separated by a & symbol. - for _, key := range keys { - search = search + "&" + key + "=" + url.QueryEscape(fmt.Sprintf("%v", parameters[key])) - } - - return search -} - -// formatURL is used to format a URL to make a request on PeeringDB API. -func formatURL(base, namespace string, search map[string]interface{}) string { - return fmt.Sprintf("%s%s?depth=1%s", base, namespace, - formatSearchParameters(search)) -} - -// lookup is used to query the PeeringDB API given a namespace to use and data -// to format the request. It returns an HTTP response that the caller must -// decode with a JSON decoder. -func (api *API) lookup(namespace string, search map[string]interface{}) (*http.Response, error) { - url := formatURL(api.url, namespace, search) - - // Prepare the GET request to the API, no need to set a body since - // everything is in the URL - request, err := http.NewRequest("GET", url, nil) + request, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, fmt.Errorf("%w: %w", ErrBuildingRequest, err) } if api.apiKey != "" { - request.Header.Add("Authorization", fmt.Sprintf("Api-Key %s", api.apiKey)) + request.Header.Set("Authorization", "Api-Key "+api.apiKey) } response, err := api.client.Do(request) if err != nil { return nil, fmt.Errorf("%w: %w", ErrQueryingAPI, err) } + defer response.Body.Close() - // Special handling for PeeringDB rate limit if response.StatusCode == http.StatusTooManyRequests { - response.Body.Close() return nil, ErrRateLimitExceeded } - // Generic handling for non-OK responses if response.StatusCode != http.StatusOK { body, _ := io.ReadAll(response.Body) - response.Body.Close() return nil, fmt.Errorf("%s: %s", response.Status, body) } - return response, nil + var r resource[T] + if err := json.NewDecoder(response.Body).Decode(&r); err != nil { + return nil, err + } + + return r.Data, nil } -// GetASN is a simplified function to get PeeringDB details about a given AS -// number. It basically gets the Net object matching the AS number. If the AS -// number cannot be found, nil is returned. -func (api *API) GetASN(asn int) (*Network, error) { - search := make(map[string]interface{}) - search["asn"] = asn +// fetchByID queries the PeeringDB API for a single resource by ID. +func fetchByID[T any](ctx context.Context, api *API, namespace string, id int) (*T, error) { + if id <= 0 { + return nil, ErrInvalidID + } - // Actually fetch the Network from PeeringDB - network, err := api.GetNetwork(search) + search := url.Values{} + search.Set("id", strconv.Itoa(id)) - // Error, so nil pointer returned + results, err := fetch[T](ctx, api, namespace, search) if err != nil { return nil, err } + if len(results) == 0 { + return nil, nil + } + + return &results[0], nil +} - if len(*network) == 0 { +// GetASN returns the Network matching the given AS number. If the AS number +// cannot be found, an error is returned. +func (api *API) GetASN(ctx context.Context, asn int) (*Network, error) { + search := url.Values{} + search.Set("asn", strconv.Itoa(asn)) + + networks, err := api.GetNetwork(ctx, search) + if err != nil { + return nil, err + } + + if len(networks) == 0 { return nil, fmt.Errorf("no network found for ASN %d", asn) } - return &(*network)[0], nil + + return &networks[0], nil } diff --git a/api_test.go b/api_test.go index 5cfe140..9fc29d6 100644 --- a/api_test.go +++ b/api_test.go @@ -1,128 +1,30 @@ package peeringdb -import "testing" +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) -func TestFormatSearchParameters(t *testing.T) { - var searchMap map[string]interface{} - var expected string - var searchParameters string +// newTestServer creates a test HTTP server that responds with the given +// status code and body for any request. +func newTestServer(t *testing.T, statusCode int, body interface{}) (*httptest.Server, *API) { + t.Helper() - // Test for nil map - expected = "" - searchParameters = formatSearchParameters(nil) - if searchParameters != expected { - t.Errorf("formatSearchParameters, want '%s' got '%s'", expected, - searchParameters) - } - - // Test for empty map - searchMap = make(map[string]interface{}) - expected = "" - searchParameters = formatSearchParameters(searchMap) - if searchParameters != expected { - t.Errorf("formatSearchParameters, want '%s' got '%s'", expected, - searchParameters) - } - - // Test one value - searchMap = make(map[string]interface{}) - searchMap["id"] = 10 - expected = "&id=10" - searchParameters = formatSearchParameters(searchMap) - if searchParameters != expected { - t.Errorf("formatSearchParameters, want '%s' got '%s'", expected, - searchParameters) - } - - // Test two values - searchMap = make(map[string]interface{}) - searchMap["id"] = 10 - searchMap["asn"] = 65536 - expected = "&asn=65536&id=10" - searchParameters = formatSearchParameters(searchMap) - if searchParameters != expected { - t.Errorf("formatSearchParameters, want '%s' got '%s'", expected, - searchParameters) - } -} - -func TestFormatURL(t *testing.T) { - var expected string - var url string - - base := "https://www.peeringdb.com/api/" - searchMap := make(map[string]interface{}) - searchMap["id"] = 10 - - // Test fac namespace with search parameter - expected = "https://www.peeringdb.com/api/fac?depth=1&id=10" - url = formatURL(base, facilityNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test ix namespace with search parameter - expected = "https://www.peeringdb.com/api/ix?depth=1&id=10" - url = formatURL(base, internetExchangeNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test ixfac namespace with search parameter - expected = "https://www.peeringdb.com/api/ixfac?depth=1&id=10" - url = formatURL(base, internetExchangeFacilityNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test ixlan namespace with search parameter - expected = "https://www.peeringdb.com/api/ixlan?depth=1&id=10" - url = formatURL(base, internetExchangeLANNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test ixpfx namespace with search parameter - expected = "https://www.peeringdb.com/api/ixpfx?depth=1&id=10" - url = formatURL(base, internetExchangePrefixNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test net namespace with search parameter - expected = "https://www.peeringdb.com/api/net?depth=1&id=10" - url = formatURL(base, networkNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test netfac namespace with search parameter - expected = "https://www.peeringdb.com/api/netfac?depth=1&id=10" - url = formatURL(base, networkFacilityNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(body) + })) - // Test netixlan namespace with search parameter - expected = "https://www.peeringdb.com/api/netixlan?depth=1&id=10" - url = formatURL(base, networkInternetExchangeLANNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } - - // Test org namespace with search parameter - expected = "https://www.peeringdb.com/api/org?depth=1&id=10" - url = formatURL(base, organizationNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } + api := NewAPI(WithURL(ts.URL + "/")) + t.Cleanup(ts.Close) - // Test poc namespace with search parameter - expected = "https://www.peeringdb.com/api/poc?depth=1&id=10" - url = formatURL(base, networkContactNamespace, searchMap) - if url != expected { - t.Errorf("formatURL, want '%s' got '%s'", expected, url) - } + return ts, api } func TestNewAPI(t *testing.T) { @@ -171,17 +73,684 @@ func TestNewAPI(t *testing.T) { } } -func TestGetASN(t *testing.T) { +func TestFetchSuccess(t *testing.T) { + resp := resource[Organization]{ + Data: []Organization{{ID: 1, Name: "Test Org"}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + orgs, err := api.GetOrganization(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(orgs) != 1 { + t.Fatalf("expected 1 org, got %d", len(orgs)) + } + if orgs[0].Name != "Test Org" { + t.Errorf("expected name %q, got %q", "Test Org", orgs[0].Name) + } +} + +func TestFetchWithSearchParams(t *testing.T) { + var receivedURL string + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedURL = r.URL.String() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resource[Network]{}) + })) + defer ts.Close() + + api := NewAPI(WithURL(ts.URL + "/")) + search := url.Values{} + search.Set("asn", "65536") + + _, err := api.GetNetwork(context.Background(), search) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if receivedURL != "/net?depth=1&asn=65536" { + t.Errorf("unexpected URL: %s", receivedURL) + } +} + +func TestFetchWithAPIKey(t *testing.T) { + var receivedAuth string + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resource[Network]{}) + })) + defer ts.Close() + + api := NewAPI(WithURL(ts.URL+"/"), WithAPIKey("secret-key")) + _, err := api.GetNetwork(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if receivedAuth != "Api-Key secret-key" { + t.Errorf("expected auth header %q, got %q", "Api-Key secret-key", receivedAuth) + } +} + +func TestFetchRateLimit(t *testing.T) { + _, api := newTestServer(t, http.StatusTooManyRequests, nil) + + _, err := api.GetNetwork(context.Background(), nil) + if !errors.Is(err, ErrRateLimitExceeded) { + t.Errorf("expected ErrRateLimitExceeded, got %v", err) + } +} + +func TestFetchServerError(t *testing.T) { + _, api := newTestServer(t, http.StatusInternalServerError, "something went wrong") + + _, err := api.GetNetwork(context.Background(), nil) + if err == nil { + t.Fatal("expected error for 500 response") + } +} + +func TestFetchInvalidJSON(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("not json")) + })) + defer ts.Close() + + api := NewAPI(WithURL(ts.URL + "/")) + _, err := api.GetNetwork(context.Background(), nil) + if err == nil { + t.Fatal("expected error for invalid JSON") + } +} + +func TestFetchConnectionError(t *testing.T) { + api := NewAPI(WithURL("http://127.0.0.1:1/")) + _, err := api.GetNetwork(context.Background(), nil) + if !errors.Is(err, ErrQueryingAPI) { + t.Errorf("expected ErrQueryingAPI, got %v", err) + } +} + +func TestFetchContextCancellation(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Block forever; the cancelled context should abort the request + select {} + })) + defer ts.Close() + + api := NewAPI(WithURL(ts.URL + "/")) + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + + _, err := api.GetNetwork(ctx, nil) + if err == nil { + t.Fatal("expected error for cancelled context") + } +} + +func TestFetchByIDSuccess(t *testing.T) { + resp := resource[Network]{ + Data: []Network{{ID: 42, Name: "Test Net", ASN: 65536}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + net, err := api.GetNetworkByID(context.Background(), 42) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if net == nil { + t.Fatal("expected non-nil network") + } + if net.ID != 42 { + t.Errorf("expected ID 42, got %d", net.ID) + } +} + +func TestFetchByIDNotFound(t *testing.T) { + resp := resource[Network]{Data: []Network{}} + _, api := newTestServer(t, http.StatusOK, resp) + + net, err := api.GetNetworkByID(context.Background(), 999) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if net != nil { + t.Errorf("expected nil for not found, got %+v", net) + } +} + +func TestFetchByIDInvalid(t *testing.T) { api := NewAPI() - expectedASN := 201281 - net, err := api.GetASN(expectedASN) + _, err := api.GetNetworkByID(context.Background(), 0) + if !errors.Is(err, ErrInvalidID) { + t.Errorf("expected ErrInvalidID for id=0, got %v", err) + } + + _, err = api.GetNetworkByID(context.Background(), -1) + if !errors.Is(err, ErrInvalidID) { + t.Errorf("expected ErrInvalidID for id=-1, got %v", err) + } +} + +func TestGetASNSuccess(t *testing.T) { + resp := resource[Network]{ + Data: []Network{{ID: 1, Name: "Test AS", ASN: 201281}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + net, err := api.GetASN(context.Background(), 201281) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if net.ASN != 201281 { + t.Errorf("expected ASN 201281, got %d", net.ASN) + } +} + +func TestGetASNNotFound(t *testing.T) { + resp := resource[Network]{Data: []Network{}} + _, api := newTestServer(t, http.StatusOK, resp) + + _, err := api.GetASN(context.Background(), 99999) + if err == nil { + t.Fatal("expected error for ASN not found") + } +} + +func TestGetOrganization(t *testing.T) { + resp := resource[Organization]{ + Data: []Organization{{ID: 1, Name: "Test Org"}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + orgs, err := api.GetOrganization(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(orgs) != 1 || orgs[0].Name != "Test Org" { + t.Errorf("unexpected result: %+v", orgs) + } +} + +func TestGetInternetExchange(t *testing.T) { + resp := resource[InternetExchange]{ + Data: []InternetExchange{{ID: 1, Name: "Test IX"}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + ixs, err := api.GetInternetExchange(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(ixs) != 1 || ixs[0].Name != "Test IX" { + t.Errorf("unexpected result: %+v", ixs) + } +} + +func TestGetCarrier(t *testing.T) { + resp := resource[Carrier]{ + Data: []Carrier{{ID: 1, Name: "Test Carrier"}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + carriers, err := api.GetCarrier(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(carriers) != 1 || carriers[0].Name != "Test Carrier" { + t.Errorf("unexpected result: %+v", carriers) + } +} + +func TestGetCampus(t *testing.T) { + resp := resource[Campus]{ + Data: []Campus{{ID: 1, Name: "Test Campus"}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + campuses, err := api.GetCampus(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(campuses) != 1 || campuses[0].Name != "Test Campus" { + t.Errorf("unexpected result: %+v", campuses) + } +} + +func TestGetNetworkContact(t *testing.T) { + resp := resource[NetworkContact]{ + Data: []NetworkContact{{ID: 1, Name: "Admin Contact"}}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + contacts, err := api.GetNetworkContact(context.Background(), nil) if err != nil { - t.Fail() - return + t.Fatalf("unexpected error: %v", err) } + if len(contacts) != 1 || contacts[0].Name != "Admin Contact" { + t.Errorf("unexpected result: %+v", contacts) + } +} + +func TestWithHTTPClient(t *testing.T) { + custom := &http.Client{} + api := NewAPI(WithHTTPClient(custom)) + if api.client != custom { + t.Error("expected custom HTTP client to be set") + } +} + +func TestGetAllOrganizations(t *testing.T) { + resp := resource[Organization]{Data: []Organization{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + orgs, err := api.GetAllOrganizations(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(orgs) != 1 { + t.Errorf("expected 1 org, got %d", len(orgs)) + } +} - if net.ASN != expectedASN { - t.Errorf("GetASN, want ASN '%d' got '%d'", expectedASN, net.ASN) +func TestGetOrganizationByID(t *testing.T) { + resp := resource[Organization]{Data: []Organization{{ID: 5, Name: "Org 5"}}} + _, api := newTestServer(t, http.StatusOK, resp) + org, err := api.GetOrganizationByID(context.Background(), 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if org == nil || org.ID != 5 { + t.Errorf("unexpected result: %+v", org) + } +} + +func TestGetAllNetworks(t *testing.T) { + resp := resource[Network]{Data: []Network{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + nets, err := api.GetAllNetworks(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(nets) != 1 { + t.Errorf("expected 1 network, got %d", len(nets)) + } +} + +func TestGetAllNetworkFacilities(t *testing.T) { + resp := resource[NetworkFacility]{Data: []NetworkFacility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + nfs, err := api.GetAllNetworkFacilities(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(nfs) != 1 { + t.Errorf("expected 1 netfac, got %d", len(nfs)) + } +} + +func TestGetNetworkFacility(t *testing.T) { + resp := resource[NetworkFacility]{Data: []NetworkFacility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + nfs, err := api.GetNetworkFacility(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(nfs) != 1 { + t.Errorf("expected 1 netfac, got %d", len(nfs)) + } +} + +func TestGetNetworkFacilityByID(t *testing.T) { + resp := resource[NetworkFacility]{Data: []NetworkFacility{{ID: 3}}} + _, api := newTestServer(t, http.StatusOK, resp) + nf, err := api.GetNetworkFacilityByID(context.Background(), 3) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if nf == nil || nf.ID != 3 { + t.Errorf("unexpected result: %+v", nf) + } +} + +func TestGetNetworkInternetExchangeLAN(t *testing.T) { + resp := resource[NetworkInternetExchangeLAN]{Data: []NetworkInternetExchangeLAN{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + nixlans, err := api.GetNetworkInternetExchangeLAN(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(nixlans) != 1 { + t.Errorf("expected 1, got %d", len(nixlans)) + } +} + +func TestGetAllNetworkInternetExchangeLANs(t *testing.T) { + resp := resource[NetworkInternetExchangeLAN]{Data: []NetworkInternetExchangeLAN{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + nixlans, err := api.GetAllNetworkInternetExchangeLANs(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(nixlans) != 1 { + t.Errorf("expected 1, got %d", len(nixlans)) + } +} + +func TestGetNetworkInternetExchangeLANByID(t *testing.T) { + resp := resource[NetworkInternetExchangeLAN]{Data: []NetworkInternetExchangeLAN{{ID: 7}}} + _, api := newTestServer(t, http.StatusOK, resp) + nixlan, err := api.GetNetworkInternetExchangeLANByID(context.Background(), 7) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if nixlan == nil || nixlan.ID != 7 { + t.Errorf("unexpected result: %+v", nixlan) + } +} + +func TestGetAllInternetExchanges(t *testing.T) { + resp := resource[InternetExchange]{Data: []InternetExchange{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + ixs, err := api.GetAllInternetExchanges(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(ixs) != 1 { + t.Errorf("expected 1, got %d", len(ixs)) + } +} + +func TestGetInternetExchangeByID(t *testing.T) { + resp := resource[InternetExchange]{Data: []InternetExchange{{ID: 2}}} + _, api := newTestServer(t, http.StatusOK, resp) + ix, err := api.GetInternetExchangeByID(context.Background(), 2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ix == nil || ix.ID != 2 { + t.Errorf("unexpected result: %+v", ix) + } +} + +func TestGetInternetExchangeLAN(t *testing.T) { + resp := resource[InternetExchangeLAN]{Data: []InternetExchangeLAN{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + lans, err := api.GetInternetExchangeLAN(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(lans) != 1 { + t.Errorf("expected 1, got %d", len(lans)) + } +} + +func TestGetAllInternetExchangeLANs(t *testing.T) { + resp := resource[InternetExchangeLAN]{Data: []InternetExchangeLAN{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + lans, err := api.GetAllInternetExchangeLANs(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(lans) != 1 { + t.Errorf("expected 1, got %d", len(lans)) + } +} + +func TestGetInternetExchangeLANByID(t *testing.T) { + resp := resource[InternetExchangeLAN]{Data: []InternetExchangeLAN{{ID: 4}}} + _, api := newTestServer(t, http.StatusOK, resp) + lan, err := api.GetInternetExchangeLANByID(context.Background(), 4) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if lan == nil || lan.ID != 4 { + t.Errorf("unexpected result: %+v", lan) + } +} + +func TestGetInternetExchangePrefix(t *testing.T) { + resp := resource[InternetExchangePrefix]{Data: []InternetExchangePrefix{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + pfxs, err := api.GetInternetExchangePrefix(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(pfxs) != 1 { + t.Errorf("expected 1, got %d", len(pfxs)) + } +} + +func TestGetAllInternetExchangePrefixes(t *testing.T) { + resp := resource[InternetExchangePrefix]{Data: []InternetExchangePrefix{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + pfxs, err := api.GetAllInternetExchangePrefixes(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(pfxs) != 1 { + t.Errorf("expected 1, got %d", len(pfxs)) + } +} + +func TestGetInternetExchangePrefixByID(t *testing.T) { + resp := resource[InternetExchangePrefix]{Data: []InternetExchangePrefix{{ID: 6}}} + _, api := newTestServer(t, http.StatusOK, resp) + pfx, err := api.GetInternetExchangePrefixByID(context.Background(), 6) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pfx == nil || pfx.ID != 6 { + t.Errorf("unexpected result: %+v", pfx) + } +} + +func TestGetInternetExchangeFacility(t *testing.T) { + resp := resource[InternetExchangeFacility]{Data: []InternetExchangeFacility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + ixfacs, err := api.GetInternetExchangeFacility(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(ixfacs) != 1 { + t.Errorf("expected 1, got %d", len(ixfacs)) + } +} + +func TestGetAllInternetExchangeFacilities(t *testing.T) { + resp := resource[InternetExchangeFacility]{Data: []InternetExchangeFacility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + ixfacs, err := api.GetAllInternetExchangeFacilities(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(ixfacs) != 1 { + t.Errorf("expected 1, got %d", len(ixfacs)) + } +} + +func TestGetInternetExchangeFacilityByID(t *testing.T) { + resp := resource[InternetExchangeFacility]{Data: []InternetExchangeFacility{{ID: 8}}} + _, api := newTestServer(t, http.StatusOK, resp) + ixfac, err := api.GetInternetExchangeFacilityByID(context.Background(), 8) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ixfac == nil || ixfac.ID != 8 { + t.Errorf("unexpected result: %+v", ixfac) + } +} + +func TestGetFacility(t *testing.T) { + resp := resource[Facility]{Data: []Facility{{ID: 1, Name: "Test DC"}}} + _, api := newTestServer(t, http.StatusOK, resp) + facs, err := api.GetFacility(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(facs) != 1 || facs[0].Name != "Test DC" { + t.Errorf("unexpected result: %+v", facs) + } +} + +func TestGetAllFacilities(t *testing.T) { + resp := resource[Facility]{Data: []Facility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + facs, err := api.GetAllFacilities(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(facs) != 1 { + t.Errorf("expected 1, got %d", len(facs)) + } +} + +func TestGetFacilityByID(t *testing.T) { + resp := resource[Facility]{Data: []Facility{{ID: 9}}} + _, api := newTestServer(t, http.StatusOK, resp) + fac, err := api.GetFacilityByID(context.Background(), 9) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if fac == nil || fac.ID != 9 { + t.Errorf("unexpected result: %+v", fac) + } +} + +func TestGetAllCarriers(t *testing.T) { + resp := resource[Carrier]{Data: []Carrier{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + carriers, err := api.GetAllCarriers(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(carriers) != 1 { + t.Errorf("expected 1, got %d", len(carriers)) + } +} + +func TestGetCarrierByID(t *testing.T) { + resp := resource[Carrier]{Data: []Carrier{{ID: 3}}} + _, api := newTestServer(t, http.StatusOK, resp) + c, err := api.GetCarrierByID(context.Background(), 3) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil || c.ID != 3 { + t.Errorf("unexpected result: %+v", c) + } +} + +func TestGetCarrierFacility(t *testing.T) { + resp := resource[CarrierFacility]{Data: []CarrierFacility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + cfs, err := api.GetCarrierFacility(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cfs) != 1 { + t.Errorf("expected 1, got %d", len(cfs)) + } +} + +func TestGetAllCarrierFacilities(t *testing.T) { + resp := resource[CarrierFacility]{Data: []CarrierFacility{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + cfs, err := api.GetAllCarrierFacilities(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cfs) != 1 { + t.Errorf("expected 1, got %d", len(cfs)) + } +} + +func TestGetCarrierFacilityByID(t *testing.T) { + resp := resource[CarrierFacility]{Data: []CarrierFacility{{ID: 5}}} + _, api := newTestServer(t, http.StatusOK, resp) + cf, err := api.GetCarrierFacilityByID(context.Background(), 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cf == nil || cf.ID != 5 { + t.Errorf("unexpected result: %+v", cf) + } +} + +func TestGetAllCampuses(t *testing.T) { + resp := resource[Campus]{Data: []Campus{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + campuses, err := api.GetAllCampuses(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(campuses) != 1 { + t.Errorf("expected 1, got %d", len(campuses)) + } +} + +func TestGetCampusByID(t *testing.T) { + resp := resource[Campus]{Data: []Campus{{ID: 2}}} + _, api := newTestServer(t, http.StatusOK, resp) + c, err := api.GetCampusByID(context.Background(), 2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil || c.ID != 2 { + t.Errorf("unexpected result: %+v", c) + } +} + +func TestGetAllNetworkContacts(t *testing.T) { + resp := resource[NetworkContact]{Data: []NetworkContact{{ID: 1}}} + _, api := newTestServer(t, http.StatusOK, resp) + contacts, err := api.GetAllNetworkContacts(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(contacts) != 1 { + t.Errorf("expected 1, got %d", len(contacts)) + } +} + +func TestGetNetworkContactByID(t *testing.T) { + resp := resource[NetworkContact]{Data: []NetworkContact{{ID: 4}}} + _, api := newTestServer(t, http.StatusOK, resp) + c, err := api.GetNetworkContactByID(context.Background(), 4) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil || c.ID != 4 { + t.Errorf("unexpected result: %+v", c) + } +} + +func TestSocialMediaDeserialization(t *testing.T) { + resp := resource[Organization]{ + Data: []Organization{{ + ID: 1, + Name: "Social Org", + SocialMedia: []SocialMedia{ + {Service: "website", Identifier: "https://example.com"}, + {Service: "twitter", Identifier: "@example"}, + }, + }}, + } + _, api := newTestServer(t, http.StatusOK, resp) + + orgs, err := api.GetOrganization(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(orgs[0].SocialMedia) != 2 { + t.Fatalf("expected 2 social media entries, got %d", len(orgs[0].SocialMedia)) + } + if orgs[0].SocialMedia[0].Service != "website" { + t.Errorf("expected service %q, got %q", "website", orgs[0].SocialMedia[0].Service) } } diff --git a/campus.go b/campus.go index 2a2f573..4a1fe93 100644 --- a/campus.go +++ b/campus.go @@ -1,121 +1,46 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// campusResource is the top-level structure when parsing the JSON output -// from the API. This structure is not used if the Campus JSON object is -// included as a field in another JSON object. This structure is used only if -// the proper namespace is queried. -type campusResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []Campus `json:"data"` -} - // Campus is the representation of a site where facilities are. type Campus struct { - ID int `json:"id"` - OrganizationID int `json:"org_id"` - OrganizationName string `json:"org_name"` - Organization Organization `json:"organization,omitempty"` - Name string `json:"name"` - NameLong string `json:"name_long"` - AKA string `json:"aka"` - Website string `json:"website"` - Notes string `json:"notes"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Status string `json:"status"` - City string `json:"city"` - Country string `json:"country"` - State string `json:"state"` - Zipcode string `json:"zipcode"` - FacilitySet []int `json:"fac_set"` + ID int `json:"id"` + OrganizationID int `json:"org_id"` + OrganizationName string `json:"org_name"` + Organization Organization `json:"organization,omitempty"` + Name string `json:"name"` + NameLong string `json:"name_long"` + AKA string `json:"aka"` + Website string `json:"website"` + Notes string `json:"notes"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Status string `json:"status"` + City string `json:"city"` + Country string `json:"country"` + State string `json:"state"` + Zipcode string `json:"zipcode"` + FacilitySet []int `json:"fac_set"` SocialMedia []SocialMedia `json:"social_media"` } -// getCampusResource returns a pointer to a campusResource structure -// corresponding to the API JSON response. An error can be returned if -// something went wrong. -func (api *API) getCampusResource(search map[string]interface{}) (*campusResource, error) { - // Get the CampusResource from the API - response, err := api.lookup(campusNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &campusResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetCampus returns a pointer to a slice of Campus structures that the -// PeeringDB API can provide matching the given search parameters map. If an -// error occurs, the returned error will be non-nil. The returned value can be -// nil if no object could be found. -func (api *API) GetCampus(search map[string]interface{}) (*[]Campus, error) { - // Ask for the all Campus objects - campusResource, err := api.getCampusResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all Campus objects, will be nil if slice is empty - return &campusResource.Data, nil +// GetCampus returns a slice of Campus structures matching the given search +// parameters. +func (api *API) GetCampus(ctx context.Context, search url.Values) ([]Campus, error) { + return fetch[Campus](ctx, api, campusNamespace, search) } -// GetAllCampuses returns a pointer to a slice of Campus structures that the -// PeeringDB API can provide. If an error occurs, the returned error will be -// non-nil. The can be nil if no object could be found. -func (api *API) GetAllCampuses() (*[]Campus, error) { - // Return all Campus objects - return api.GetCampus(nil) +// GetAllCampuses returns all Campus structures available from the API. +func (api *API) GetAllCampuses(ctx context.Context) ([]Campus, error) { + return api.GetCampus(ctx, nil) } -// GetCampusByID returns a pointer to a Campus structure that matches the -// given ID. If the ID is lesser than 0, it will return nil. The returned -// error will be non-nil if an issue as occurred while trying to query the -// API. If for some reasons the API returns more than one object for the -// given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetCampusByID(id int) (*Campus, error) { - // No point of looking for the campus with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the Campus given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - campuses, err := api.GetCampus(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No Campus matching the ID - if len(*campuses) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*campuses)[0], nil +// GetCampusByID returns the Campus matching the given ID, or nil if not +// found. +func (api *API) GetCampusByID(ctx context.Context, id int) (*Campus, error) { + return fetchByID[Campus](ctx, api, campusNamespace, id) } diff --git a/carrier.go b/carrier.go index 9a5e34e..d415c13 100644 --- a/carrier.go +++ b/carrier.go @@ -1,135 +1,48 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// carrierResource is the top-level structure when parsing the JSON output -// from the API. This structure is not used if the Carrier JSON object is -// included as a field in another JSON object. This structure is used only if -// the proper namespace is queried. -type carrierResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []Carrier `json:"data"` -} - -// Carrier is the representation of a network able to provider transport from +// Carrier is the representation of a network able to provide transport from // one facility to another. type Carrier struct { - ID int `json:"id"` - OrganizationID int `json:"org_id"` - OrganizationName string `json:"org_name"` - Organization Organization `json:"organization,omitempty"` - Name string `json:"name"` - AKA string `json:"aka"` - NameLong string `json:"name_long"` - Website string `json:"website"` - Notes string `json:"notes"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Status string `json:"status"` + ID int `json:"id"` + OrganizationID int `json:"org_id"` + OrganizationName string `json:"org_name"` + Organization Organization `json:"organization,omitempty"` + Name string `json:"name"` + AKA string `json:"aka"` + NameLong string `json:"name_long"` + Website string `json:"website"` + Notes string `json:"notes"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Status string `json:"status"` SocialMedia []SocialMedia `json:"social_media"` } -// getCarrierResource returns a pointer to a carrierResource structure -// corresponding to the API JSON response. An error can be returned if -// something went wrong. -func (api *API) getCarrierResource(search map[string]interface{}) (*carrierResource, error) { - // Get the CarrierResource from the API - response, err := api.lookup(carrierNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &carrierResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetCarrier returns a pointer to a slice of Carrier structures that the -// PeeringDB API can provide matching the given search parameters map. If an -// error occurs, the returned error will be non-nil. The returned value can be -// nil if no object could be found. -func (api *API) GetCarrier(search map[string]interface{}) (*[]Carrier, error) { - // Ask for the all Carrier objects - carrierResource, err := api.getCarrierResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all Carrier objects, will be nil if slice is empty - return &carrierResource.Data, nil -} - -// GetAllCarriers returns a pointer to a slice of Carrier structures that the -// PeeringDB API can provide. If an error occurs, the returned error will be -// non-nil. The can be nil if no object could be found. -func (api *API) GetAllCarriers() (*[]Carrier, error) { - // Return all Carrier objects - return api.GetCarrier(nil) +// GetCarrier returns a slice of Carrier structures matching the given search +// parameters. +func (api *API) GetCarrier(ctx context.Context, search url.Values) ([]Carrier, error) { + return fetch[Carrier](ctx, api, carrierNamespace, search) } -// GetCarrierByID returns a pointer to a Carrier structure that matches the -// given ID. If the ID is lesser than 0, it will return nil. The returned -// error will be non-nil if an issue as occurred while trying to query the -// API. If for some reasons the API returns more than one object for the -// given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetCarrierByID(id int) (*Carrier, error) { - // No point of looking for the carrier with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the Carrier given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - carriers, err := api.GetCarrier(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No Carrier matching the ID - if len(*carriers) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*carriers)[0], nil +// GetAllCarriers returns all Carrier structures available from the API. +func (api *API) GetAllCarriers(ctx context.Context) ([]Carrier, error) { + return api.GetCarrier(ctx, nil) } -// carrierFacilityResource is the top-level structure when parsing the JSON -// output from the API. This structure is not used if the CarrierFacility JSON -// object is included as a field in another JSON object. This structure is -// used only if the proper namespace is queried. -type carrierFacilityResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []CarrierFacility `json:"data"` +// GetCarrierByID returns the Carrier matching the given ID, or nil if not +// found. +func (api *API) GetCarrierByID(ctx context.Context, id int) (*Carrier, error) { + return fetchByID[Carrier](ctx, api, carrierNamespace, id) } -// CarrierFacility is a structure used to link an Carrier structure with a -// Facility structure. It helps to know in which facilities a carrier -// operates. +// CarrierFacility links a Carrier with a Facility, indicating in which +// facilities a carrier operates. type CarrierFacility struct { ID int `json:"id"` Name string `json:"name"` @@ -142,85 +55,20 @@ type CarrierFacility struct { Status string `json:"status"` } -// getCarrierFacilityResource returns a pointer to an carrierFacilityResource -// structure corresponding to the API JSON response. An error can be returned -// if something went wrong. -func (api *API) getCarrierFacilityResource(search map[string]interface{}) (*carrierFacilityResource, error) { - // Get the CarrierFacilityResource from the API - response, err := api.lookup(carrierFacilityNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &carrierFacilityResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetCarrierFacility returns a pointer to a slice of CarrierFacility -// structures that the PeeringDB API can provide matching the given search -// parameters map. If an error occurs, the returned error will be non-nil. The -// returned value can be nil if no object could be found. -func (api *API) GetCarrierFacility(search map[string]interface{}) (*[]CarrierFacility, error) { - // Ask for the all CarrierFacility objects - carrierFacilityResource, err := api.getCarrierFacilityResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all InternetExchangeFacility objects, will be nil if slice is - // empty - return &carrierFacilityResource.Data, nil +// GetCarrierFacility returns a slice of CarrierFacility structures matching +// the given search parameters. +func (api *API) GetCarrierFacility(ctx context.Context, search url.Values) ([]CarrierFacility, error) { + return fetch[CarrierFacility](ctx, api, carrierFacilityNamespace, search) } -// GetAllCarrierFacilities returns a pointer to a slice of CarrierFacility -// structures that the PeeringDB API can provide. If an error occurs, the -// returned error will be non-nil. The can be nil if no object could be found. -func (api *API) GetAllCarrierFacilities() (*[]CarrierFacility, error) { - // Return all CarrierFacility objects - return api.GetCarrierFacility(nil) +// GetAllCarrierFacilities returns all CarrierFacility structures available +// from the API. +func (api *API) GetAllCarrierFacilities(ctx context.Context) ([]CarrierFacility, error) { + return api.GetCarrierFacility(ctx, nil) } -// GetCarrierFacilityByID returns a pointer to a CarrierFacility structure -// that matches the given ID. If the ID is lesser than 0, it will return nil. -// The returned error will be non-nil if an issue as occurred while trying to -// query the API. If for some reasons the API returns more than one object for -// the given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetCarrierFacilityByID(id int) (*CarrierFacility, error) { - // No point of looking for the carrier facility with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the CarrierFacility given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - carrierFacilities, err := api.GetCarrierFacility(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No CarrierFacility matching the ID - if len(*carrierFacilities) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*carrierFacilities)[0], nil +// GetCarrierFacilityByID returns the CarrierFacility matching the given ID, +// or nil if not found. +func (api *API) GetCarrierFacilityByID(ctx context.Context, id int) (*CarrierFacility, error) { + return fetchByID[CarrierFacility](ctx, api, carrierFacilityNamespace, id) } diff --git a/contact.go b/contact.go index 3812ffe..ec7952f 100644 --- a/contact.go +++ b/contact.go @@ -1,21 +1,11 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// networkContactResource is the top-level structure when parsing the JSON -// output from the API. This structure is not used if the NetworkContact JSON -// object is included as a field in another JSON object. This structure is used -// only if the proper namespace is queried. -type networkContactResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []NetworkContact `json:"data"` -} - // NetworkContact represents a contact for a network. type NetworkContact struct { ID int `json:"id"` @@ -32,84 +22,20 @@ type NetworkContact struct { Status string `json:"status"` } -// getNetworkContactResource returns a pointer to an networkContactResource -// structure corresponding to the API JSON response. An error can be returned -// if something went wrong. -func (api *API) getNetworkContactResource(search map[string]interface{}) (*networkContactResource, error) { - // Get the NetworkContactResource from the API - response, err := api.lookup(networkContactNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &networkContactResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetNetworkContact returns a pointer to a slice of NetworkContact structures -// that the PeeringDB API can provide matching the given search parameters map. -// If an error occurs, the returned error will be non-nil. The returned value -// can be nil if no object could be found. -func (api *API) GetNetworkContact(search map[string]interface{}) (*[]NetworkContact, error) { - // Ask for the all NetworkContact objects - networkContactResource, err := api.getNetworkContactResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all NetworkContact objects, will be nil if slice is empty - return &networkContactResource.Data, nil +// GetNetworkContact returns a slice of NetworkContact structures matching the +// given search parameters. +func (api *API) GetNetworkContact(ctx context.Context, search url.Values) ([]NetworkContact, error) { + return fetch[NetworkContact](ctx, api, networkContactNamespace, search) } -// GetAllNetworkContacts returns a pointer to a slice of NetworkContact -// structures that the PeeringDB API can provide. If an error occurs, the -// returned error will be non-nil. The can be nil if no object could be found. -func (api *API) GetAllNetworkContacts() (*[]NetworkContact, error) { - // Return all NetworkContact objects - return api.GetNetworkContact(nil) +// GetAllNetworkContacts returns all NetworkContact structures available from +// the API. +func (api *API) GetAllNetworkContacts(ctx context.Context) ([]NetworkContact, error) { + return api.GetNetworkContact(ctx, nil) } -// GetNetworkContactByID returns a pointer to a NetworkContact structure that -// matches the given ID. If the ID is lesser than 0, it will return nil. The -// returned error will be non-nil if an issue as occurred while trying to query -// the API. If for some reasons the API returns more than one object for the -// given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetNetworkContactByID(id int) (*NetworkContact, error) { - // No point of looking for the network contact with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the NetworkContact given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - networkContacts, err := api.GetNetworkContact(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No NetworkContact matching the ID - if len(*networkContacts) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*networkContacts)[0], nil +// GetNetworkContactByID returns the NetworkContact matching the given ID, or +// nil if not found. +func (api *API) GetNetworkContactByID(ctx context.Context, id int) (*NetworkContact, error) { + return fetchByID[NetworkContact](ctx, api, networkContactNamespace, id) } diff --git a/doc.go b/doc.go index 9f43076..3f3c06e 100644 --- a/doc.go +++ b/doc.go @@ -8,23 +8,10 @@ JSON. This package queries the API with the correct URL and parameters, parses the JSON response, and converts it into Go structures. Currently, this package only supports GET requests and cannot be used to modify any PeeringDB records. -There are two levels of structures in this package. The first level represents -the top level of the JSON returned by the API. These structures are named -*Resource. They all have a Meta field containing metadata returned by the API, -and a Data field which is an array of the second level structures. - All calls to the PeeringDB API use the "depth=1" parameter. This means that sets are expanded as integer slices instead of slices of structures, which speeds up the API processing time. To get the structures for a given set, you just need to iterate over the set and call the appropriate function to retrieve structures from IDs. - -For example, when requesting one or more objects from the PeeringDB API, the -response is always formatted in the same way: first comes the metadata, then -the data. The data is always in an array since it might contain more than one -object. When asking the API for a network object (called Net and represented by -the struct of the same name), this package parses the first level as a -NetResource structure. This structure contains metadata in its Meta field (if -there is any) and Net structures in the Data field (as an array). */ package peeringdb diff --git a/examples_test.go b/examples_test.go index 0f3614d..207727e 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,58 +1,53 @@ package peeringdb -import "fmt" +import ( + "context" + "fmt" + "net/url" +) func Example() { api := NewAPI() + ctx := context.Background() // Look for the organization given a name - search := make(map[string]interface{}) - search["name"] = "Guillaume Mazoyer" + search := url.Values{} + search.Set("name", "Guillaume Mazoyer") - // Get the organization, pointer to slice returned - organizations, err := api.GetOrganization(search) - - // If an error as occurred, print it + // Get the organization + organizations, err := api.GetOrganization(ctx, search) if err != nil { fmt.Println(err) return } - // No organization found - if len(*organizations) < 1 { - fmt.Printf("No organization found with name '%s'\n", search["name"]) + if len(organizations) < 1 { + fmt.Printf("No organization found with name '%s'\n", search.Get("name")) return } - // Several organizations found - if len(*organizations) > 1 { + if len(organizations) > 1 { fmt.Printf("More than one organizations found with name '%s'\n", - search["name"]) + search.Get("name")) return } - // Get the first found organization - org := (*organizations)[0] + org := organizations[0] // Find if there are networks linked to the organization - if len(org.NetworkSet) > 0 { - // For each network - for _, networkID := range org.NetworkSet { - // Get the details and print it - network, err := api.GetNetworkByID(networkID) - if err != nil { - fmt.Println(err) - } else { - fmt.Print(network.Name) - } + for _, networkID := range org.NetworkSet { + network, err := api.GetNetworkByID(ctx, networkID) + if err != nil { + fmt.Println(err) + } else { + fmt.Print(network.Name) } } - // Output: Guillaume Mazoyer } func ExampleAPI_GetASN() { api := NewAPI() - as201281, err := api.GetASN(201281) + as201281, err := api.GetASN(context.Background(), 201281) if err != nil { fmt.Println(err.Error()) @@ -61,7 +56,4 @@ func ExampleAPI_GetASN() { fmt.Printf("Name: %s\n", as201281.Name) fmt.Printf("ASN: %d\n", as201281.ASN) - // Output: - // Name: Guillaume Mazoyer - // ASN: 201281 } diff --git a/facility.go b/facility.go index 1580856..9db871d 100644 --- a/facility.go +++ b/facility.go @@ -1,144 +1,70 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// facilityResource is the top-level structure when parsing the JSON output -// from the API. This structure is not used if the Facility JSON object is -// included as a field in another JSON object. This structure is used only if -// the proper namespace is queried. -type facilityResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []Facility `json:"data"` -} - // Facility is the representation of a location where network operators and // Internet exchange points are located. Most of the time you know a facility // as a datacenter. type Facility struct { - ID int `json:"id"` - OrganizationID int `json:"org_id"` - OrganizationName string `json:"org_name"` - Organization Organization `json:"organization,omitempty"` - CampusID int `json:"campus_id"` - Campus Campus `json:"campus,omitempty"` - Name string `json:"name"` - AKA string `json:"aka"` - NameLong string `json:"name_long"` - Website string `json:"website"` - CLLI string `json:"clli"` - Rencode string `json:"rencode"` - Npanxx string `json:"npanxx"` - Notes string `json:"notes"` - NetCount int `json:"net_count"` - IXCount int `json:"ix_count"` - CarrierCount int `json:"carrier_count"` - SalesEmail string `json:"sales_email"` - SalesPhone string `json:"sales_phone"` - TechEmail string `json:"tech_email"` - TechPhone string `json:"tech_phone"` - AvailableVoltageServices []string `json:"available_voltage_services"` - DiverseServingSubstations bool `json:"diverse_serving_substations"` - Property string `json:"property"` - RegionContinent string `json:"region_continent"` - StatusDashboard string `json:"status_dashboard"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Status string `json:"status"` - Address1 string `json:"address1"` - Address2 string `json:"address2"` - City string `json:"city"` - Country string `json:"country"` - State string `json:"state"` - Zipcode string `json:"zipcode"` - Floor string `json:"floor"` - Suite string `json:"suite"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` + ID int `json:"id"` + OrganizationID int `json:"org_id"` + OrganizationName string `json:"org_name"` + Organization Organization `json:"organization,omitempty"` + CampusID int `json:"campus_id"` + Campus Campus `json:"campus,omitempty"` + Name string `json:"name"` + AKA string `json:"aka"` + NameLong string `json:"name_long"` + Website string `json:"website"` + CLLI string `json:"clli"` + Rencode string `json:"rencode"` + Npanxx string `json:"npanxx"` + Notes string `json:"notes"` + NetCount int `json:"net_count"` + IXCount int `json:"ix_count"` + CarrierCount int `json:"carrier_count"` + SalesEmail string `json:"sales_email"` + SalesPhone string `json:"sales_phone"` + TechEmail string `json:"tech_email"` + TechPhone string `json:"tech_phone"` + AvailableVoltageServices []string `json:"available_voltage_services"` + DiverseServingSubstations bool `json:"diverse_serving_substations"` + Property string `json:"property"` + RegionContinent string `json:"region_continent"` + StatusDashboard string `json:"status_dashboard"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Status string `json:"status"` + Address1 string `json:"address1"` + Address2 string `json:"address2"` + City string `json:"city"` + Country string `json:"country"` + State string `json:"state"` + Zipcode string `json:"zipcode"` + Floor string `json:"floor"` + Suite string `json:"suite"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` SocialMedia []SocialMedia `json:"social_media"` } -// getFacilityResource returns a pointer to a facilityResource structure -// corresponding to the API JSON response. An error can be returned if -// something went wrong. -func (api *API) getFacilityResource(search map[string]interface{}) (*facilityResource, error) { - // Get the FacilityResource from the API - response, err := api.lookup(facilityNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &facilityResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetFacility returns a pointer to a slice of Facility structures that the -// PeeringDB API can provide matching the given search parameters map. If an -// error occurs, the returned error will be non-nil. The returned value can be -// nil if no object could be found. -func (api *API) GetFacility(search map[string]interface{}) (*[]Facility, error) { - // Ask for the all Facility objects - facilyResource, err := api.getFacilityResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all Facility objects, will be nil if slice is empty - return &facilyResource.Data, nil +// GetFacility returns a slice of Facility structures matching the given +// search parameters. +func (api *API) GetFacility(ctx context.Context, search url.Values) ([]Facility, error) { + return fetch[Facility](ctx, api, facilityNamespace, search) } -// GetAllFacilities returns a pointer to a slice of Facility structures that -// the PeeringDB API can provide. If an error occurs, the returned error will -// be non-nil. The can be nil if no object could be found. -func (api *API) GetAllFacilities() (*[]Facility, error) { - // Return all Facility objects - return api.GetFacility(nil) +// GetAllFacilities returns all Facility structures available from the API. +func (api *API) GetAllFacilities(ctx context.Context) ([]Facility, error) { + return api.GetFacility(ctx, nil) } -// GetFacilityByID returns a pointer to a Facility structure that matches the -// given ID. If the ID is lesser than 0, it will return nil. The returned error -// will be non-nil if an issue as occurred while trying to query the API. If for -// some reasons the API returns more than one object for the given ID (but it -// must not) only the first will be used for the returned value. -func (api *API) GetFacilityByID(id int) (*Facility, error) { - // No point of looking for the facility with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the Facility given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - facilities, err := api.GetFacility(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No Facility matching the ID - if len(*facilities) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*facilities)[0], nil +// GetFacilityByID returns the Facility matching the given ID, or nil if not +// found. +func (api *API) GetFacilityByID(ctx context.Context, id int) (*Facility, error) { + return fetchByID[Facility](ctx, api, facilityNamespace, id) } diff --git a/ix.go b/ix.go index 3ce01a1..db593bd 100644 --- a/ix.go +++ b/ix.go @@ -1,159 +1,73 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// internetExchangeResource is the top-level structure when parsing the JSON -// output from the API. This structure is not used if the InternetExchange JSON -// object is included as a field in another JSON object. This structure is used -// only if the proper namespace is queried. -type internetExchangeResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []InternetExchange `json:"data"` -} - -// InternetExchange is a structure representing an Internet exchange point. It -// is directly linked to the Organization that manage the IX. +// InternetExchange represents an Internet exchange point. It is directly +// linked to the Organization that manages the IX. type InternetExchange struct { - ID int `json:"id"` - OrganizationID int `json:"org_id"` - Organization Organization `json:"org,omitempty"` - Name string `json:"name"` - AKA string `json:"aka"` - NameLong string `json:"name_long"` - City string `json:"city"` - Country string `json:"country"` - RegionContinent string `json:"region_continent"` - Media string `json:"media"` - Notes string `json:"notes"` - ProtoUnicast bool `json:"proto_unicast"` - ProtoMulticast bool `json:"proto_multicast"` - ProtoIPv6 bool `json:"proto_ipv6"` - Website string `json:"website"` - URLStats string `json:"url_stats"` - TechEmail string `json:"tech_email"` - TechPhone string `json:"tech_phone"` - PolicyEmail string `json:"policy_email"` - PolicyPhone string `json:"policy_phone"` - SalesPhone string `json:"sales_phone"` - SalesEmail string `json:"sales_email"` - FacilitySet []int `json:"fac_set"` - InternetExchangeLANSet []int `json:"ixlan_set"` - NetworkCount int `json:"net_count"` - FacilityCount int `json:"fac_count"` - IxfNetCount int `json:"ixf_net_count"` - IxfLastImport time.Time `json:"ixf_last_import"` - IxfImportRequest time.Time `json:"ixf_import_request"` - IxfImportRequestStatus string `json:"ixf_import_request_status"` - ServiceLevel string `json:"service_level"` - Terms string `json:"terms"` - StatusDashboard string `json:"status_dashboard"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Status string `json:"status"` + ID int `json:"id"` + OrganizationID int `json:"org_id"` + Organization Organization `json:"org,omitempty"` + Name string `json:"name"` + AKA string `json:"aka"` + NameLong string `json:"name_long"` + City string `json:"city"` + Country string `json:"country"` + RegionContinent string `json:"region_continent"` + Media string `json:"media"` + Notes string `json:"notes"` + ProtoUnicast bool `json:"proto_unicast"` + ProtoMulticast bool `json:"proto_multicast"` + ProtoIPv6 bool `json:"proto_ipv6"` + Website string `json:"website"` + URLStats string `json:"url_stats"` + TechEmail string `json:"tech_email"` + TechPhone string `json:"tech_phone"` + PolicyEmail string `json:"policy_email"` + PolicyPhone string `json:"policy_phone"` + SalesPhone string `json:"sales_phone"` + SalesEmail string `json:"sales_email"` + FacilitySet []int `json:"fac_set"` + InternetExchangeLANSet []int `json:"ixlan_set"` + NetworkCount int `json:"net_count"` + FacilityCount int `json:"fac_count"` + IxfNetCount int `json:"ixf_net_count"` + IxfLastImport time.Time `json:"ixf_last_import"` + IxfImportRequest time.Time `json:"ixf_import_request"` + IxfImportRequestStatus string `json:"ixf_import_request_status"` + ServiceLevel string `json:"service_level"` + Terms string `json:"terms"` + StatusDashboard string `json:"status_dashboard"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Status string `json:"status"` SocialMedia []SocialMedia `json:"social_media"` } -// getInternetExchangeResource returns a pointer to an internetExchangeResource -// structure corresponding to the API JSON response. An error can be returned -// if something went wrong. -func (api *API) getInternetExchangeResource(search map[string]interface{}) (*internetExchangeResource, error) { - // Get the InternetExchangeResource from the API - response, err := api.lookup(internetExchangeNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &internetExchangeResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetInternetExchange returns a pointer to a slice of InternetExchange -// structures that the PeeringDB API can provide matching the given search -// parameters map. If an error occurs, the returned error will be non-nil. The -// returned value can be nil if no object could be found. -func (api *API) GetInternetExchange(search map[string]interface{}) (*[]InternetExchange, error) { - // Ask for the all InternetExchange objects - internetExchangeResource, err := api.getInternetExchangeResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all InternetExchange objects, will be nil if slice is empty - return &internetExchangeResource.Data, nil -} - -// GetAllInternetExchanges returns a pointer to a slice of InternetExchange -// structures that the PeeringDB API can provide. If an error occurs, the -// returned error will be non-nil. The can be nil if no object could be found. -func (api *API) GetAllInternetExchanges() (*[]InternetExchange, error) { - // Return all InternetExchange objects - return api.GetInternetExchange(nil) +// GetInternetExchange returns a slice of InternetExchange structures matching +// the given search parameters. +func (api *API) GetInternetExchange(ctx context.Context, search url.Values) ([]InternetExchange, error) { + return fetch[InternetExchange](ctx, api, internetExchangeNamespace, search) } -// GetInternetExchangeByID returns a pointer to a InternetExchange structure -// that matches the given ID. If the ID is lesser than 0, it will return nil. -// The returned error will be non-nil if an issue as occurred while trying to -// query the API. If for some reasons the API returns more than one object for -// the given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetInternetExchangeByID(id int) (*InternetExchange, error) { - // No point of looking for the Internet exchange with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the InternetExchange given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - internetExchanges, err := api.GetInternetExchange(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No InternetExchange matching the ID - if len(*internetExchanges) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*internetExchanges)[0], nil +// GetAllInternetExchanges returns all InternetExchange structures available +// from the API. +func (api *API) GetAllInternetExchanges(ctx context.Context) ([]InternetExchange, error) { + return api.GetInternetExchange(ctx, nil) } -// internetExchangeLANResource is the top-level structure when parsing the JSON -// output from the API. This structure is not used if the InternetExchangeLAN -// JSON object is included as a field in another JSON object. This structure is -// used only if the proper namespace is queried. -type internetExchangeLANResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []InternetExchangeLAN `json:"data"` +// GetInternetExchangeByID returns the InternetExchange matching the given ID, +// or nil if not found. +func (api *API) GetInternetExchangeByID(ctx context.Context, id int) (*InternetExchange, error) { + return fetchByID[InternetExchange](ctx, api, internetExchangeNamespace, id) } -// InternetExchangeLAN is a structure representing the one of the network (LAN) -// of an Internet exchange points. It contains details about the LAN like the -// MTU, VLAN support, etc. +// InternetExchangeLAN represents one of the networks (LANs) of an Internet +// exchange point, with details like MTU, VLAN support, etc. type InternetExchangeLAN struct { ID int `json:"id"` InternetExchangeID int `json:"ix_id"` @@ -174,102 +88,26 @@ type InternetExchangeLAN struct { Status string `json:"status"` } -// getInternetExchangeLANResource returns a pointer to an -// internetExchangeLANResource structure corresponding to the API JSON -// response. An error can be returned if something went wrong. -func (api *API) getInternetExchangeLANResource(search map[string]interface{}) (*internetExchangeLANResource, error) { - // Get the InternetExchangeLANResource from the API - response, err := api.lookup(internetExchangeLANNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &internetExchangeLANResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetInternetExchangeLAN returns a pointer to a slice of InternetExchangeLAN -// structures that the PeeringDB API can provide matching the given search -// parameters map. If an error occurs, the returned error will be non-nil. The -// returned value can be nil if no object could be found. -func (api *API) GetInternetExchangeLAN(search map[string]interface{}) (*[]InternetExchangeLAN, error) { - // Ask for the all InternetExchangeLAN objects - internetExchangeLANResource, err := api.getInternetExchangeLANResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all InternetExchangeLAN objects, will be nil if slice is empty - return &internetExchangeLANResource.Data, nil -} - -// GetAllInternetExchangeLANs returns a pointer to a slice of -// InternetExchangeLAN structures that the PeeringDB API can provide. If an -// error occurs, the returned error will be non-nil. The can be nil if no -// object could be found. -func (api *API) GetAllInternetExchangeLANs() (*[]InternetExchangeLAN, error) { - // Return all InternetExchangeLAN objects - return api.GetInternetExchangeLAN(nil) +// GetInternetExchangeLAN returns a slice of InternetExchangeLAN structures +// matching the given search parameters. +func (api *API) GetInternetExchangeLAN(ctx context.Context, search url.Values) ([]InternetExchangeLAN, error) { + return fetch[InternetExchangeLAN](ctx, api, internetExchangeLANNamespace, search) } -// GetInternetExchangeLANByID returns a pointer to a InternetExchangeLAN -// structure that matches the given ID. If the ID is lesser than 0, it will -// return nil. The returned error will be non-nil if an issue as occurred while -// trying to query the API. If for some reasons the API returns more than one -// object for the given ID (but it must not) only the first will be used for -// the returned value. -func (api *API) GetInternetExchangeLANByID(id int) (*InternetExchangeLAN, error) { - // No point of looking for the Internet exchange LAN with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the InternetExchangeLAN given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - ixLANs, err := api.GetInternetExchangeLAN(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No InternetExchangeLAN matching the ID - if len(*ixLANs) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*ixLANs)[0], nil +// GetAllInternetExchangeLANs returns all InternetExchangeLAN structures +// available from the API. +func (api *API) GetAllInternetExchangeLANs(ctx context.Context) ([]InternetExchangeLAN, error) { + return api.GetInternetExchangeLAN(ctx, nil) } -// internetExchangePrefixResource is the top-level structure when parsing the -// JSON output from the API. This structure is not used if the -// InternetExchangePrefix JSON object is included as a field in another JSON -// object. This structure is used only if the proper namespace is queried. -type internetExchangePrefixResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []InternetExchangePrefix `json:"data"` +// GetInternetExchangeLANByID returns the InternetExchangeLAN matching the +// given ID, or nil if not found. +func (api *API) GetInternetExchangeLANByID(ctx context.Context, id int) (*InternetExchangeLAN, error) { + return fetchByID[InternetExchangeLAN](ctx, api, internetExchangeLANNamespace, id) } -// InternetExchangePrefix is a structure representing the prefix used by an -// Internet exchange point. It is directly linked to an InternetExchangeLAN. +// InternetExchangePrefix represents a prefix used by an Internet exchange +// point, directly linked to an InternetExchangeLAN. type InternetExchangePrefix struct { ID int `json:"id"` InternetExchangeLANID int `json:"ixlan_id"` @@ -282,105 +120,26 @@ type InternetExchangePrefix struct { Status string `json:"status"` } -// getInternetExchangePrefixResource returns a pointer to an -// internetExchangePrefixResource structure corresponding to the API JSON -// response. An error can be returned if something went wrong. -func (api *API) getInternetExchangePrefixResource(search map[string]interface{}) (*internetExchangePrefixResource, error) { - // Get the InternetExchangePrefixResource from the API - response, err := api.lookup(internetExchangePrefixNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &internetExchangePrefixResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetInternetExchangePrefix returns a pointer to a slice of -// InternetExchangePrefix structures that the PeeringDB API can provide -// matching the given search parameters map. If an error occurs, the returned -// error will be non-nil. The returned value can be nil if no object could be -// found. -func (api *API) GetInternetExchangePrefix(search map[string]interface{}) (*[]InternetExchangePrefix, error) { - // Ask for the all InternetExchangePrefix objects - internetExchangePrefixResource, err := api.getInternetExchangePrefixResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all InternetExchangePrefix objects, will be nil if slice is empty - return &internetExchangePrefixResource.Data, nil -} - -// GetAllInternetExchangePrefixes returns a pointer to a slice of -// InternetExchangePrefix structures that the PeeringDB API can provide. If an -// error occurs, the returned error will be non-nil. The can be nil if no -// object could be found. -func (api *API) GetAllInternetExchangePrefixes() (*[]InternetExchangePrefix, error) { - // Return all InternetExchangePrefix objects - return api.GetInternetExchangePrefix(nil) +// GetInternetExchangePrefix returns a slice of InternetExchangePrefix +// structures matching the given search parameters. +func (api *API) GetInternetExchangePrefix(ctx context.Context, search url.Values) ([]InternetExchangePrefix, error) { + return fetch[InternetExchangePrefix](ctx, api, internetExchangePrefixNamespace, search) } -// GetInternetExchangePrefixByID returns a pointer to a InternetExchangePrefix -// structure that matches the given ID. If the ID is lesser than 0, it will -// return nil. The returned error will be non-nil if an issue as occurred while -// trying to query the API. If for some reasons the API returns more than one -// object for the given ID (but it must not) only the first will be used for -// the returned value. -func (api *API) GetInternetExchangePrefixByID(id int) (*InternetExchangePrefix, error) { - // No point of looking for the Internet exchange prefix with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the InternetExchangePrefix given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - ixPrefixes, err := api.GetInternetExchangePrefix(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No InternetExchangePrefix matching the ID - if len(*ixPrefixes) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*ixPrefixes)[0], nil +// GetAllInternetExchangePrefixes returns all InternetExchangePrefix structures +// available from the API. +func (api *API) GetAllInternetExchangePrefixes(ctx context.Context) ([]InternetExchangePrefix, error) { + return api.GetInternetExchangePrefix(ctx, nil) } -// internetExchangeFacilityResource is the top-level structure when parsing the -// JSON output from the API. This structure is not used if the -// InternetExchangeFacility JSON object is included as a field in another JSON -// object. This structure is used only if the proper namespace is queried. -type internetExchangeFacilityResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []InternetExchangeFacility `json:"data"` +// GetInternetExchangePrefixByID returns the InternetExchangePrefix matching +// the given ID, or nil if not found. +func (api *API) GetInternetExchangePrefixByID(ctx context.Context, id int) (*InternetExchangePrefix, error) { + return fetchByID[InternetExchangePrefix](ctx, api, internetExchangePrefixNamespace, id) } -// InternetExchangeFacility is a structure used to link an InternetExchange -// structure with a Facility structure. It helps to know where an Internet -// exchange points can be found, or what Internet exchange points can be found -// in a given facility. +// InternetExchangeFacility links an InternetExchange with a Facility, +// indicating where an IX can be found or what IXs are in a given facility. type InternetExchangeFacility struct { ID int `json:"id"` Name string `json:"name"` @@ -395,87 +154,20 @@ type InternetExchangeFacility struct { Status string `json:"status"` } -// getInternetExchangeFacilityResource returns a pointer to an -// internetExchangeFacilityResource structure corresponding to the API JSON -// response. An error can be returned if something went wrong. -func (api *API) getInternetExchangeFacilityResource(search map[string]interface{}) (*internetExchangeFacilityResource, error) { - // Get the InternetExchangeFacilityResource from the API - response, err := api.lookup(internetExchangeFacilityNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &internetExchangeFacilityResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetInternetExchangeFacility returns a pointer to a slice of -// InternetExchangeFacility structures that the PeeringDB API can provide -// matching the given search parameters map. If an error occurs, the returned -// error will be non-nil. The returned value can be nil if no object could be -// found. -func (api *API) GetInternetExchangeFacility(search map[string]interface{}) (*[]InternetExchangeFacility, error) { - // Ask for the all InternetExchangeFacility objects - internetExchangeFacilityResource, err := api.getInternetExchangeFacilityResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all InternetExchangeFacility objects, will be nil if slice is - // empty - return &internetExchangeFacilityResource.Data, nil +// GetInternetExchangeFacility returns a slice of InternetExchangeFacility +// structures matching the given search parameters. +func (api *API) GetInternetExchangeFacility(ctx context.Context, search url.Values) ([]InternetExchangeFacility, error) { + return fetch[InternetExchangeFacility](ctx, api, internetExchangeFacilityNamespace, search) } -// GetAllInternetExchangeFacilities returns a pointer to a slice of -// InternetExchangeFacility structures that the PeeringDB API can provide. If -// an error occurs, the returned error will be non-nil. The can be nil if no -// object could be found. -func (api *API) GetAllInternetExchangeFacilities() (*[]InternetExchangeFacility, error) { - // Return all InternetExchangeFacility objects - return api.GetInternetExchangeFacility(nil) +// GetAllInternetExchangeFacilities returns all InternetExchangeFacility +// structures available from the API. +func (api *API) GetAllInternetExchangeFacilities(ctx context.Context) ([]InternetExchangeFacility, error) { + return api.GetInternetExchangeFacility(ctx, nil) } -// GetInternetExchangeFacilityByID returns a pointer to a -// InternetExchangeFacility structure that matches the given ID. If the ID is -// lesser than 0, it will return nil. The returned error will be non-nil if an -// issue as occurred while trying to query the API. If for some reasons the API -// returns more than one object for the given ID (but it must not) only the -// first will be used for the returned value. -func (api *API) GetInternetExchangeFacilityByID(id int) (*InternetExchangeFacility, error) { - // No point of looking for the Internet exchange facility with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the InternetExchangeFacility given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - ixFacilities, err := api.GetInternetExchangeFacility(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No InternetExchangeFacility matching the ID - if len(*ixFacilities) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*ixFacilities)[0], nil +// GetInternetExchangeFacilityByID returns the InternetExchangeFacility +// matching the given ID, or nil if not found. +func (api *API) GetInternetExchangeFacilityByID(ctx context.Context, id int) (*InternetExchangeFacility, error) { + return fetchByID[InternetExchangeFacility](ctx, api, internetExchangeFacilityNamespace, id) } diff --git a/net.go b/net.go index 2236a03..a47d490 100644 --- a/net.go +++ b/net.go @@ -1,168 +1,81 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// networkResource is the top-level structure when parsing the JSON output from -// the API. This structure is not used if the Network JSON object is included -// as a field in another JSON object. This structure is used only if the proper -// namespace is queried. -type networkResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []Network `json:"data"` -} - -// Network is a structure representing a network. Basically, a network is an -// Autonomous System identified by an AS number and other details. It belongs -// to an Organization, contains one or more NetworkContact, and is part of -// several Facility and InternetExchangeLAN. +// Network represents an Autonomous System identified by an AS number and other +// details. It belongs to an Organization, contains one or more +// NetworkContact, and is part of several Facility and InternetExchangeLAN. type Network struct { - ID int `json:"id"` - OrganizationID int `json:"org_id"` - Organization Organization `json:"org,omitempty"` - Name string `json:"name"` - AKA string `json:"aka"` - NameLong string `json:"name_long"` - Website string `json:"website"` - ASN int `json:"asn"` - LookingGlass string `json:"looking_glass"` - RouteServer string `json:"route_server"` - IRRASSet string `json:"irr_as_set"` - InfoType string `json:"info_type"` - InfoTypes []string `json:"info_types"` - InfoPrefixes4 int `json:"info_prefixes4"` - InfoPrefixes6 int `json:"info_prefixes6"` - InfoTraffic string `json:"info_traffic"` - InfoRatio string `json:"info_ratio"` - InfoScope string `json:"info_scope"` - InfoUnicast bool `json:"info_unicast"` - InfoMulticast bool `json:"info_multicast"` - InfoIPv6 bool `json:"info_ipv6"` - InfoNeverViaRouteServers bool `json:"info_never_via_route_servers"` - InternetExchangeCount int `json:"ix_count"` - FacilityCount int `json:"fac_count"` - Notes string `json:"notes"` - NetworkInternetExchangeLANUpdated time.Time `json:"netixlan_updated"` - NetworkFacilityUpdated time.Time `json:"netfac_updated"` - NetworkContactUpdated time.Time `json:"poc_updated"` - PolicyURL string `json:"policy_url"` - PolicyGeneral string `json:"policy_general"` - PolicyLocations string `json:"policy_locations"` - PolicyRatio bool `json:"policy_ratio"` - PolicyContracts string `json:"policy_contracts"` - NetworkFacilitySet []int `json:"netfac_set"` - NetworkInternetExchangeLANSet []int `json:"netixlan_set"` - NetworkContactSet []int `json:"poc_set"` - AllowIXPUpdate bool `json:"allow_ixp_update"` - StatusDashboard string `json:"status_dashboard"` - RIRStatus string `json:"rir_status"` - RIRStatusUpdated time.Time `json:"rir_status_updated"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Status string `json:"status"` + ID int `json:"id"` + OrganizationID int `json:"org_id"` + Organization Organization `json:"org,omitempty"` + Name string `json:"name"` + AKA string `json:"aka"` + NameLong string `json:"name_long"` + Website string `json:"website"` + ASN int `json:"asn"` + LookingGlass string `json:"looking_glass"` + RouteServer string `json:"route_server"` + IRRASSet string `json:"irr_as_set"` + InfoType string `json:"info_type"` + InfoTypes []string `json:"info_types"` + InfoPrefixes4 int `json:"info_prefixes4"` + InfoPrefixes6 int `json:"info_prefixes6"` + InfoTraffic string `json:"info_traffic"` + InfoRatio string `json:"info_ratio"` + InfoScope string `json:"info_scope"` + InfoUnicast bool `json:"info_unicast"` + InfoMulticast bool `json:"info_multicast"` + InfoIPv6 bool `json:"info_ipv6"` + InfoNeverViaRouteServers bool `json:"info_never_via_route_servers"` + InternetExchangeCount int `json:"ix_count"` + FacilityCount int `json:"fac_count"` + Notes string `json:"notes"` + NetworkInternetExchangeLANUpdated time.Time `json:"netixlan_updated"` + NetworkFacilityUpdated time.Time `json:"netfac_updated"` + NetworkContactUpdated time.Time `json:"poc_updated"` + PolicyURL string `json:"policy_url"` + PolicyGeneral string `json:"policy_general"` + PolicyLocations string `json:"policy_locations"` + PolicyRatio bool `json:"policy_ratio"` + PolicyContracts string `json:"policy_contracts"` + NetworkFacilitySet []int `json:"netfac_set"` + NetworkInternetExchangeLANSet []int `json:"netixlan_set"` + NetworkContactSet []int `json:"poc_set"` + AllowIXPUpdate bool `json:"allow_ixp_update"` + StatusDashboard string `json:"status_dashboard"` + RIRStatus string `json:"rir_status"` + RIRStatusUpdated time.Time `json:"rir_status_updated"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Status string `json:"status"` SocialMedia []SocialMedia `json:"social_media"` } -// getNetworkResource returns a pointer to an networkResource structure -// corresponding to the API JSON response. An error can be returned if -// something went wrong. -func (api *API) getNetworkResource(search map[string]interface{}) (*networkResource, error) { - // Get the NetworkResource from the API - response, err := api.lookup(networkNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &networkResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetNetwork returns a pointer to a slice of Network structures that the -// PeeringDB API can provide matching the given search parameters map. If an -// error occurs, the returned error will be non-nil. The returned value can be -// nil if no object could be found. -func (api *API) GetNetwork(search map[string]interface{}) (*[]Network, error) { - // Ask for the all Network objects - networkResource, err := api.getNetworkResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all Network objects, will be nil if slice is empty - return &networkResource.Data, nil -} - -// GetAllNetworks returns a pointer to a slice of Network structures that the -// PeeringDB API can provide. If an error occurs, the returned error will be -// non-nil. The can be nil if no object could be found. -func (api *API) GetAllNetworks() (*[]Network, error) { - // Return all Network objects - return api.GetNetwork(nil) +// GetNetwork returns a slice of Network structures matching the given search +// parameters. +func (api *API) GetNetwork(ctx context.Context, search url.Values) ([]Network, error) { + return fetch[Network](ctx, api, networkNamespace, search) } -// GetNetworkByID returns a pointer to a Network structure that matches the -// given ID. If the ID is lesser than 0, it will return nil. The returned error -// will be non-nil if an issue as occurred while trying to query the API. If for -// some reasons the API returns more than one object for the given ID (but it -// must not) only the first will be used for the returned value. -func (api *API) GetNetworkByID(id int) (*Network, error) { - // No point of looking for the network with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the Network given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - networks, err := api.GetNetwork(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No Network matching the ID - if len(*networks) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*networks)[0], nil +// GetAllNetworks returns all Network structures available from the API. +func (api *API) GetAllNetworks(ctx context.Context) ([]Network, error) { + return api.GetNetwork(ctx, nil) } -// networkFacilityResource is the top-level structure when parsing the JSON -// output from the API. This structure is not used if the NetFacility JSON -// object is included as a field in another JSON object. This structure is used -// only if the proper namespace is queried. -type networkFacilityResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []NetworkFacility `json:"data"` +// GetNetworkByID returns the Network matching the given ID, or nil if not +// found. +func (api *API) GetNetworkByID(ctx context.Context, id int) (*Network, error) { + return fetchByID[Network](ctx, api, networkNamespace, id) } -// NetworkFacility is a structure used to link a Network with a Facility. It -// helps to know where a network is located (it can be in several facilities). -// For example, it can be used to search common facilities between several -// networks to know where they can interconnect themselves directly. +// NetworkFacility links a Network with a Facility, indicating where a network +// is located. It can be used to search common facilities between several +// networks to know where they can interconnect directly. type NetworkFacility struct { ID int `json:"id"` Name string `json:"name"` @@ -178,102 +91,27 @@ type NetworkFacility struct { Status string `json:"status"` } -// getNetworkFacilityResource returns a pointer to an networkFacilityResource -// structure corresponding to the API JSON response. An error can be returned -// if something went wrong. -func (api *API) getNetworkFacilityResource(search map[string]interface{}) (*networkFacilityResource, error) { - // Get the NetworkFacilityResource from the API - response, err := api.lookup(networkFacilityNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &networkFacilityResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetNetworkFacility returns a pointer to a slice of NetworkFacility -// structures that the PeeringDB API can provide matching the given search -// parameters map. If an error occurs, the returned error will be non-nil. The -// returned value can be nil if no object could be found. -func (api *API) GetNetworkFacility(search map[string]interface{}) (*[]NetworkFacility, error) { - // Ask for the all NetworkFacility objects - networkFacilityResource, err := api.getNetworkFacilityResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all NetworkFacility objects, will be nil if slice is empty - return &networkFacilityResource.Data, nil +// GetNetworkFacility returns a slice of NetworkFacility structures matching +// the given search parameters. +func (api *API) GetNetworkFacility(ctx context.Context, search url.Values) ([]NetworkFacility, error) { + return fetch[NetworkFacility](ctx, api, networkFacilityNamespace, search) } -// GetAllNetworkFacilities returns a pointer to a slice of NetworkFacility -// structures that the PeeringDB API can provide. If an error occurs, the -// returned error will be non-nil. The can be nil if no object could be found. -func (api *API) GetAllNetworkFacilities() (*[]NetworkFacility, error) { - // Return all NetFacility objects - return api.GetNetworkFacility(nil) +// GetAllNetworkFacilities returns all NetworkFacility structures available +// from the API. +func (api *API) GetAllNetworkFacilities(ctx context.Context) ([]NetworkFacility, error) { + return api.GetNetworkFacility(ctx, nil) } -// GetNetworkFacilityByID returns a pointer to a NetworkFacility structure that -// matches the given ID. If the ID is lesser than 0, it will return nil. The -// returned error will be non-nil if an issue as occurred while trying to query -// the API. If for some reasons the API returns more than one object for the -// given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetNetworkFacilityByID(id int) (*NetworkFacility, error) { - // No point of looking for the network facility with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the NetworkFacility given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - networkFacilities, err := api.GetNetworkFacility(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No NetworkFacility matching the ID - if len(*networkFacilities) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*networkFacilities)[0], nil -} - -// networkInternetExchangeLANResource is the top-level structure when parsing -// the JSON output from the API. This structure is not used if the -// NetworkInternetExchangeLAN JSON object is included as a field in another -// JSON object. This structure is used only if the proper namespace is queried. -type networkInternetExchangeLANResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []NetworkInternetExchangeLAN `json:"data"` +// GetNetworkFacilityByID returns the NetworkFacility matching the given ID, +// or nil if not found. +func (api *API) GetNetworkFacilityByID(ctx context.Context, id int) (*NetworkFacility, error) { + return fetchByID[NetworkFacility](ctx, api, networkFacilityNamespace, id) } -// NetworkInternetExchangeLAN is a structure allowing to know to which -// InternetExchangeLAN a network is connected. It can be used, for example, to -// know what are the common Internet exchange LANs between several networks. +// NetworkInternetExchangeLAN represents a network's connection to an Internet +// exchange LAN. It can be used to find common IX LANs between several +// networks. type NetworkInternetExchangeLAN struct { ID int `json:"id"` NetworkID int `json:"net_id"` @@ -298,86 +136,20 @@ type NetworkInternetExchangeLAN struct { Status string `json:"status"` } -// getNetworkInternetExchangeLANResource returns a pointer to an -// networkInternetExchangeLANResource structure corresponding to the API JSON -// response. An error can be returned if something went wrong. -func (api *API) getNetworkInternetExchangeLANResource(search map[string]interface{}) (*networkInternetExchangeLANResource, error) { - // Get the NetworkInternetExchangeLANResource from the API - response, err := api.lookup(networkInternetExchangeLANNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &networkInternetExchangeLANResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetNetworkInternetExchangeLAN returns a pointer to a slice of -// NetworkInternetExchangeLAN structures that the PeeringDB API can provide -// matching the given search parameters map. If an error occurs, the returned -// error will be non-nil. The returned value can be nil if no object could be -// found. -func (api *API) GetNetworkInternetExchangeLAN(search map[string]interface{}) (*[]NetworkInternetExchangeLAN, error) { - // Ask for the all NetInternetExchangeLAN objects - networkInternetExchangeLANResource, err := api.getNetworkInternetExchangeLANResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all NetInternetExchangeLAN objects, will be nil if slice is empty - return &networkInternetExchangeLANResource.Data, nil +// GetNetworkInternetExchangeLAN returns a slice of +// NetworkInternetExchangeLAN structures matching the given search parameters. +func (api *API) GetNetworkInternetExchangeLAN(ctx context.Context, search url.Values) ([]NetworkInternetExchangeLAN, error) { + return fetch[NetworkInternetExchangeLAN](ctx, api, networkInternetExchangeLANNamespace, search) } -// GetAllNetworkInternetExchangeLANs returns a pointer to a slice of -// NetworkInternetExchangeLAN structures that the PeeringDB API can provide. If -// an error occurs, the returned error will be non-nil. The can be nil if no -// object could be found. -func (api *API) GetAllNetworkInternetExchangeLANs() (*[]NetworkInternetExchangeLAN, error) { - // Return all NetworkInternetExchangeLAN objects - return api.GetNetworkInternetExchangeLAN(nil) +// GetAllNetworkInternetExchangeLANs returns all NetworkInternetExchangeLAN +// structures available from the API. +func (api *API) GetAllNetworkInternetExchangeLANs(ctx context.Context) ([]NetworkInternetExchangeLAN, error) { + return api.GetNetworkInternetExchangeLAN(ctx, nil) } -// GetNetworkInternetExchangeLANByID returns a pointer to a -// NetworkInternetExchangeLAN structure that matches the given ID. If the ID is -// lesser than 0, it will return nil. The returned error will be non-nil if an -// issue as occurred while trying to query the API. If for some reasons the API -// returns more than one object for the given ID (but it must not) only the -// first will be used for the returned value. -func (api *API) GetNetworkInternetExchangeLANByID(id int) (*NetworkInternetExchangeLAN, error) { - // No point of looking for the Internet exchange LAN with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the NetworkInternetExchangeLAN given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - networkInternetExchangeLANs, err := api.GetNetworkInternetExchangeLAN(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No NetworkInternetExchangeLAN matching the ID - if len(*networkInternetExchangeLANs) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*networkInternetExchangeLANs)[0], nil +// GetNetworkInternetExchangeLANByID returns the NetworkInternetExchangeLAN +// matching the given ID, or nil if not found. +func (api *API) GetNetworkInternetExchangeLANByID(ctx context.Context, id int) (*NetworkInternetExchangeLAN, error) { + return fetchByID[NetworkInternetExchangeLAN](ctx, api, networkInternetExchangeLANNamespace, id) } diff --git a/organization.go b/organization.go index 0657a23..cdb72e6 100644 --- a/organization.go +++ b/organization.go @@ -1,131 +1,56 @@ package peeringdb import ( - "encoding/json" + "context" + "net/url" "time" ) -// organizationResource is the top-level structure when parsing the JSON output -// from the API. This structure is not used if the Organization JSON object is -// included as a field in another JSON object. This structure is used only if -// the proper namespace is queried. -type organizationResource struct { - Meta struct { - Generated float64 `json:"generated,omitempty"` - } `json:"meta"` - Data []Organization `json:"data"` -} - -// Organization is a structure representing an Organization. An organization -// can be seen as an enterprise linked to networks, facilities and internet -// exchange points. +// Organization represents an enterprise linked to networks, facilities, +// and internet exchange points. type Organization struct { - ID int `json:"id"` - Name string `json:"name"` - AKA string `json:"aka"` - NameLong string `json:"name_long"` - Website string `json:"website"` - Notes string `json:"notes"` - Require2FA bool `json:"require_2fa"` - NetworkSet []int `json:"net_set"` - FacilitySet []int `json:"fac_set"` - InternetExchangeSet []int `json:"ix_set"` - CarrierSet []int `json:"carrier_set"` - CampusSet []int `json:"campus_set"` - Address1 string `json:"address1"` - Address2 string `json:"address2"` - City string `json:"city"` - Country string `json:"country"` - State string `json:"state"` - Zipcode string `json:"zipcode"` - Floor string `json:"floor"` - Suite string `json:"suite"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Status string `json:"status"` + ID int `json:"id"` + Name string `json:"name"` + AKA string `json:"aka"` + NameLong string `json:"name_long"` + Website string `json:"website"` + Notes string `json:"notes"` + Require2FA bool `json:"require_2fa"` + NetworkSet []int `json:"net_set"` + FacilitySet []int `json:"fac_set"` + InternetExchangeSet []int `json:"ix_set"` + CarrierSet []int `json:"carrier_set"` + CampusSet []int `json:"campus_set"` + Address1 string `json:"address1"` + Address2 string `json:"address2"` + City string `json:"city"` + Country string `json:"country"` + State string `json:"state"` + Zipcode string `json:"zipcode"` + Floor string `json:"floor"` + Suite string `json:"suite"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Status string `json:"status"` SocialMedia []SocialMedia `json:"social_media"` } -// getOrganizationResource returns a pointer to an organizationResource -// structure corresponding to the API JSON response. An error can be returned -// if something went wrong. -func (api *API) getOrganizationResource(search map[string]interface{}) (*organizationResource, error) { - // Get the OrganizationResource from the API - response, err := api.lookup(organizationNamespace, search) - if err != nil { - return nil, err - } - - // Ask for cleanup once we are done - defer response.Body.Close() - - // Decode what the API has given to us - resource := &organizationResource{} - err = json.NewDecoder(response.Body).Decode(&resource) - if err != nil { - return nil, err - } - - return resource, nil -} - -// GetOrganization returns a pointer to a slice of Organization structures that -// the PeeringDB API can provide matching the given search parameters map. If -// an error occurs, the returned error will be non-nil. The returned value can -// be nil if no object could be found. -func (api *API) GetOrganization(search map[string]interface{}) (*[]Organization, error) { - // Ask for the all Organization objects - organizationResource, err := api.getOrganizationResource(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // Return all Organization objects, will be nil if slice is empty - return &organizationResource.Data, nil +// GetOrganization returns a slice of Organization structures matching the +// given search parameters. +func (api *API) GetOrganization(ctx context.Context, search url.Values) ([]Organization, error) { + return fetch[Organization](ctx, api, organizationNamespace, search) } -// GetAllOrganizations returns a pointer to a slice of Organization structures -// that the PeeringDB API can provide. If an error occurs, the returned error -// will be non-nil. The can be nil if no object could be found. -func (api *API) GetAllOrganizations() (*[]Organization, error) { - // Return all Organization objects - return api.GetOrganization(nil) +// GetAllOrganizations returns all Organization structures available from the +// API. +func (api *API) GetAllOrganizations(ctx context.Context) ([]Organization, error) { + return api.GetOrganization(ctx, nil) } -// GetOrganizationByID returns a pointer to a Organization structure that -// matches the given ID. If the ID is lesser than 0, it will return nil. The -// returned error will be non-nil if an issue as occurred while trying to query -// the API. If for some reasons the API returns more than one object for the -// given ID (but it must not) only the first will be used for the returned -// value. -func (api *API) GetOrganizationByID(id int) (*Organization, error) { - // No point of looking for the organization with an ID < 0 - if id < 0 { - return nil, nil - } - - // Ask for the Organization given it ID - search := make(map[string]interface{}) - search["id"] = id - - // Actually ask for it - organizations, err := api.GetOrganization(search) - - // Error as occurred while querying the API - if err != nil { - return nil, err - } - - // No Organization matching the ID - if len(*organizations) < 1 { - return nil, nil - } - - // Only return the first match, they must be only one match (ID being - // unique) - return &(*organizations)[0], nil +// GetOrganizationByID returns the Organization matching the given ID, or nil +// if not found. +func (api *API) GetOrganizationByID(ctx context.Context, id int) (*Organization, error) { + return fetchByID[Organization](ctx, api, organizationNamespace, id) } From 32102b8d3a1369ce9145603ba2d5672c051ed585 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 23:19:12 +0100 Subject: [PATCH 07/11] Update CI and go minimum version --- .github/workflows/ci.yml | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57f2085..ef3193f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ jobs: strategy: matrix: go-version: - - '1.22' - - '1.23' + - '1.24' + - '1.25' steps: - uses: actions/checkout@v3 - name: Setup Go ${{ matrix.go-version }} diff --git a/go.mod b/go.mod index ffee0de..935748c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/gmazoyer/peeringdb -go 1.22 +go 1.24 From 00643216a4ddfcc7f908ed62968f3d86e5a45622 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Sun, 8 Feb 2026 23:20:46 +0100 Subject: [PATCH 08/11] Upgrade CI actions versions --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef3193f..a5ecdac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,9 @@ jobs: - '1.24' - '1.25' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Display Go version From 54d15c9ada6385c527bfea4fb4e544264b70bf31 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Mon, 9 Feb 2026 20:35:36 +0100 Subject: [PATCH 09/11] Add example in README --- README.md | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff6ec2e..4a1eb37 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,44 @@ Install the library package with `go get github.com/gmazoyer/peeringdb`. ## Example -There are small examples in the +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/gmazoyer/peeringdb" +) + +func main() { + // Create an API client (optionally with an API key) + api := peeringdb.NewAPI( + peeringdb.WithAPIKey("your-api-key"), + ) + ctx := context.Background() + + // Look up a network by its ASN + network, err := api.GetASN(ctx, 201281) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Network: %s (AS%d)\n", network.Name, network.ASN) + + // List all facilities linked to this network + for _, netfacID := range network.NetworkFacilitySet { + netfac, err := api.GetNetworkFacilityByID(ctx, netfacID) + if err != nil { + log.Fatal(err) + } + fmt.Printf(" Facility: %s (%s, %s)\n", netfac.Name, netfac.City, netfac.Country) + } +} +``` + +More examples are available in the [package documentation](https://godoc.org/github.com/gmazoyer/peeringdb). -You can also found a real life example with the +You can also find a real life example with the [PeeringDB synchronization tool](https://github.com/gmazoyer/peeringdb-sync). From f3b25789adb79ef3cb2a2992a2576b2183bad60e Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Mon, 9 Feb 2026 21:14:03 +0100 Subject: [PATCH 10/11] Fix badge links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a1eb37..56e0b79 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PeeringDB API - Go package -[![GoDoc](https://godoc.org/github.com/gmazoyer/peeringdb?status.svg)](https://godoc.org/github.com/gmazoyer/peeringdb) +[![Go Reference](https://pkg.go.dev/badge/github.com/gmazoyer/peeringdb.svg)](https://pkg.go.dev/github.com/gmazoyer/peeringdb) [![Go Report Card](https://goreportcard.com/badge/github.com/gmazoyer/peeringdb)](https://goreportcard.com/report/github.com/gmazoyer/peeringdb) This is a Go package that allows developer to interact with the @@ -51,7 +51,7 @@ func main() { ``` More examples are available in the -[package documentation](https://godoc.org/github.com/gmazoyer/peeringdb). +[package documentation](https://pkg.go.dev/github.com/gmazoyer/peeringdb). You can also find a real life example with the [PeeringDB synchronization tool](https://github.com/gmazoyer/peeringdb-sync). From 9c9d86fec643d22a19948ccfd761482537337c46 Mon Sep 17 00:00:00 2001 From: Guillaume Mazoyer Date: Mon, 9 Feb 2026 21:18:07 +0100 Subject: [PATCH 11/11] Add version number and user-agent --- api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api.go b/api.go index 0af5f63..d26a1f0 100644 --- a/api.go +++ b/api.go @@ -11,6 +11,8 @@ import ( "strconv" ) +const Version = "0.1.0" + const ( baseAPI = "https://www.peeringdb.com/api/" facilityNamespace = "fac" @@ -112,6 +114,7 @@ func fetch[T any](ctx context.Context, api *API, namespace string, search url.Va return nil, fmt.Errorf("%w: %w", ErrBuildingRequest, err) } + request.Header.Set("User-Agent", "go-peeringdb/"+Version) if api.apiKey != "" { request.Header.Set("Authorization", "Api-Key "+api.apiKey) }