# README

// ReadMe API Client for Go is for performing API operations with ReadMe.com. // // Refer to https://docs.readme.com/main/reference/intro/getting-started for more information about // the ReadMe API. package readme

import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "reflect" "regexp" "strconv" "strings" "time" )

// Markdown documentation generation. //go:generate go run github.com/princjef/gomarkdoc/cmd/gomarkdoc --output ../docs/README.md ./...

const ( // PaginationHeader is the name of the HTTP response header with pagination links. PaginationHeader = "link"

// ReadmeAPIURL is the default base URL for the ReadMe API.
ReadmeAPIURL = "https://dash.readme.com/api/v1"

// TotalCountHeader is the name of the HTTP response header with the total count in results.
TotalCountHeader = "x-total-count"

// UserAgent is the name of the HTTP UserAgent when making requests.
UserAgent = "readme-api-go-client"

)

// IDValidCharacters is a compiled RegEx pattern that matches valid characters in an object ID or // API Registry UUID. var IDValidCharacters = regexp.MustCompile("^[0-9a-zA-Z]+$")

// Client sets up the API HTTP client with authentication and exposes the API interfaces. type Client struct { // APIURL is the base URL for the ReadMe API. APIURL string // HTTPClient is the initialized HTTP client. HTTPClient *http.Client // Token is the API token for authenticating with ReadMe. Token string

// APIRegistry implements the ReadMe API Registry API for managing API definitions.
APIRegistry APIRegistryService
// APISpecification implements the ReadMe API Specification API for managing API specifications.
APISpecification APISpecificationService
// Apply implements the ReadMe API Apply API for retrieving and applying for positions at ReadMe.
Apply ApplyService
// Category implements the ReadMe Category API for managing categories.
Category CategoryService
// Changelog implements the ReadMe Changelog API for managing changelogs.
Changelog ChangelogService
// CustomPage implements the ReadMe CustomPage API for managing custom pages.
CustomPage CustomPageService
// Doc implements the ReadMe Docs API for managing docs.
Doc DocService
// Image implements the ReadMe Image API for uploading images.
Image ImageService
// OutboundIP implements the ReadMe OutboundIP API for retrieving outbound IP addresses.
OutboundIP OutboundIPService
// Project implements the ReadMe Project API for retrieving metadata about the project.
Project ProjectService
// Version implements the ReadMe Version API for managing versions.
Version VersionService

}

// RequestHeader represents an HTTP header set on requests. type RequestHeader map[string]string

// APIRequest represents a request to the ReadMe.com API. type APIRequest struct { // Endpoint is the API endpoint (after the base URL) for the request. Endpoint string

// Headers lists HTTP headers to send in the request, in addition to the implicit headers.
Headers []RequestHeader

// Slice of HTTP status codes that are considered 'ok'.
// Any other status code in the response results in an error.
OkStatusCode []int

// Method is the HTTP method to use for the request.
Method string

// An optional payload, in bytes, for the request.
Payload []byte

// Optional options for a request, including headers, version and pagination options.
RequestOptions

// Interface of a struct to map the response body to.
Response interface{}

// UseAuth toggles whether the request should use authentication or not.
UseAuth bool

// URL is a full URL string to use for the request as an alternative to Endpoint.
URL string

}

// APIResponse represents the response from a request to the ReadMe API. type APIResponse struct { // APIErrorResponse is a structured error from the ReadMe API when a request results in error. APIErrorResponse APIErrorResponse // Body is the response body in bytes. Body []byte // HTTPResponse is the stdlib http.Response type. HTTPResponse *http.Response // Request is the APIRequest struct used to create the request. Request *APIRequest }

// APIErrorResponse represents the response ReadMe provides in the body of requests that failed. type APIErrorResponse struct { // Docs is a ReadMe Metrics log URL where more information about the request can be retrieved. // If metrics URLs are unavailable for the request, this URL will be a URL to the ReadMe API Reference. Docs string json:"docs" // Error is an error code unique to the error received. Error string json:"error" // Help is information on where additional assistance from the ReadMe support team can be obtained. Help string json:"help" // Message is the reason why the error occurred. Message string json:"message" // Poem is a short poem about the error. Poem []string json:"poem" // Suggestion is a helpful suggestion for how to alleviate the error. Suggestion string json:"suggestion" }

// RequestOptions is used for specifying options for requests, such as pagination options. type RequestOptions struct { // Headers is a list of additional headers to add to the request. Headers []RequestHeader // PerPage is the number of items to return in each request when using pagination. // The maximum and default is 100. PerPage int // Page is the page number to request when using pagination. Page int // ProductionDoc is used by readme.Docs.Get() to indicate whether the requested document is a // 'production' doc. ProductionDoc bool // Version number of a ReadMe project, for example, v3.0. By default the main project version is used. Version string }

// NewClient initializes the API client configuration and returns the HTTP client with an auth token and URL set. // // Optionally provide a custom API URL as a second parameter. func NewClient(token string, apiURL ...string) (*Client, error) { client := &Client{ HTTPClient: &http.Client{Timeout: 10 * time.Second}, } client.APIURL = ReadmeAPIURL client.Token = token

if apiURL != nil {
	if len(apiURL) > 1 {
		return nil, fmt.Errorf("unable to configure ReadMe API client: "+
			"too many values specified for API URL (got: %v; expects 1)", len(apiURL))
	}
	client.APIURL = apiURL[0]
}

client.APIRegistry = &APIRegistryClient{client: client}
client.APISpecification = &APISpecificationClient{client: client}
client.Apply = &ApplyClient{client: client}
client.Category = &CategoryClient{client: client}
client.Changelog = &ChangelogClient{client: client}
client.CustomPage = &CustomPageClient{client: client}
client.Doc = &DocClient{client: client}
client.Image = &ImageClient{client: client}
client.OutboundIP = &OutboundIPClient{client: client}
client.Project = &ProjectClient{client: client}
client.Version = &VersionClient{client: client}

return client, nil

}

// APIRequest performs a request to the ReadMe API and handles parsing the response and API errors. // // This function is called directly by the receiver functions used to implement each endpoint. func (c *Client) APIRequest(request *APIRequest) (*APIResponse, error) { // Perform the request body, httpResponse, err := c.doRequest(request) if err != nil { return nil, err }

apiResponse := &APIResponse{
	Body:         body,
	HTTPResponse: &httpResponse,
	Request:      request,
}

// Verify the HTTP response from the API.
apiErrorResponse, err := checkResponseStatus(body, httpResponse.StatusCode, request)
if err != nil {
	apiResponse.APIErrorResponse = apiErrorResponse

	return apiResponse, err
}

// Parse the response into the specified interface.
if request.Response != nil {
	err = json.Unmarshal(body, &request.Response)
	if err != nil {
		return apiResponse, fmt.Errorf("unable to parse API response: %w", err)
	}
}

err = httpResponse.Body.Close()
if err != nil {
	return apiResponse, fmt.Errorf("problem closing HTTP response body")
}

return apiResponse, nil

}

// doRequest performs an API request and returns the response or error. func (c *Client) doRequest(request *APIRequest) ([]byte, http.Response, error) { req, err := c.prepareRequest(request) if err != nil { return nil, http.Response{}, err }

// Perform the request.
res, err := c.HTTPClient.Do(req)
if err != nil {
	return nil, http.Response{}, fmt.Errorf("unable to make request: %w", err)
}

if res.Body == nil {
	return nil, *res, fmt.Errorf("response body is nil in %s request to %s", req.Method, req.URL)
}

body, err := io.ReadAll(res.Body)
if err != nil {
	return nil, *res, fmt.Errorf("unable to read response: %w", err)
}

err = res.Body.Close()
if err != nil {
	return nil, *res, fmt.Errorf("problem closing HTTP response body")
}

return body, *res, nil

}

// checkResponseStatus compares an HTTP response status code against a slice of 'OK' status codes. // // If the response code matches a provided code listed in okCodes, no error is returned. // If the response code doesn't match, an error and APIErrorResponse is returned. func checkResponseStatus(body []byte, responseCode int, req *APIRequest) (APIErrorResponse, error) { var apiErrorResponse APIErrorResponse for _, okCode := range req.OkStatusCode { if responseCode == okCode { return apiErrorResponse, nil } }

err := json.Unmarshal(body, &apiErrorResponse)
if err != nil {
	return apiErrorResponse, fmt.Errorf("unable to decode API error response: %w", err)
}

return apiErrorResponse,
	fmt.Errorf("ReadMe API Error: %v on %s %s: %s",
		responseCode, req.Method, req.Endpoint, body)

}

// prepareRequest prepares an http.Request for the ReadMe API. // // This sets common headers and prepares an optional payload for the request. func (c *Client) prepareRequest(request *APIRequest) (*http.Request, error) { // Prepare the request. if request.URL == "" { request.URL = c.APIURL + request.Endpoint } req, reqErr := http.NewRequest(request.Method, request.URL, nil)

if request.Payload != nil {
	data := bytes.NewBuffer(request.Payload)
	req, reqErr = http.NewRequest(request.Method, request.URL, data)
}

if reqErr != nil {
	return nil, fmt.Errorf("unable to prepare request: %w", reqErr)
}

for _, r := range request.Headers {
	for header, value := range r {
		req.Header.Set(header, value)
	}
}

if request.UseAuth {
	encodedToken := base64.StdEncoding.EncodeToString([]byte(c.Token))
	authHeader := "Basic " + encodedToken
	req.Header.Set("authorization", authHeader)
}

if request.RequestOptions.Version != "" {
	req.Header.Set("x-readme-version", request.RequestOptions.Version)
}

req.Header.Set("accept", "application/json")
req.Header.Set("User-Agent", UserAgent)

return req, nil

}

// paginatedRequest makes a request to the ReadMe API with pagination query parameters set. // // An abbreviated *APIRequest struct should be passed, leaving the Headers and Version fields unset. // These are derived from the RequestOptions field. // // This function is intended to be called within a loop and returns the APIResponse struct and a // boolean indicating if there is a next page indicated in the pagination header. func (c *Client) paginatedRequest(apiRequest *APIRequest, page int) (*APIResponse, bool, error) { // Set default values perPage := 100

// Check for custom values in RequestOptions
if apiRequest.RequestOptions.PerPage != 0 {
	perPage = apiRequest.RequestOptions.PerPage
}
if apiRequest.RequestOptions.Headers != nil {
	apiRequest.Headers = apiRequest.RequestOptions.Headers
}

// Add pagination parameters to endpoint
baseEndpoint := apiRequest.Endpoint
apiRequest.Endpoint = fmt.Sprintf("%s?perPage=%d&page=%d", baseEndpoint, perPage, page)

if apiRequest.URL == "" {
	apiRequest.URL = c.APIURL + apiRequest.Endpoint
}

// Make API request
apiResponse, err := c.APIRequest(apiRequest)
if err != nil {
	return apiResponse, false, fmt.Errorf("unable to make request: %w", err)
}

// Check for next page
hasNextPage, err := HasNextPage(apiResponse.HTTPResponse.Header.Get(PaginationHeader))
if err != nil {
	return apiResponse, false, fmt.Errorf(
		"unable to check pagination link header '%s': %w; ",
		PaginationHeader,
		err,
	)
}
if !hasNextPage {
	return apiResponse, false, nil
}

// Get total count of items
totalCountHeader := apiResponse.HTTPResponse.Header.Get(TotalCountHeader)
totalCount, err := strconv.Atoi(totalCountHeader)
if err != nil {
	return apiResponse, false, fmt.Errorf(
		"unable to parse '%s' header: %w; Response: %v",
		TotalCountHeader,
		err,
		apiResponse,
	)
}

// Check if current page is last page
if page >= (totalCount / perPage) {
	return apiResponse, false, nil
}

return apiResponse, true, nil

}

func (c *Client) fetchAllPages( endpoint string, options *RequestOptions, result interface{}, ) (*APIResponse, error) { hasNextPage := false page := 1 var apiResponse *APIResponse var err error

// Convert the result to a reflect.Value to manipulate the underlying slice
resultValue := reflect.ValueOf(result)
if resultValue.Kind() != reflect.Ptr || resultValue.Elem().Kind() != reflect.Slice {
	return nil, fmt.Errorf("result argument must be a pointer to a slice")
}

if options != nil && options.Page != 0 {
	page = options.Page
}

for {
	pageResults := reflect.New(resultValue.Elem().Type()).Interface()

	apiRequest := &APIRequest{
		Method:       "GET",
		Endpoint:     endpoint,
		UseAuth:      true,
		OkStatusCode: []int{200},
		Response:     pageResults,
	}
	if options != nil {
		apiRequest.RequestOptions = *options
	}

	apiResponse, hasNextPage, err = c.paginatedRequest(apiRequest, page)
	if err != nil {
		return apiResponse, fmt.Errorf("unable to retrieve data: %w", err)
	}

	// Append the current page's results to the original result slice
	resultValue.Elem().Set(reflect.AppendSlice(resultValue.Elem(), reflect.ValueOf(pageResults).Elem()))

	if !hasNextPage {
		break
	}
	page++
}

return apiResponse, nil

}

// parseRequestOptions is a helper function to parse the RequestOptions slice // and return the first element as a *RequestOptions struct. func parseRequestOptions(options []RequestOptions) *RequestOptions { if len(options) > 0 { return &options[0] } return nil }

// HasNextPage checks if a "next" link is provided in the "links" response header for pagination, // indicating the request has a next page. // // This does a rudimentary parsing of the header value, splitting on the comma-separated links and // parsing the value of "rel". // // A link header looks like: // </api-specification?page=2>; rel="next", <>; rel="prev", <>; rel="last" func HasNextPage(links string) (bool, error) { // Split links by comma parts := strings.Split(links, ",")

// Return error if invalid format
if len(parts) < 3 {
	return false, fmt.Errorf("unable to parse link header - invalid format: "+
		"'%s'; expected "+`'<>; rel="next", <>; rel="prev", <>; rel="last"'`, links)
}

// Check for "rel=next" in parts
for _, part := range parts {
	rel := strings.Split(part, ";")
	if len(rel) != 2 {
		return false, fmt.Errorf("unable to parse link header - invalid format: "+
			"'%s'; expected "+`'<>; rel="next", <>; rel="prev", <>; rel="last"'`, links)
	}
	if rel[1] == " rel=\"next\"" && rel[0] != "<>" {
		return true, nil
	}
}

// Return false if "rel=next" is not found
return false, nil

}

// ValidateID is a helper script for parseID() and parseUUID() that checks a string to determine if // it appears to be a valid ReadMe API object ID or Registry UUID. func ValidateID(id, prefix string, min_len, max_len int) (bool, string) { if !strings.HasPrefix(id, prefix+":") { return false, "" }

parts := strings.Split(id, ":")

if len(parts[1]) < min_len || len(parts[1]) > max_len {
	return false, ""
}

return IDValidCharacters.MatchString(parts[1]), parts[1]

}

// ParseUUID checks a string to determine if it appears to be a valid ReadMe API Registry UUID. // // The provided parameter should be a ReadMe API Registry UUID prefixed with "uuid:". // // NOTE: The min and max lengths aren't certain or documented in the API. The UUID length varies. func ParseUUID(uuid string) (bool, string) { return ValidateID(uuid, "uuid", 10, 24) }

// ParseID checks a string to determine if it appears to be a valid ReadMe API object ID. // // The provided parameter should be a ReadMe API object ID prefixed with "id:". // // NOTE: The min and max lengths aren't certain or documented in the API. func ParseID(id string) (bool, string) { return ValidateID(id, "id", 20, 24) }