Add new vendor libraries for newrelic provider
This commit is contained in:
parent
c2322b1e55
commit
f39cfe61ce
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,87 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) queryAlertChannels() ([]AlertChannel, error) {
|
||||||
|
channels := []AlertChannel{}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse("/alerts_channels.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
for nextPath != "" {
|
||||||
|
resp := struct {
|
||||||
|
Channels []AlertChannel `json:"channels,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
nextPath, err = c.Do("GET", nextPath, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = append(channels, resp.Channels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlertChannel returns a specific alert channel by ID
|
||||||
|
func (c *Client) GetAlertChannel(id int) (*AlertChannel, error) {
|
||||||
|
channels, err := c.queryAlertChannels()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.ID == id {
|
||||||
|
return &channel, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAlertChannels returns all alert policies for the account.
|
||||||
|
func (c *Client) ListAlertChannels() ([]AlertChannel, error) {
|
||||||
|
return c.queryAlertChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateAlertChannel(channel AlertChannel) (*AlertChannel, error) {
|
||||||
|
// TODO: support attaching policy ID's here?
|
||||||
|
// qs := map[string]string{
|
||||||
|
// "policy_ids[]": channel.Links.PolicyIDs,
|
||||||
|
// }
|
||||||
|
|
||||||
|
if len(channel.Links.PolicyIDs) > 0 {
|
||||||
|
return nil, fmt.Errorf("You cannot create an alert channel with policy IDs, you must attach polidy IDs after creation.")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Channel AlertChannel `json:"channel"`
|
||||||
|
}{
|
||||||
|
Channel: channel,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Channels []AlertChannel `json:"channels,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
_, err := c.Do("POST", "/alerts_channels.json", req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp.Channels[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteAlertChannel(id int) error {
|
||||||
|
u := &url.URL{Path: fmt.Sprintf("/alerts_channels/%v.json", id)}
|
||||||
|
_, err := c.Do("DELETE", u.String(), nil, nil)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) queryAlertConditions(policyID int) ([]AlertCondition, error) {
|
||||||
|
conditions := []AlertCondition{}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse("/alerts_conditions.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := reqURL.Query()
|
||||||
|
qs.Set("policy_id", strconv.Itoa(policyID))
|
||||||
|
|
||||||
|
reqURL.RawQuery = qs.Encode()
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
for nextPath != "" {
|
||||||
|
resp := struct {
|
||||||
|
Conditions []AlertCondition `json:"conditions,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
nextPath, err = c.Do("GET", nextPath, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range resp.Conditions {
|
||||||
|
c.PolicyID = policyID
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, resp.Conditions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAlertCondition(policyID int, id int) (*AlertCondition, error) {
|
||||||
|
conditions, err := c.queryAlertConditions(policyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range conditions {
|
||||||
|
if condition.ID == id {
|
||||||
|
return &condition, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAlertConditions returns alert conditions for the specified policy.
|
||||||
|
func (c *Client) ListAlertConditions(policyID int) ([]AlertCondition, error) {
|
||||||
|
return c.queryAlertConditions(policyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateAlertCondition(condition AlertCondition) (*AlertCondition, error) {
|
||||||
|
policyID := condition.PolicyID
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Condition AlertCondition `json:"condition"`
|
||||||
|
}{
|
||||||
|
Condition: condition,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Condition AlertCondition `json:"condition,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
u := &url.URL{Path: fmt.Sprintf("/alerts_conditions/policies/%v.json", policyID)}
|
||||||
|
_, err := c.Do("POST", u.String(), req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Condition.PolicyID = policyID
|
||||||
|
|
||||||
|
return &resp.Condition, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateAlertCondition(condition AlertCondition) (*AlertCondition, error) {
|
||||||
|
policyID := condition.PolicyID
|
||||||
|
id := condition.ID
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Condition AlertCondition `json:"condition"`
|
||||||
|
}{
|
||||||
|
Condition: condition,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Condition AlertCondition `json:"condition,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
u := &url.URL{Path: fmt.Sprintf("/alerts_conditions/%v.json", id)}
|
||||||
|
_, err := c.Do("PUT", u.String(), req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Condition.PolicyID = policyID
|
||||||
|
|
||||||
|
return &resp.Condition, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteAlertCondition(policyID int, id int) error {
|
||||||
|
u := &url.URL{Path: fmt.Sprintf("/alerts_conditions/%v.json", id)}
|
||||||
|
_, err := c.Do("DELETE", u.String(), nil, nil)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) queryAlertPolicies(name *string) ([]AlertPolicy, error) {
|
||||||
|
policies := []AlertPolicy{}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse("/alerts_policies.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := reqURL.Query()
|
||||||
|
if name != nil {
|
||||||
|
qs.Set("filter[name]", *name)
|
||||||
|
}
|
||||||
|
reqURL.RawQuery = qs.Encode()
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
for nextPath != "" {
|
||||||
|
resp := struct {
|
||||||
|
Policies []AlertPolicy `json:"policies,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
nextPath, err = c.Do("GET", nextPath, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
policies = append(policies, resp.Policies...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return policies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlertPolicy returns a specific alert policy by ID
|
||||||
|
func (c *Client) GetAlertPolicy(id int) (*AlertPolicy, error) {
|
||||||
|
policies, err := c.queryAlertPolicies(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, policy := range policies {
|
||||||
|
if policy.ID == id {
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAlertPolicies returns all alert policies for the account.
|
||||||
|
func (c *Client) ListAlertPolicies() ([]AlertPolicy, error) {
|
||||||
|
return c.queryAlertPolicies(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAlertPolicy creates a new alert policy for the account.
|
||||||
|
func (c *Client) CreateAlertPolicy(policy AlertPolicy) (*AlertPolicy, error) {
|
||||||
|
req := struct {
|
||||||
|
Policy AlertPolicy `json:"policy"`
|
||||||
|
}{
|
||||||
|
Policy: policy,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Policy AlertPolicy `json:"policy,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
_, err := c.Do("POST", "/alerts_policies.json", req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp.Policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAlertPolicy deletes an existing alert policy from the account.
|
||||||
|
func (c *Client) DeleteAlertPolicy(id int) error {
|
||||||
|
u := &url.URL{Path: fmt.Sprintf("/alerts_policies/%v.json", id)}
|
||||||
|
_, err := c.Do("DELETE", u.String(), nil, nil)
|
||||||
|
return err
|
||||||
|
}
|
64
vendor/github.com/paultyng/go-newrelic/api/alert_policy_channels.go
generated
vendored
Normal file
64
vendor/github.com/paultyng/go-newrelic/api/alert_policy_channels.go
generated
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) UpdateAlertPolicyChannels(policyID int, channelIDs []int) error {
|
||||||
|
channelIDStrings := make([]string, len(channelIDs))
|
||||||
|
|
||||||
|
for i, channelID := range channelIDs {
|
||||||
|
channelIDStrings[i] = strconv.Itoa(channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse("/alerts_policy_channels.json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := url.Values{
|
||||||
|
"policy_id": []string{strconv.Itoa(policyID)},
|
||||||
|
"channel_ids": channelIDStrings,
|
||||||
|
}
|
||||||
|
reqURL.RawQuery = qs.Encode()
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
_, err = c.Do("PUT", nextPath, nil, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteAlertPolicyChannel(policyID int, channelID int) error {
|
||||||
|
reqURL, err := url.Parse("/alerts_policy_channels.json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := url.Values{
|
||||||
|
"policy_id": []string{strconv.Itoa(policyID)},
|
||||||
|
"channel_id": []string{strconv.Itoa(channelID)},
|
||||||
|
}
|
||||||
|
reqURL.RawQuery = qs.Encode()
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
_, err = c.Do("DELETE", nextPath, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
if apiErr, ok := err.(*ErrorResponse); ok {
|
||||||
|
matched, err := regexp.MatchString("Alerts policy with ID: \\d+ is not valid.", apiErr.Detail.Title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type applicationsFilters struct {
|
||||||
|
Name *string
|
||||||
|
Host *string
|
||||||
|
IDs []int
|
||||||
|
Language *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) queryApplications(filters applicationsFilters) ([]Application, error) {
|
||||||
|
applications := []Application{}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse("/applications.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := reqURL.Query()
|
||||||
|
if filters.Name != nil {
|
||||||
|
qs.Set("filter[name]", *filters.Name)
|
||||||
|
}
|
||||||
|
if filters.Host != nil {
|
||||||
|
qs.Set("filter[host]", *filters.Host)
|
||||||
|
}
|
||||||
|
for _, id := range filters.IDs {
|
||||||
|
qs.Add("filter[ids]", strconv.Itoa(id))
|
||||||
|
}
|
||||||
|
if filters.Language != nil {
|
||||||
|
qs.Set("filter[language]", *filters.Language)
|
||||||
|
}
|
||||||
|
reqURL.RawQuery = qs.Encode()
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
for nextPath != "" {
|
||||||
|
resp := struct {
|
||||||
|
Applications []Application `json:"applications,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
nextPath, err = c.Do("GET", nextPath, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
applications = append(applications, resp.Applications...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return applications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListApplications() ([]Application, error) {
|
||||||
|
return c.queryApplications(applicationsFilters{})
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tomnomnom/linkheader"
|
||||||
|
|
||||||
|
resty "gopkg.in/resty.v0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents the client state for the API.
|
||||||
|
type Client struct {
|
||||||
|
RestyClient *resty.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Detail *ErrorDetail `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorResponse) Error() string {
|
||||||
|
if e != nil && e.Detail != nil {
|
||||||
|
return e.Detail.Title
|
||||||
|
}
|
||||||
|
return "Unknown error"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorDetail struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains all the configuration data for the API Client
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
BaseURL string
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Client for the specified apiKey.
|
||||||
|
func New(config Config) Client {
|
||||||
|
r := resty.New()
|
||||||
|
|
||||||
|
baseURL := config.BaseURL
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "https://api.newrelic.com/v2"
|
||||||
|
}
|
||||||
|
|
||||||
|
r.SetHeader("X-Api-Key", config.APIKey)
|
||||||
|
r.SetHostURL(baseURL)
|
||||||
|
|
||||||
|
if config.Debug {
|
||||||
|
r.SetDebug(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Client{
|
||||||
|
RestyClient: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do exectes an API request with the specified parameters.
|
||||||
|
func (c *Client) Do(method string, path string, body interface{}, response interface{}) (string, error) {
|
||||||
|
r := c.RestyClient.R().
|
||||||
|
SetError(&ErrorResponse{})
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
r = r.SetBody(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response != nil {
|
||||||
|
r = r.SetResult(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResponse, err := r.Execute(method, path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPath := ""
|
||||||
|
header := apiResponse.Header().Get("Link")
|
||||||
|
if header != "" {
|
||||||
|
links := linkheader.Parse(header)
|
||||||
|
|
||||||
|
for _, link := range links.FilterByRel("next") {
|
||||||
|
nextPath = link.URL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusClass := apiResponse.StatusCode() / 100 % 10
|
||||||
|
|
||||||
|
if statusClass == 2 {
|
||||||
|
return nextPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawError := apiResponse.Error()
|
||||||
|
|
||||||
|
if rawError != nil {
|
||||||
|
apiError := rawError.(*ErrorResponse)
|
||||||
|
|
||||||
|
if apiError.Detail != nil {
|
||||||
|
return "", apiError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("Unexpected status %v returned from API", apiResponse.StatusCode())
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) queryLabels() ([]Label, error) {
|
||||||
|
labels := []Label{}
|
||||||
|
|
||||||
|
reqURL, err := url.Parse("/labels.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPath := reqURL.String()
|
||||||
|
|
||||||
|
for nextPath != "" {
|
||||||
|
resp := struct {
|
||||||
|
Labels []Label `json:"labels,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
nextPath, err = c.Do("GET", nextPath, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = append(labels, resp.Labels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetLabel(key string) (*Label, error) {
|
||||||
|
labels, err := c.queryLabels()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range labels {
|
||||||
|
if label.Key == key {
|
||||||
|
return &label, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLabels returns the labels for the account.
|
||||||
|
func (c *Client) ListLabels() ([]Label, error) {
|
||||||
|
return c.queryLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLabel creates a new label for the account.
|
||||||
|
func (c *Client) CreateLabel(label Label) error {
|
||||||
|
if label.Links.Applications == nil {
|
||||||
|
label.Links.Applications = make([]int, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if label.Links.Servers == nil {
|
||||||
|
label.Links.Servers = make([]int, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Label Label `json:"label,omitempty"`
|
||||||
|
}{
|
||||||
|
Label: label,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.Do("PUT", "/labels.json", req, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLabel deletes a label on the account specified by key.
|
||||||
|
func (c *Client) DeleteLabel(key string) error {
|
||||||
|
u := &url.URL{Path: fmt.Sprintf("/labels/%v.json", key)}
|
||||||
|
_, err := c.Do("DELETE", u.String(), nil, nil)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("newrelic: Resource not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// LabelLinks represents external references on the Label.
|
||||||
|
type LabelLinks struct {
|
||||||
|
Applications []int `json:"applications"`
|
||||||
|
Servers []int `json:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label represents a New Relic label.
|
||||||
|
type Label struct {
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Links LabelLinks `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertPolicy represents a New Relic alert policy.
|
||||||
|
type AlertPolicy struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
IncidentPreference string `json:"incident_preference,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
CreatedAt int `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt int `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertConditionUserDefined represents user defined metrics for the New Relic alert condition.
|
||||||
|
type AlertConditionUserDefined struct {
|
||||||
|
Metric string `json:"metric,omitempty"`
|
||||||
|
ValueFunction string `json:"value_function,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertConditionTerm represents the terms of a New Relic alert condition.
|
||||||
|
type AlertConditionTerm struct {
|
||||||
|
Duration int `json:"duration,string,omitempty"`
|
||||||
|
Operator string `json:"operator,omitempty"`
|
||||||
|
Priority string `json:"priority,omitempty"`
|
||||||
|
Threshold float64 `json:"threshold,string,omitempty"`
|
||||||
|
TimeFunction string `json:"time_function,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertCondition represents a New Relic alert condition.
|
||||||
|
// TODO: custom unmarshal entities to ints?
|
||||||
|
// TODO: handle unmarshaling .75 for float (not just 0.75)
|
||||||
|
type AlertCondition struct {
|
||||||
|
PolicyID int `json:"-"`
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Entities []string `json:"entities,omitempty"`
|
||||||
|
Metric string `json:"metric,omitempty"`
|
||||||
|
RunbookURL string `json:"runbook_url,omitempty"`
|
||||||
|
Terms []AlertConditionTerm `json:"terms,omitempty"`
|
||||||
|
UserDefined AlertConditionUserDefined `json:"uder_defined,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertChannelLinks represent the links between policies and alert channels
|
||||||
|
type AlertChannelLinks struct {
|
||||||
|
PolicyIDs []int `json:"policy_ids,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertChannel represents a New Relic alert notification channel
|
||||||
|
type AlertChannel struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Configuration map[string]interface{} `json:"configuration,omitempty"`
|
||||||
|
Links AlertChannelLinks `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApplicationSummary struct {
|
||||||
|
ResponseTime float64 `json:"response_time"`
|
||||||
|
Throughput float64 `json:"throughput"`
|
||||||
|
ErrorRate float64 `json:"error_rate"`
|
||||||
|
ApdexTarget float64 `json:"apdex_target"`
|
||||||
|
ApdexScore float64 `json:"apdex_score"`
|
||||||
|
HostCount int `json:"host_count"`
|
||||||
|
InstanceCount int `json:"instance_count"`
|
||||||
|
ConcurrentInstanceCount int `json:"concurrent_instance_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApplicationEndUserSummary struct {
|
||||||
|
ResponseTime float64 `json:"response_time"`
|
||||||
|
Throughput float64 `json:"throughput"`
|
||||||
|
ApdexTarget float64 `json:"apdex_target"`
|
||||||
|
ApdexScore float64 `json:"apdex_score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApplicationSettings struct {
|
||||||
|
AppApdexThreshold float64 `json:"app_apdex_threshold,omitempty"`
|
||||||
|
EndUserApdexThreshold float64 `json:"end_user_apdex_threshold,omitempty"`
|
||||||
|
EnableRealUserMonitoring bool `json:"enable_real_user_monitoring,omitempty"`
|
||||||
|
UseServerSideConfig bool `json:"use_server_side_config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApplicationLinks struct {
|
||||||
|
ServerIDs []int `json:"servers,omitempty"`
|
||||||
|
HostIDs []int `json:"application_hosts,omitempty"`
|
||||||
|
InstanceIDs []int `json:"application_instances,omitempty"`
|
||||||
|
AlertPolicyID int `json:"alert_policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Language string `json:"language,omitempty"`
|
||||||
|
HealthStatus string `json:"health_status,omitempty"`
|
||||||
|
Reporting bool `json:"reporting,omitempty"`
|
||||||
|
LastReportedAt string `json:"last_reported_at,omitempty"`
|
||||||
|
Summary ApplicationSummary `json:"application_summary,omitempty"`
|
||||||
|
EndUserSummary ApplicationEndUserSummary `json:"end_user_summary,omitempty"`
|
||||||
|
Settings ApplicationSettings `json:"settings,omitempty"`
|
||||||
|
Links ApplicationLinks `json:"links,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
* Raise an issue if appropriate
|
||||||
|
* Fork the repo
|
||||||
|
* Bootstrap the dev dependencies (run `./script/bootstrap`)
|
||||||
|
* Make your changes
|
||||||
|
* Use [gofmt](https://golang.org/cmd/gofmt/)
|
||||||
|
* Make sure the tests pass (run `./script/test`)
|
||||||
|
* Make sure the linters pass (run `./script/lint`)
|
||||||
|
* Issue a pull request
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 Tom Hudson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Golang Link Header Parser
|
||||||
|
|
||||||
|
Library for parsing HTTP Link headers. Requires Go 1.2 or higher.
|
||||||
|
|
||||||
|
Docs can be found on [the GoDoc page](https://godoc.org/github.com/tomnomnom/linkheader).
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/tomnomnom/linkheader.svg)](https://travis-ci.org/tomnomnom/linkheader)
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tomnomnom/linkheader"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
header := "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"," +
|
||||||
|
"<https://api.github.com/user/58276/repos?page=2>; rel=\"last\""
|
||||||
|
links := linkheader.Parse(header)
|
||||||
|
|
||||||
|
for _, link := range links {
|
||||||
|
fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// URL: https://api.github.com/user/58276/repos?page=2; Rel: next
|
||||||
|
// URL: https://api.github.com/user/58276/repos?page=2; Rel: last
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Package linkheader provides functions for parsing HTTP Link headers
|
||||||
|
package linkheader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Link is a single URL and related parameters
|
||||||
|
type Link struct {
|
||||||
|
URL string
|
||||||
|
Rel string
|
||||||
|
Params map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasParam returns if a Link has a particular parameter or not
|
||||||
|
func (l Link) HasParam(key string) bool {
|
||||||
|
for p := range l.Params {
|
||||||
|
if p == key {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param returns the value of a parameter if it exists
|
||||||
|
func (l Link) Param(key string) string {
|
||||||
|
for k, v := range l.Params {
|
||||||
|
if key == k {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of a link
|
||||||
|
func (l Link) String() string {
|
||||||
|
|
||||||
|
p := make([]string, 0, len(l.Params))
|
||||||
|
for k, v := range l.Params {
|
||||||
|
p = append(p, fmt.Sprintf("%s=\"%s\"", k, v))
|
||||||
|
}
|
||||||
|
if l.Rel != "" {
|
||||||
|
p = append(p, fmt.Sprintf("%s=\"%s\"", "rel", l.Rel))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<%s>; %s", l.URL, strings.Join(p, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links is a slice of Link structs
|
||||||
|
type Links []Link
|
||||||
|
|
||||||
|
// FilterByRel filters a group of Links by the provided Rel attribute
|
||||||
|
func (l Links) FilterByRel(r string) Links {
|
||||||
|
links := make(Links, 0)
|
||||||
|
for _, link := range l {
|
||||||
|
if link.Rel == r {
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of multiple Links
|
||||||
|
// for use in HTTP responses etc
|
||||||
|
func (l Links) String() string {
|
||||||
|
var strs []string
|
||||||
|
for _, link := range l {
|
||||||
|
strs = append(strs, link.String())
|
||||||
|
}
|
||||||
|
return strings.Join(strs, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a raw Link header in the form:
|
||||||
|
// <url>; rel="foo", <url>; rel="bar"; wat="dis"
|
||||||
|
// returning a slice of Link structs
|
||||||
|
func Parse(raw string) Links {
|
||||||
|
links := make(Links, 0)
|
||||||
|
|
||||||
|
// One chunk: <url>; rel="foo"
|
||||||
|
for _, chunk := range strings.Split(raw, ",") {
|
||||||
|
|
||||||
|
link := Link{URL: "", Rel: "", Params: make(map[string]string)}
|
||||||
|
|
||||||
|
// Figure out what each piece of the chunk is
|
||||||
|
for _, piece := range strings.Split(chunk, ";") {
|
||||||
|
|
||||||
|
piece = strings.Trim(piece, " ")
|
||||||
|
if piece == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL
|
||||||
|
if piece[0] == '<' && piece[len(piece)-1] == '>' {
|
||||||
|
link.URL = strings.Trim(piece, "<>")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params
|
||||||
|
key, val := parseParam(piece)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for rel
|
||||||
|
if strings.ToLower(key) == "rel" {
|
||||||
|
link.Rel = val
|
||||||
|
}
|
||||||
|
|
||||||
|
link.Params[key] = val
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMultiple is like Parse, but accepts a slice of headers
|
||||||
|
// rather than just one header string
|
||||||
|
func ParseMultiple(headers []string) Links {
|
||||||
|
links := make(Links, 0)
|
||||||
|
for _, header := range headers {
|
||||||
|
links = append(links, Parse(header)...)
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParam takes a raw param in the form key="val" and
|
||||||
|
// returns the key and value as seperate strings
|
||||||
|
func parseParam(raw string) (key, val string) {
|
||||||
|
|
||||||
|
parts := strings.SplitN(raw, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
key = parts[0]
|
||||||
|
val = strings.Trim(parts[1], "\"")
|
||||||
|
|
||||||
|
return key, val
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package idna implements IDNA2008 (Internationalized Domain Names for
|
||||||
|
// Applications), defined in RFC 5890, RFC 5891, RFC 5892, RFC 5893 and
|
||||||
|
// RFC 5894.
|
||||||
|
package idna // import "golang.org/x/net/idna"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(nigeltao): specify when errors occur. For example, is ToASCII(".") or
|
||||||
|
// ToASCII("foo\x00") an error? See also http://www.unicode.org/faq/idn.html#11
|
||||||
|
|
||||||
|
// acePrefix is the ASCII Compatible Encoding prefix.
|
||||||
|
const acePrefix = "xn--"
|
||||||
|
|
||||||
|
// ToASCII converts a domain or domain label to its ASCII form. For example,
|
||||||
|
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
|
||||||
|
// ToASCII("golang") is "golang".
|
||||||
|
func ToASCII(s string) (string, error) {
|
||||||
|
if ascii(s) {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
labels := strings.Split(s, ".")
|
||||||
|
for i, label := range labels {
|
||||||
|
if !ascii(label) {
|
||||||
|
a, err := encode(acePrefix, label)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
labels[i] = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(labels, "."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUnicode converts a domain or domain label to its Unicode form. For example,
|
||||||
|
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
|
||||||
|
// ToUnicode("golang") is "golang".
|
||||||
|
func ToUnicode(s string) (string, error) {
|
||||||
|
if !strings.Contains(s, acePrefix) {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
labels := strings.Split(s, ".")
|
||||||
|
for i, label := range labels {
|
||||||
|
if strings.HasPrefix(label, acePrefix) {
|
||||||
|
u, err := decode(label[len(acePrefix):])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
labels[i] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(labels, "."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ascii(s string) bool {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] >= utf8.RuneSelf {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package idna
|
||||||
|
|
||||||
|
// This file implements the Punycode algorithm from RFC 3492.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These parameter values are specified in section 5.
|
||||||
|
//
|
||||||
|
// All computation is done with int32s, so that overflow behavior is identical
|
||||||
|
// regardless of whether int is 32-bit or 64-bit.
|
||||||
|
const (
|
||||||
|
base int32 = 36
|
||||||
|
damp int32 = 700
|
||||||
|
initialBias int32 = 72
|
||||||
|
initialN int32 = 128
|
||||||
|
skew int32 = 38
|
||||||
|
tmax int32 = 26
|
||||||
|
tmin int32 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// decode decodes a string as specified in section 6.2.
|
||||||
|
func decode(encoded string) (string, error) {
|
||||||
|
if encoded == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
pos := 1 + strings.LastIndex(encoded, "-")
|
||||||
|
if pos == 1 {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", encoded)
|
||||||
|
}
|
||||||
|
if pos == len(encoded) {
|
||||||
|
return encoded[:len(encoded)-1], nil
|
||||||
|
}
|
||||||
|
output := make([]rune, 0, len(encoded))
|
||||||
|
if pos != 0 {
|
||||||
|
for _, r := range encoded[:pos-1] {
|
||||||
|
output = append(output, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i, n, bias := int32(0), initialN, initialBias
|
||||||
|
for pos < len(encoded) {
|
||||||
|
oldI, w := i, int32(1)
|
||||||
|
for k := base; ; k += base {
|
||||||
|
if pos == len(encoded) {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", encoded)
|
||||||
|
}
|
||||||
|
digit, ok := decodeDigit(encoded[pos])
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", encoded)
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
i += digit * w
|
||||||
|
if i < 0 {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", encoded)
|
||||||
|
}
|
||||||
|
t := k - bias
|
||||||
|
if t < tmin {
|
||||||
|
t = tmin
|
||||||
|
} else if t > tmax {
|
||||||
|
t = tmax
|
||||||
|
}
|
||||||
|
if digit < t {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w *= base - t
|
||||||
|
if w >= math.MaxInt32/base {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x := int32(len(output) + 1)
|
||||||
|
bias = adapt(i-oldI, x, oldI == 0)
|
||||||
|
n += i / x
|
||||||
|
i %= x
|
||||||
|
if n > utf8.MaxRune || len(output) >= 1024 {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", encoded)
|
||||||
|
}
|
||||||
|
output = append(output, 0)
|
||||||
|
copy(output[i+1:], output[i:])
|
||||||
|
output[i] = n
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode encodes a string as specified in section 6.3 and prepends prefix to
|
||||||
|
// the result.
|
||||||
|
//
|
||||||
|
// The "while h < length(input)" line in the specification becomes "for
|
||||||
|
// remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes.
|
||||||
|
func encode(prefix, s string) (string, error) {
|
||||||
|
output := make([]byte, len(prefix), len(prefix)+1+2*len(s))
|
||||||
|
copy(output, prefix)
|
||||||
|
delta, n, bias := int32(0), initialN, initialBias
|
||||||
|
b, remaining := int32(0), int32(0)
|
||||||
|
for _, r := range s {
|
||||||
|
if r < 0x80 {
|
||||||
|
b++
|
||||||
|
output = append(output, byte(r))
|
||||||
|
} else {
|
||||||
|
remaining++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h := b
|
||||||
|
if b > 0 {
|
||||||
|
output = append(output, '-')
|
||||||
|
}
|
||||||
|
for remaining != 0 {
|
||||||
|
m := int32(0x7fffffff)
|
||||||
|
for _, r := range s {
|
||||||
|
if m > r && r >= n {
|
||||||
|
m = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delta += (m - n) * (h + 1)
|
||||||
|
if delta < 0 {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", s)
|
||||||
|
}
|
||||||
|
n = m
|
||||||
|
for _, r := range s {
|
||||||
|
if r < n {
|
||||||
|
delta++
|
||||||
|
if delta < 0 {
|
||||||
|
return "", fmt.Errorf("idna: invalid label %q", s)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r > n {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
q := delta
|
||||||
|
for k := base; ; k += base {
|
||||||
|
t := k - bias
|
||||||
|
if t < tmin {
|
||||||
|
t = tmin
|
||||||
|
} else if t > tmax {
|
||||||
|
t = tmax
|
||||||
|
}
|
||||||
|
if q < t {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output = append(output, encodeDigit(t+(q-t)%(base-t)))
|
||||||
|
q = (q - t) / (base - t)
|
||||||
|
}
|
||||||
|
output = append(output, encodeDigit(q))
|
||||||
|
bias = adapt(delta, h+1, h == b)
|
||||||
|
delta = 0
|
||||||
|
h++
|
||||||
|
remaining--
|
||||||
|
}
|
||||||
|
delta++
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDigit(x byte) (digit int32, ok bool) {
|
||||||
|
switch {
|
||||||
|
case '0' <= x && x <= '9':
|
||||||
|
return int32(x - ('0' - 26)), true
|
||||||
|
case 'A' <= x && x <= 'Z':
|
||||||
|
return int32(x - 'A'), true
|
||||||
|
case 'a' <= x && x <= 'z':
|
||||||
|
return int32(x - 'a'), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDigit(digit int32) byte {
|
||||||
|
switch {
|
||||||
|
case 0 <= digit && digit < 26:
|
||||||
|
return byte(digit + 'a')
|
||||||
|
case 26 <= digit && digit < 36:
|
||||||
|
return byte(digit + ('0' - 26))
|
||||||
|
}
|
||||||
|
panic("idna: internal error in punycode encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapt is the bias adaptation function specified in section 6.1.
|
||||||
|
func adapt(delta, numPoints int32, firstTime bool) int32 {
|
||||||
|
if firstTime {
|
||||||
|
delta /= damp
|
||||||
|
} else {
|
||||||
|
delta /= 2
|
||||||
|
}
|
||||||
|
delta += delta / numPoints
|
||||||
|
k := int32(0)
|
||||||
|
for delta > ((base-tmin)*tmax)/2 {
|
||||||
|
delta /= base - tmin
|
||||||
|
k += base
|
||||||
|
}
|
||||||
|
return k + (base-tmin+1)*delta/(delta+skew)
|
||||||
|
}
|
|
@ -0,0 +1,713 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// This program generates table.go and table_test.go based on the authoritative
|
||||||
|
// public suffix list at https://publicsuffix.org/list/effective_tld_names.dat
|
||||||
|
//
|
||||||
|
// The version is derived from
|
||||||
|
// https://api.github.com/repos/publicsuffix/list/commits?path=public_suffix_list.dat
|
||||||
|
// and a human-readable form is at
|
||||||
|
// https://github.com/publicsuffix/list/commits/master/public_suffix_list.dat
|
||||||
|
//
|
||||||
|
// To fetch a particular git revision, such as 5c70ccd250, pass
|
||||||
|
// -url "https://raw.githubusercontent.com/publicsuffix/list/5c70ccd250/public_suffix_list.dat"
|
||||||
|
// and -version "an explicit version string".
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// These sum of these four values must be no greater than 32.
|
||||||
|
nodesBitsChildren = 9
|
||||||
|
nodesBitsICANN = 1
|
||||||
|
nodesBitsTextOffset = 15
|
||||||
|
nodesBitsTextLength = 6
|
||||||
|
|
||||||
|
// These sum of these four values must be no greater than 32.
|
||||||
|
childrenBitsWildcard = 1
|
||||||
|
childrenBitsNodeType = 2
|
||||||
|
childrenBitsHi = 14
|
||||||
|
childrenBitsLo = 14
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
maxChildren int
|
||||||
|
maxTextOffset int
|
||||||
|
maxTextLength int
|
||||||
|
maxHi uint32
|
||||||
|
maxLo uint32
|
||||||
|
)
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func u32max(a, b uint32) uint32 {
|
||||||
|
if a < b {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodeTypeNormal = 0
|
||||||
|
nodeTypeException = 1
|
||||||
|
nodeTypeParentOnly = 2
|
||||||
|
numNodeType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func nodeTypeStr(n int) string {
|
||||||
|
switch n {
|
||||||
|
case nodeTypeNormal:
|
||||||
|
return "+"
|
||||||
|
case nodeTypeException:
|
||||||
|
return "!"
|
||||||
|
case nodeTypeParentOnly:
|
||||||
|
return "o"
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultURL = "https://publicsuffix.org/list/effective_tld_names.dat"
|
||||||
|
gitCommitURL = "https://api.github.com/repos/publicsuffix/list/commits?path=public_suffix_list.dat"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
labelEncoding = map[string]uint32{}
|
||||||
|
labelsList = []string{}
|
||||||
|
labelsMap = map[string]bool{}
|
||||||
|
rules = []string{}
|
||||||
|
|
||||||
|
// validSuffixRE is used to check that the entries in the public suffix
|
||||||
|
// list are in canonical form (after Punycode encoding). Specifically,
|
||||||
|
// capital letters are not allowed.
|
||||||
|
validSuffixRE = regexp.MustCompile(`^[a-z0-9_\!\*\-\.]+$`)
|
||||||
|
|
||||||
|
shaRE = regexp.MustCompile(`"sha":"([^"]+)"`)
|
||||||
|
dateRE = regexp.MustCompile(`"committer":{[^{]+"date":"([^"]+)"`)
|
||||||
|
|
||||||
|
comments = flag.Bool("comments", false, "generate table.go comments, for debugging")
|
||||||
|
subset = flag.Bool("subset", false, "generate only a subset of the full table, for debugging")
|
||||||
|
url = flag.String("url", defaultURL, "URL of the publicsuffix.org list. If empty, stdin is read instead")
|
||||||
|
v = flag.Bool("v", false, "verbose output (to stderr)")
|
||||||
|
version = flag.String("version", "", "the effective_tld_names.dat version")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := main1(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main1() error {
|
||||||
|
flag.Parse()
|
||||||
|
if nodesBitsTextLength+nodesBitsTextOffset+nodesBitsICANN+nodesBitsChildren > 32 {
|
||||||
|
return fmt.Errorf("not enough bits to encode the nodes table")
|
||||||
|
}
|
||||||
|
if childrenBitsLo+childrenBitsHi+childrenBitsNodeType+childrenBitsWildcard > 32 {
|
||||||
|
return fmt.Errorf("not enough bits to encode the children table")
|
||||||
|
}
|
||||||
|
if *version == "" {
|
||||||
|
if *url != defaultURL {
|
||||||
|
return fmt.Errorf("-version was not specified, and the -url is not the default one")
|
||||||
|
}
|
||||||
|
sha, date, err := gitCommit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*version = fmt.Sprintf("publicsuffix.org's public_suffix_list.dat, git revision %s (%s)", sha, date)
|
||||||
|
}
|
||||||
|
var r io.Reader = os.Stdin
|
||||||
|
if *url != "" {
|
||||||
|
res, err := http.Get(*url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("bad GET status for %s: %d", *url, res.Status)
|
||||||
|
}
|
||||||
|
r = res.Body
|
||||||
|
defer res.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var root node
|
||||||
|
icann := false
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
for {
|
||||||
|
s, err := br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if strings.Contains(s, "BEGIN ICANN DOMAINS") {
|
||||||
|
icann = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "END ICANN DOMAINS") {
|
||||||
|
icann = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s == "" || strings.HasPrefix(s, "//") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, err = idna.ToASCII(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !validSuffixRE.MatchString(s) {
|
||||||
|
return fmt.Errorf("bad publicsuffix.org list data: %q", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *subset {
|
||||||
|
switch {
|
||||||
|
case s == "ac.jp" || strings.HasSuffix(s, ".ac.jp"):
|
||||||
|
case s == "ak.us" || strings.HasSuffix(s, ".ak.us"):
|
||||||
|
case s == "ao" || strings.HasSuffix(s, ".ao"):
|
||||||
|
case s == "ar" || strings.HasSuffix(s, ".ar"):
|
||||||
|
case s == "arpa" || strings.HasSuffix(s, ".arpa"):
|
||||||
|
case s == "cy" || strings.HasSuffix(s, ".cy"):
|
||||||
|
case s == "dyndns.org" || strings.HasSuffix(s, ".dyndns.org"):
|
||||||
|
case s == "jp":
|
||||||
|
case s == "kobe.jp" || strings.HasSuffix(s, ".kobe.jp"):
|
||||||
|
case s == "kyoto.jp" || strings.HasSuffix(s, ".kyoto.jp"):
|
||||||
|
case s == "om" || strings.HasSuffix(s, ".om"):
|
||||||
|
case s == "uk" || strings.HasSuffix(s, ".uk"):
|
||||||
|
case s == "uk.com" || strings.HasSuffix(s, ".uk.com"):
|
||||||
|
case s == "tw" || strings.HasSuffix(s, ".tw"):
|
||||||
|
case s == "zw" || strings.HasSuffix(s, ".zw"):
|
||||||
|
case s == "xn--p1ai" || strings.HasSuffix(s, ".xn--p1ai"):
|
||||||
|
// xn--p1ai is Russian-Cyrillic "рф".
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, s)
|
||||||
|
|
||||||
|
nt, wildcard := nodeTypeNormal, false
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s, "*."):
|
||||||
|
s, nt = s[2:], nodeTypeParentOnly
|
||||||
|
wildcard = true
|
||||||
|
case strings.HasPrefix(s, "!"):
|
||||||
|
s, nt = s[1:], nodeTypeException
|
||||||
|
}
|
||||||
|
labels := strings.Split(s, ".")
|
||||||
|
for n, i := &root, len(labels)-1; i >= 0; i-- {
|
||||||
|
label := labels[i]
|
||||||
|
n = n.child(label)
|
||||||
|
if i == 0 {
|
||||||
|
if nt != nodeTypeParentOnly && n.nodeType == nodeTypeParentOnly {
|
||||||
|
n.nodeType = nt
|
||||||
|
}
|
||||||
|
n.icann = n.icann && icann
|
||||||
|
n.wildcard = n.wildcard || wildcard
|
||||||
|
}
|
||||||
|
labelsMap[label] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labelsList = make([]string, 0, len(labelsMap))
|
||||||
|
for label := range labelsMap {
|
||||||
|
labelsList = append(labelsList, label)
|
||||||
|
}
|
||||||
|
sort.Strings(labelsList)
|
||||||
|
|
||||||
|
if err := generate(printReal, &root, "table.go"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := generate(printTest, &root, "table_test.go"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(p func(io.Writer, *node) error, root *node, filename string) error {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := p(buf, root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := format.Source(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(filename, b, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitCommit() (sha, date string, retErr error) {
|
||||||
|
res, err := http.Get(gitCommitURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return "", "", fmt.Errorf("bad GET status for %s: %d", gitCommitURL, res.Status)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if m := shaRE.FindSubmatch(b); m != nil {
|
||||||
|
sha = string(m[1])
|
||||||
|
}
|
||||||
|
if m := dateRE.FindSubmatch(b); m != nil {
|
||||||
|
date = string(m[1])
|
||||||
|
}
|
||||||
|
if sha == "" || date == "" {
|
||||||
|
retErr = fmt.Errorf("could not find commit SHA and date in %s", gitCommitURL)
|
||||||
|
}
|
||||||
|
return sha, date, retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTest(w io.Writer, n *node) error {
|
||||||
|
fmt.Fprintf(w, "// generated by go run gen.go; DO NOT EDIT\n\n")
|
||||||
|
fmt.Fprintf(w, "package publicsuffix\n\nvar rules = [...]string{\n")
|
||||||
|
for _, rule := range rules {
|
||||||
|
fmt.Fprintf(w, "%q,\n", rule)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "}\n\nvar nodeLabels = [...]string{\n")
|
||||||
|
if err := n.walk(w, printNodeLabel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "}\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printReal(w io.Writer, n *node) error {
|
||||||
|
const header = `// generated by go run gen.go; DO NOT EDIT
|
||||||
|
|
||||||
|
package publicsuffix
|
||||||
|
|
||||||
|
const version = %q
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodesBitsChildren = %d
|
||||||
|
nodesBitsICANN = %d
|
||||||
|
nodesBitsTextOffset = %d
|
||||||
|
nodesBitsTextLength = %d
|
||||||
|
|
||||||
|
childrenBitsWildcard = %d
|
||||||
|
childrenBitsNodeType = %d
|
||||||
|
childrenBitsHi = %d
|
||||||
|
childrenBitsLo = %d
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodeTypeNormal = %d
|
||||||
|
nodeTypeException = %d
|
||||||
|
nodeTypeParentOnly = %d
|
||||||
|
)
|
||||||
|
|
||||||
|
// numTLD is the number of top level domains.
|
||||||
|
const numTLD = %d
|
||||||
|
|
||||||
|
`
|
||||||
|
fmt.Fprintf(w, header, *version,
|
||||||
|
nodesBitsChildren, nodesBitsICANN, nodesBitsTextOffset, nodesBitsTextLength,
|
||||||
|
childrenBitsWildcard, childrenBitsNodeType, childrenBitsHi, childrenBitsLo,
|
||||||
|
nodeTypeNormal, nodeTypeException, nodeTypeParentOnly, len(n.children))
|
||||||
|
|
||||||
|
text := combineText(labelsList)
|
||||||
|
if text == "" {
|
||||||
|
return fmt.Errorf("internal error: makeText returned no text")
|
||||||
|
}
|
||||||
|
for _, label := range labelsList {
|
||||||
|
offset, length := strings.Index(text, label), len(label)
|
||||||
|
if offset < 0 {
|
||||||
|
return fmt.Errorf("internal error: could not find %q in text %q", label, text)
|
||||||
|
}
|
||||||
|
maxTextOffset, maxTextLength = max(maxTextOffset, offset), max(maxTextLength, length)
|
||||||
|
if offset >= 1<<nodesBitsTextOffset {
|
||||||
|
return fmt.Errorf("text offset %d is too large, or nodeBitsTextOffset is too small", offset)
|
||||||
|
}
|
||||||
|
if length >= 1<<nodesBitsTextLength {
|
||||||
|
return fmt.Errorf("text length %d is too large, or nodeBitsTextLength is too small", length)
|
||||||
|
}
|
||||||
|
labelEncoding[label] = uint32(offset)<<nodesBitsTextLength | uint32(length)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "// Text is the combined text of all labels.\nconst text = ")
|
||||||
|
for len(text) > 0 {
|
||||||
|
n, plus := len(text), ""
|
||||||
|
if n > 64 {
|
||||||
|
n, plus = 64, " +"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%q%s\n", text[:n], plus)
|
||||||
|
text = text[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := n.walk(w, assignIndexes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, `
|
||||||
|
|
||||||
|
// nodes is the list of nodes. Each node is represented as a uint32, which
|
||||||
|
// encodes the node's children, wildcard bit and node type (as an index into
|
||||||
|
// the children array), ICANN bit and text.
|
||||||
|
//
|
||||||
|
// If the table was generated with the -comments flag, there is a //-comment
|
||||||
|
// after each node's data. In it is the nodes-array indexes of the children,
|
||||||
|
// formatted as (n0x1234-n0x1256), with * denoting the wildcard bit. The
|
||||||
|
// nodeType is printed as + for normal, ! for exception, and o for parent-only
|
||||||
|
// nodes that have children but don't match a domain label in their own right.
|
||||||
|
// An I denotes an ICANN domain.
|
||||||
|
//
|
||||||
|
// The layout within the uint32, from MSB to LSB, is:
|
||||||
|
// [%2d bits] unused
|
||||||
|
// [%2d bits] children index
|
||||||
|
// [%2d bits] ICANN bit
|
||||||
|
// [%2d bits] text index
|
||||||
|
// [%2d bits] text length
|
||||||
|
var nodes = [...]uint32{
|
||||||
|
`,
|
||||||
|
32-nodesBitsChildren-nodesBitsICANN-nodesBitsTextOffset-nodesBitsTextLength,
|
||||||
|
nodesBitsChildren, nodesBitsICANN, nodesBitsTextOffset, nodesBitsTextLength)
|
||||||
|
if err := n.walk(w, printNode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, `}
|
||||||
|
|
||||||
|
// children is the list of nodes' children, the parent's wildcard bit and the
|
||||||
|
// parent's node type. If a node has no children then their children index
|
||||||
|
// will be in the range [0, 6), depending on the wildcard bit and node type.
|
||||||
|
//
|
||||||
|
// The layout within the uint32, from MSB to LSB, is:
|
||||||
|
// [%2d bits] unused
|
||||||
|
// [%2d bits] wildcard bit
|
||||||
|
// [%2d bits] node type
|
||||||
|
// [%2d bits] high nodes index (exclusive) of children
|
||||||
|
// [%2d bits] low nodes index (inclusive) of children
|
||||||
|
var children=[...]uint32{
|
||||||
|
`,
|
||||||
|
32-childrenBitsWildcard-childrenBitsNodeType-childrenBitsHi-childrenBitsLo,
|
||||||
|
childrenBitsWildcard, childrenBitsNodeType, childrenBitsHi, childrenBitsLo)
|
||||||
|
for i, c := range childrenEncoding {
|
||||||
|
s := "---------------"
|
||||||
|
lo := c & (1<<childrenBitsLo - 1)
|
||||||
|
hi := (c >> childrenBitsLo) & (1<<childrenBitsHi - 1)
|
||||||
|
if lo != hi {
|
||||||
|
s = fmt.Sprintf("n0x%04x-n0x%04x", lo, hi)
|
||||||
|
}
|
||||||
|
nodeType := int(c>>(childrenBitsLo+childrenBitsHi)) & (1<<childrenBitsNodeType - 1)
|
||||||
|
wildcard := c>>(childrenBitsLo+childrenBitsHi+childrenBitsNodeType) != 0
|
||||||
|
if *comments {
|
||||||
|
fmt.Fprintf(w, "0x%08x, // c0x%04x (%s)%s %s\n",
|
||||||
|
c, i, s, wildcardStr(wildcard), nodeTypeStr(nodeType))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "0x%x,\n", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "}\n\n")
|
||||||
|
fmt.Fprintf(w, "// max children %d (capacity %d)\n", maxChildren, 1<<nodesBitsChildren-1)
|
||||||
|
fmt.Fprintf(w, "// max text offset %d (capacity %d)\n", maxTextOffset, 1<<nodesBitsTextOffset-1)
|
||||||
|
fmt.Fprintf(w, "// max text length %d (capacity %d)\n", maxTextLength, 1<<nodesBitsTextLength-1)
|
||||||
|
fmt.Fprintf(w, "// max hi %d (capacity %d)\n", maxHi, 1<<childrenBitsHi-1)
|
||||||
|
fmt.Fprintf(w, "// max lo %d (capacity %d)\n", maxLo, 1<<childrenBitsLo-1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
label string
|
||||||
|
nodeType int
|
||||||
|
icann bool
|
||||||
|
wildcard bool
|
||||||
|
// nodesIndex and childrenIndex are the index of this node in the nodes
|
||||||
|
// and the index of its children offset/length in the children arrays.
|
||||||
|
nodesIndex, childrenIndex int
|
||||||
|
// firstChild is the index of this node's first child, or zero if this
|
||||||
|
// node has no children.
|
||||||
|
firstChild int
|
||||||
|
// children are the node's children, in strictly increasing node label order.
|
||||||
|
children []*node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) walk(w io.Writer, f func(w1 io.Writer, n1 *node) error) error {
|
||||||
|
if err := f(w, n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, c := range n.children {
|
||||||
|
if err := c.walk(w, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// child returns the child of n with the given label. The child is created if
|
||||||
|
// it did not exist beforehand.
|
||||||
|
func (n *node) child(label string) *node {
|
||||||
|
for _, c := range n.children {
|
||||||
|
if c.label == label {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c := &node{
|
||||||
|
label: label,
|
||||||
|
nodeType: nodeTypeParentOnly,
|
||||||
|
icann: true,
|
||||||
|
}
|
||||||
|
n.children = append(n.children, c)
|
||||||
|
sort.Sort(byLabel(n.children))
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type byLabel []*node
|
||||||
|
|
||||||
|
func (b byLabel) Len() int { return len(b) }
|
||||||
|
func (b byLabel) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||||
|
func (b byLabel) Less(i, j int) bool { return b[i].label < b[j].label }
|
||||||
|
|
||||||
|
var nextNodesIndex int
|
||||||
|
|
||||||
|
// childrenEncoding are the encoded entries in the generated children array.
|
||||||
|
// All these pre-defined entries have no children.
|
||||||
|
var childrenEncoding = []uint32{
|
||||||
|
0 << (childrenBitsLo + childrenBitsHi), // Without wildcard bit, nodeTypeNormal.
|
||||||
|
1 << (childrenBitsLo + childrenBitsHi), // Without wildcard bit, nodeTypeException.
|
||||||
|
2 << (childrenBitsLo + childrenBitsHi), // Without wildcard bit, nodeTypeParentOnly.
|
||||||
|
4 << (childrenBitsLo + childrenBitsHi), // With wildcard bit, nodeTypeNormal.
|
||||||
|
5 << (childrenBitsLo + childrenBitsHi), // With wildcard bit, nodeTypeException.
|
||||||
|
6 << (childrenBitsLo + childrenBitsHi), // With wildcard bit, nodeTypeParentOnly.
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstCallToAssignIndexes = true
|
||||||
|
|
||||||
|
func assignIndexes(w io.Writer, n *node) error {
|
||||||
|
if len(n.children) != 0 {
|
||||||
|
// Assign nodesIndex.
|
||||||
|
n.firstChild = nextNodesIndex
|
||||||
|
for _, c := range n.children {
|
||||||
|
c.nodesIndex = nextNodesIndex
|
||||||
|
nextNodesIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// The root node's children is implicit.
|
||||||
|
if firstCallToAssignIndexes {
|
||||||
|
firstCallToAssignIndexes = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign childrenIndex.
|
||||||
|
maxChildren = max(maxChildren, len(childrenEncoding))
|
||||||
|
if len(childrenEncoding) >= 1<<nodesBitsChildren {
|
||||||
|
return fmt.Errorf("children table size %d is too large, or nodeBitsChildren is too small", len(childrenEncoding))
|
||||||
|
}
|
||||||
|
n.childrenIndex = len(childrenEncoding)
|
||||||
|
lo := uint32(n.firstChild)
|
||||||
|
hi := lo + uint32(len(n.children))
|
||||||
|
maxLo, maxHi = u32max(maxLo, lo), u32max(maxHi, hi)
|
||||||
|
if lo >= 1<<childrenBitsLo {
|
||||||
|
return fmt.Errorf("children lo %d is too large, or childrenBitsLo is too small", lo)
|
||||||
|
}
|
||||||
|
if hi >= 1<<childrenBitsHi {
|
||||||
|
return fmt.Errorf("children hi %d is too large, or childrenBitsHi is too small", hi)
|
||||||
|
}
|
||||||
|
enc := hi<<childrenBitsLo | lo
|
||||||
|
enc |= uint32(n.nodeType) << (childrenBitsLo + childrenBitsHi)
|
||||||
|
if n.wildcard {
|
||||||
|
enc |= 1 << (childrenBitsLo + childrenBitsHi + childrenBitsNodeType)
|
||||||
|
}
|
||||||
|
childrenEncoding = append(childrenEncoding, enc)
|
||||||
|
} else {
|
||||||
|
n.childrenIndex = n.nodeType
|
||||||
|
if n.wildcard {
|
||||||
|
n.childrenIndex += numNodeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNode(w io.Writer, n *node) error {
|
||||||
|
for _, c := range n.children {
|
||||||
|
s := "---------------"
|
||||||
|
if len(c.children) != 0 {
|
||||||
|
s = fmt.Sprintf("n0x%04x-n0x%04x", c.firstChild, c.firstChild+len(c.children))
|
||||||
|
}
|
||||||
|
encoding := labelEncoding[c.label]
|
||||||
|
if c.icann {
|
||||||
|
encoding |= 1 << (nodesBitsTextLength + nodesBitsTextOffset)
|
||||||
|
}
|
||||||
|
encoding |= uint32(c.childrenIndex) << (nodesBitsTextLength + nodesBitsTextOffset + nodesBitsICANN)
|
||||||
|
if *comments {
|
||||||
|
fmt.Fprintf(w, "0x%08x, // n0x%04x c0x%04x (%s)%s %s %s %s\n",
|
||||||
|
encoding, c.nodesIndex, c.childrenIndex, s, wildcardStr(c.wildcard),
|
||||||
|
nodeTypeStr(c.nodeType), icannStr(c.icann), c.label,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "0x%x,\n", encoding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNodeLabel(w io.Writer, n *node) error {
|
||||||
|
for _, c := range n.children {
|
||||||
|
fmt.Fprintf(w, "%q,\n", c.label)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func icannStr(icann bool) string {
|
||||||
|
if icann {
|
||||||
|
return "I"
|
||||||
|
}
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
|
||||||
|
func wildcardStr(wildcard bool) string {
|
||||||
|
if wildcard {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// combineText combines all the strings in labelsList to form one giant string.
|
||||||
|
// Overlapping strings will be merged: "arpa" and "parliament" could yield
|
||||||
|
// "arparliament".
|
||||||
|
func combineText(labelsList []string) string {
|
||||||
|
beforeLength := 0
|
||||||
|
for _, s := range labelsList {
|
||||||
|
beforeLength += len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := crush(removeSubstrings(labelsList))
|
||||||
|
if *v {
|
||||||
|
fmt.Fprintf(os.Stderr, "crushed %d bytes to become %d bytes\n", beforeLength, len(text))
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
type byLength []string
|
||||||
|
|
||||||
|
func (s byLength) Len() int { return len(s) }
|
||||||
|
func (s byLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s byLength) Less(i, j int) bool { return len(s[i]) < len(s[j]) }
|
||||||
|
|
||||||
|
// removeSubstrings returns a copy of its input with any strings removed
|
||||||
|
// that are substrings of other provided strings.
|
||||||
|
func removeSubstrings(input []string) []string {
|
||||||
|
// Make a copy of input.
|
||||||
|
ss := append(make([]string, 0, len(input)), input...)
|
||||||
|
sort.Sort(byLength(ss))
|
||||||
|
|
||||||
|
for i, shortString := range ss {
|
||||||
|
// For each string, only consider strings higher than it in sort order, i.e.
|
||||||
|
// of equal length or greater.
|
||||||
|
for _, longString := range ss[i+1:] {
|
||||||
|
if strings.Contains(longString, shortString) {
|
||||||
|
ss[i] = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the empty strings.
|
||||||
|
sort.Strings(ss)
|
||||||
|
for len(ss) > 0 && ss[0] == "" {
|
||||||
|
ss = ss[1:]
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// crush combines a list of strings, taking advantage of overlaps. It returns a
|
||||||
|
// single string that contains each input string as a substring.
|
||||||
|
func crush(ss []string) string {
|
||||||
|
maxLabelLen := 0
|
||||||
|
for _, s := range ss {
|
||||||
|
if maxLabelLen < len(s) {
|
||||||
|
maxLabelLen = len(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for prefixLen := maxLabelLen; prefixLen > 0; prefixLen-- {
|
||||||
|
prefixes := makePrefixMap(ss, prefixLen)
|
||||||
|
for i, s := range ss {
|
||||||
|
if len(s) <= prefixLen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mergeLabel(ss, i, prefixLen, prefixes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(ss, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeLabel merges the label at ss[i] with the first available matching label
|
||||||
|
// in prefixMap, where the last "prefixLen" characters in ss[i] match the first
|
||||||
|
// "prefixLen" characters in the matching label.
|
||||||
|
// It will merge ss[i] repeatedly until no more matches are available.
|
||||||
|
// All matching labels merged into ss[i] are replaced by "".
|
||||||
|
func mergeLabel(ss []string, i, prefixLen int, prefixes prefixMap) {
|
||||||
|
s := ss[i]
|
||||||
|
suffix := s[len(s)-prefixLen:]
|
||||||
|
for _, j := range prefixes[suffix] {
|
||||||
|
// Empty strings mean "already used." Also avoid merging with self.
|
||||||
|
if ss[j] == "" || i == j {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *v {
|
||||||
|
fmt.Fprintf(os.Stderr, "%d-length overlap at (%4d,%4d): %q and %q share %q\n",
|
||||||
|
prefixLen, i, j, ss[i], ss[j], suffix)
|
||||||
|
}
|
||||||
|
ss[i] += ss[j][prefixLen:]
|
||||||
|
ss[j] = ""
|
||||||
|
// ss[i] has a new suffix, so merge again if possible.
|
||||||
|
// Note: we only have to merge again at the same prefix length. Shorter
|
||||||
|
// prefix lengths will be handled in the next iteration of crush's for loop.
|
||||||
|
// Can there be matches for longer prefix lengths, introduced by the merge?
|
||||||
|
// I believe that any such matches would by necessity have been eliminated
|
||||||
|
// during substring removal or merged at a higher prefix length. For
|
||||||
|
// instance, in crush("abc", "cde", "bcdef"), combining "abc" and "cde"
|
||||||
|
// would yield "abcde", which could be merged with "bcdef." However, in
|
||||||
|
// practice "cde" would already have been elimintated by removeSubstrings.
|
||||||
|
mergeLabel(ss, i, prefixLen, prefixes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefixMap maps from a prefix to a list of strings containing that prefix. The
|
||||||
|
// list of strings is represented as indexes into a slice of strings stored
|
||||||
|
// elsewhere.
|
||||||
|
type prefixMap map[string][]int
|
||||||
|
|
||||||
|
// makePrefixMap constructs a prefixMap from a slice of strings.
|
||||||
|
func makePrefixMap(ss []string, prefixLen int) prefixMap {
|
||||||
|
prefixes := make(prefixMap)
|
||||||
|
for i, s := range ss {
|
||||||
|
// We use < rather than <= because if a label matches on a prefix equal to
|
||||||
|
// its full length, that's actually a substring match handled by
|
||||||
|
// removeSubstrings.
|
||||||
|
if prefixLen < len(s) {
|
||||||
|
prefix := s[:prefixLen]
|
||||||
|
prefixes[prefix] = append(prefixes[prefix], i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefixes
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:generate go run gen.go
|
||||||
|
|
||||||
|
// Package publicsuffix provides a public suffix list based on data from
|
||||||
|
// http://publicsuffix.org/. A public suffix is one under which Internet users
|
||||||
|
// can directly register names.
|
||||||
|
package publicsuffix // import "golang.org/x/net/publicsuffix"
|
||||||
|
|
||||||
|
// TODO: specify case sensitivity and leading/trailing dot behavior for
|
||||||
|
// func PublicSuffix and func EffectiveTLDPlusOne.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List implements the cookiejar.PublicSuffixList interface by calling the
|
||||||
|
// PublicSuffix function.
|
||||||
|
var List cookiejar.PublicSuffixList = list{}
|
||||||
|
|
||||||
|
type list struct{}
|
||||||
|
|
||||||
|
func (list) PublicSuffix(domain string) string {
|
||||||
|
ps, _ := PublicSuffix(domain)
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list) String() string {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicSuffix returns the public suffix of the domain using a copy of the
|
||||||
|
// publicsuffix.org database compiled into the library.
|
||||||
|
//
|
||||||
|
// icann is whether the public suffix is managed by the Internet Corporation
|
||||||
|
// for Assigned Names and Numbers. If not, the public suffix is privately
|
||||||
|
// managed. For example, foo.org and foo.co.uk are ICANN domains,
|
||||||
|
// foo.dyndns.org and foo.blogspot.co.uk are private domains.
|
||||||
|
//
|
||||||
|
// Use cases for distinguishing ICANN domains like foo.com from private
|
||||||
|
// domains like foo.appspot.com can be found at
|
||||||
|
// https://wiki.mozilla.org/Public_Suffix_List/Use_Cases
|
||||||
|
func PublicSuffix(domain string) (publicSuffix string, icann bool) {
|
||||||
|
lo, hi := uint32(0), uint32(numTLD)
|
||||||
|
s, suffix, wildcard := domain, len(domain), false
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
dot := strings.LastIndex(s, ".")
|
||||||
|
if wildcard {
|
||||||
|
suffix = 1 + dot
|
||||||
|
}
|
||||||
|
if lo == hi {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
f := find(s[1+dot:], lo, hi)
|
||||||
|
if f == notFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
u := nodes[f] >> (nodesBitsTextOffset + nodesBitsTextLength)
|
||||||
|
icann = u&(1<<nodesBitsICANN-1) != 0
|
||||||
|
u >>= nodesBitsICANN
|
||||||
|
u = children[u&(1<<nodesBitsChildren-1)]
|
||||||
|
lo = u & (1<<childrenBitsLo - 1)
|
||||||
|
u >>= childrenBitsLo
|
||||||
|
hi = u & (1<<childrenBitsHi - 1)
|
||||||
|
u >>= childrenBitsHi
|
||||||
|
switch u & (1<<childrenBitsNodeType - 1) {
|
||||||
|
case nodeTypeNormal:
|
||||||
|
suffix = 1 + dot
|
||||||
|
case nodeTypeException:
|
||||||
|
suffix = 1 + len(s)
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
u >>= childrenBitsNodeType
|
||||||
|
wildcard = u&(1<<childrenBitsWildcard-1) != 0
|
||||||
|
|
||||||
|
if dot == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s = s[:dot]
|
||||||
|
}
|
||||||
|
if suffix == len(domain) {
|
||||||
|
// If no rules match, the prevailing rule is "*".
|
||||||
|
return domain[1+strings.LastIndex(domain, "."):], icann
|
||||||
|
}
|
||||||
|
return domain[suffix:], icann
|
||||||
|
}
|
||||||
|
|
||||||
|
const notFound uint32 = 1<<32 - 1
|
||||||
|
|
||||||
|
// find returns the index of the node in the range [lo, hi) whose label equals
|
||||||
|
// label, or notFound if there is no such node. The range is assumed to be in
|
||||||
|
// strictly increasing node label order.
|
||||||
|
func find(label string, lo, hi uint32) uint32 {
|
||||||
|
for lo < hi {
|
||||||
|
mid := lo + (hi-lo)/2
|
||||||
|
s := nodeLabel(mid)
|
||||||
|
if s < label {
|
||||||
|
lo = mid + 1
|
||||||
|
} else if s == label {
|
||||||
|
return mid
|
||||||
|
} else {
|
||||||
|
hi = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeLabel returns the label for the i'th node.
|
||||||
|
func nodeLabel(i uint32) string {
|
||||||
|
x := nodes[i]
|
||||||
|
length := x & (1<<nodesBitsTextLength - 1)
|
||||||
|
x >>= nodesBitsTextLength
|
||||||
|
offset := x & (1<<nodesBitsTextOffset - 1)
|
||||||
|
return text[offset : offset+length]
|
||||||
|
}
|
||||||
|
|
||||||
|
// EffectiveTLDPlusOne returns the effective top level domain plus one more
|
||||||
|
// label. For example, the eTLD+1 for "foo.bar.golang.org" is "golang.org".
|
||||||
|
func EffectiveTLDPlusOne(domain string) (string, error) {
|
||||||
|
suffix, _ := PublicSuffix(domain)
|
||||||
|
if len(domain) <= len(suffix) {
|
||||||
|
return "", fmt.Errorf("publicsuffix: cannot derive eTLD+1 for domain %q", domain)
|
||||||
|
}
|
||||||
|
i := len(domain) - len(suffix) - 1
|
||||||
|
if domain[i] != '.' {
|
||||||
|
return "", fmt.Errorf("publicsuffix: invalid public suffix %q for domain %q", suffix, domain)
|
||||||
|
}
|
||||||
|
return domain[1+strings.LastIndex(domain[:i], "."):], nil
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
|
@ -0,0 +1,584 @@
|
||||||
|
# resty [![Build Status](https://travis-ci.org/go-resty/resty.svg?branch=master)](https://travis-ci.org/go-resty/resty) [![codecov](https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg)](https://codecov.io/gh/go-resty/resty/branch/master) [![GoReport](https://goreportcard.com/badge/go-resty/resty)](https://goreportcard.com/report/go-resty/resty) [![GoDoc](https://godoc.org/github.com/go-resty/resty?status.svg)](https://godoc.org/github.com/go-resty/resty) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
|
||||||
|
|
||||||
|
Simple HTTP and REST client for Go inspired by Ruby rest-client. [Features](#features) section describes in detail about resty capabilities.
|
||||||
|
|
||||||
|
***v0.9 [released](https://github.com/go-resty/resty/releases/latest) and tagged on Nov 01, 2016.***
|
||||||
|
|
||||||
|
*Since Go v1.6 HTTP/2 & HTTP/1.1 protocol is used transparently. `Resty` works fine with HTTP/2 and HTTP/1.1.*
|
||||||
|
|
||||||
|
#### Roadmap
|
||||||
|
***v0.10***
|
||||||
|
|
||||||
|
I will be focusing on golint, etc. code quality improvements (may have very minor breaking change due to golint)
|
||||||
|
|
||||||
|
***v1.0 (Around New Year)***
|
||||||
|
|
||||||
|
Go Resty first released on Sep 15, 2015 then go-resty grew gradually as a very handy and helpful library of HTTP & REST Client in the golang community. I'm planning to freeze API and make v1.0 release.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
* GET, POST, PUT, DELETE, HEAD, PATCH and OPTIONS
|
||||||
|
* Simple and chainable methods for settings and request
|
||||||
|
* Request Body can be `string`, `[]byte`, `struct`, `map`, `slice` and `io.Reader` too
|
||||||
|
* Auto detects `Content-Type`
|
||||||
|
* [Response](https://godoc.org/github.com/go-resty/resty#Response) object gives you more possibility
|
||||||
|
* Access as `[]byte` array - `response.Body()` OR Access as `string` - `response.String()`
|
||||||
|
* Know your `response.Time()` and when we `response.ReceivedAt()`
|
||||||
|
* Automatic marshal and unmarshal for `JSON` and `XML` content type
|
||||||
|
* Default is `JSON`, if you supply `struct/map` without header `Content-Type`
|
||||||
|
* Easy to upload one or more file(s) via `multipart/form-data`
|
||||||
|
* Backoff Retry Mechanism with retry condition function [reference](retry_test.go)
|
||||||
|
* resty client [Request](https://godoc.org/github.com/go-resty/resty#Client.OnBeforeRequest) and [Response](https://godoc.org/github.com/go-resty/resty#Client.OnAfterResponse) middlewares
|
||||||
|
* Authorization option of `BasicAuth` and `Bearer` token
|
||||||
|
* Set request `ContentLength` value for all request or particular request
|
||||||
|
* Choose between HTTP and REST mode. Default is `REST`
|
||||||
|
* `HTTP` - default up to 10 redirects and no automatic response unmarshal
|
||||||
|
* `REST` - defaults to no redirects and automatic response marshal/unmarshal for `JSON` & `XML`
|
||||||
|
* Custom [Root Certificates](https://godoc.org/github.com/go-resty/resty#Client.SetRootCertificate) and Client [Certificates](https://godoc.org/github.com/go-resty/resty#Client.SetCertificates)
|
||||||
|
* Download/Save HTTP response directly into File, like `curl -o` flag. See [SetOutputDirectory](https://godoc.org/github.com/go-resty/resty#Client.SetOutputDirectory) & [SetOutput](https://godoc.org/github.com/go-resty/resty#Request.SetOutput).
|
||||||
|
* Cookies for your request and CookieJar support
|
||||||
|
* Client settings like `Timeout`, `RedirectPolicy`, `Proxy`, `TLSClientConfig`, `Transport`, etc.
|
||||||
|
* Client API design
|
||||||
|
* Have client level settings & options and also override at Request level if you want to
|
||||||
|
* Create Multiple clients if want to `resty.New()`
|
||||||
|
* goroutine concurrent safe
|
||||||
|
* Debug mode - clean and informative logging presentation
|
||||||
|
* Gzip - I'm not doing anything here. Go does it automatically
|
||||||
|
* Well tested client library
|
||||||
|
|
||||||
|
resty tested with Go `v1.2` and above.
|
||||||
|
|
||||||
|
#### Included Batteries
|
||||||
|
* Redirect Policies - see [how to use in action](#redirect-policy)
|
||||||
|
* NoRedirectPolicy
|
||||||
|
* FlexibleRedirectPolicy
|
||||||
|
* DomainCheckRedirectPolicy
|
||||||
|
* etc. [more info](redirect.go)
|
||||||
|
* Backoff Retry Mechanism with retry condition function [reference](retry_test.go)
|
||||||
|
* etc (upcoming - throw your idea's [here](https://github.com/go-resty/resty/issues)).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
#### Stable - Version
|
||||||
|
Please refer section [Versioning](#versioning) for detailed info.
|
||||||
|
```sh
|
||||||
|
# install the library
|
||||||
|
go get -u gopkg.in/resty.v0
|
||||||
|
```
|
||||||
|
#### Latest
|
||||||
|
```sh
|
||||||
|
# install the latest & greatest library
|
||||||
|
go get -u github.com/go-resty/resty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Following samples will assist you to become as much comfortable as possible with resty library. Resty comes with ready to use DefaultClient.
|
||||||
|
|
||||||
|
Import resty into your code and refer it as `resty`.
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"gopkg.in/resty.v0"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Simple GET
|
||||||
|
```go
|
||||||
|
// GET request
|
||||||
|
resp, err := resty.R().Get("http://httpbin.org/get")
|
||||||
|
|
||||||
|
// explore response object
|
||||||
|
fmt.Printf("\nError: %v", err)
|
||||||
|
fmt.Printf("\nResponse Status Code: %v", resp.StatusCode())
|
||||||
|
fmt.Printf("\nResponse Status: %v", resp.Status())
|
||||||
|
fmt.Printf("\nResponse Time: %v", resp.Time())
|
||||||
|
fmt.Printf("\nResponse Recevied At: %v", resp.ReceivedAt())
|
||||||
|
fmt.Printf("\nResponse Body: %v", resp) // or resp.String() or string(resp.Body())
|
||||||
|
// more...
|
||||||
|
|
||||||
|
/* Output
|
||||||
|
Error: <nil>
|
||||||
|
Response Status Code: 200
|
||||||
|
Response Status: 200 OK
|
||||||
|
Response Time: 644.290186ms
|
||||||
|
Response Recevied At: 2015-09-15 12:05:28.922780103 -0700 PDT
|
||||||
|
Response Body: {
|
||||||
|
"args": {},
|
||||||
|
"headers": {
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
"Host": "httpbin.org",
|
||||||
|
"User-Agent": "go-resty v0.1 - https://github.com/go-resty/resty"
|
||||||
|
},
|
||||||
|
"origin": "0.0.0.0",
|
||||||
|
"url": "http://httpbin.org/get"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
#### Enhanced GET
|
||||||
|
```go
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetQueryParams(map[string]string{
|
||||||
|
"page_no": "1",
|
||||||
|
"limit": "20",
|
||||||
|
"sort":"name",
|
||||||
|
"order": "asc",
|
||||||
|
"random":strconv.FormatInt(time.Now().Unix(), 10),
|
||||||
|
}).
|
||||||
|
SetHeader("Accept", "application/json").
|
||||||
|
SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
|
||||||
|
Get("/search_result")
|
||||||
|
|
||||||
|
|
||||||
|
// Sample of using Request.SetQueryString method
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more").
|
||||||
|
SetHeader("Accept", "application/json").
|
||||||
|
SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
|
||||||
|
Get("/show_product")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Various POST method combinations
|
||||||
|
```go
|
||||||
|
// POST JSON string
|
||||||
|
// No need to set content type, if you have client level setting
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(`{"username":"testuser", "password":"testpass"}`).
|
||||||
|
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||||
|
Post("https://myapp.com/login")
|
||||||
|
|
||||||
|
// POST []byte array
|
||||||
|
// No need to set content type, if you have client level setting
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody([]byte(`{"username":"testuser", "password":"testpass"}`)).
|
||||||
|
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||||
|
Post("https://myapp.com/login")
|
||||||
|
|
||||||
|
// POST Struct, default is JSON content type. No need to set one
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetBody(User{Username: "testuser", Password: "testpass"}).
|
||||||
|
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||||
|
SetError(&AuthError{}). // or SetError(AuthError{}).
|
||||||
|
Post("https://myapp.com/login")
|
||||||
|
|
||||||
|
// POST Map, default is JSON content type. No need to set one
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
|
||||||
|
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||||
|
SetError(&AuthError{}). // or SetError(AuthError{}).
|
||||||
|
Post("https://myapp.com/login")
|
||||||
|
|
||||||
|
// POST of raw bytes for file upload. For example: upload file to Dropbox
|
||||||
|
fileBytes, _ := ioutil.ReadFile("/Users/jeeva/mydocument.pdf")
|
||||||
|
|
||||||
|
// See we are not setting content-type header, since go-resty automatically detects Content-Type for you
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetBody(fileBytes).
|
||||||
|
SetContentLength(true). // Dropbox expects this value
|
||||||
|
SetAuthToken("<your-auth-token>").
|
||||||
|
SetError(&DropboxError{}). // or SetError(DropboxError{}).
|
||||||
|
Post("https://content.dropboxapi.com/1/files_put/auto/resty/mydocument.pdf") // for upload Dropbox supports PUT too
|
||||||
|
|
||||||
|
// Note: resty detects Content-Type for request body/payload if content type header is not set.
|
||||||
|
// * For struct and map data type defaults to 'application/json'
|
||||||
|
// * Fallback is plain text content type
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample PUT
|
||||||
|
You can use various combinations of `PUT` method call like demonstrated for `POST`.
|
||||||
|
```go
|
||||||
|
// Note: This is one sample of PUT method usage, refer POST for more combination
|
||||||
|
|
||||||
|
// Request goes as JSON content type
|
||||||
|
// No need to set auth token, error, if you have client level settings
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetBody(Article{
|
||||||
|
Title: "go-resty",
|
||||||
|
Content: "This is my article content, oh ya!",
|
||||||
|
Author: "Jeevanandam M",
|
||||||
|
Tags: []string{"article", "sample", "resty"},
|
||||||
|
}).
|
||||||
|
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||||
|
SetError(&Error{}). // or SetError(Error{}).
|
||||||
|
Put("https://myapp.com/article/1234")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample PATCH
|
||||||
|
You can use various combinations of `PATCH` method call like demonstrated for `POST`.
|
||||||
|
```go
|
||||||
|
// Note: This is one sample of PUT method usage, refer POST for more combination
|
||||||
|
|
||||||
|
// Request goes as JSON content type
|
||||||
|
// No need to set auth token, error, if you have client level settings
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetBody(Article{
|
||||||
|
Tags: []string{"new tag1", "new tag2"},
|
||||||
|
}).
|
||||||
|
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||||
|
SetError(&Error{}). // or SetError(Error{}).
|
||||||
|
Patch("https://myapp.com/articles/1234")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample DELETE, HEAD, OPTIONS
|
||||||
|
```go
|
||||||
|
// DELETE a article
|
||||||
|
// No need to set auth token, error, if you have client level settings
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||||
|
SetError(&Error{}). // or SetError(Error{}).
|
||||||
|
Delete("https://myapp.com/articles/1234")
|
||||||
|
|
||||||
|
// DELETE a articles with payload/body as a JSON string
|
||||||
|
// No need to set auth token, error, if you have client level settings
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||||
|
SetError(&Error{}). // or SetError(Error{}).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(`{article_ids: [1002, 1006, 1007, 87683, 45432] }`).
|
||||||
|
Delete("https://myapp.com/articles")
|
||||||
|
|
||||||
|
// HEAD of resource
|
||||||
|
// No need to set auth token, if you have client level settings
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||||
|
Head("https://myapp.com/videos/hi-res-video")
|
||||||
|
|
||||||
|
// OPTIONS of resource
|
||||||
|
// No need to set auth token, if you have client level settings
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||||
|
Options("https://myapp.com/servers/nyc-dc-01")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multipart File(s) upload
|
||||||
|
#### Using io.Reader
|
||||||
|
```go
|
||||||
|
profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png")
|
||||||
|
notesBytes, _ := ioutil.ReadFile("/Users/jeeva/text-file.txt")
|
||||||
|
|
||||||
|
resp, err := dclr().
|
||||||
|
SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)).
|
||||||
|
SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"first_name": "Jeevanandam",
|
||||||
|
"last_name": "M",
|
||||||
|
}).
|
||||||
|
Post(t"http://myapp.com/upload")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using File directly from Path
|
||||||
|
```go
|
||||||
|
// Single file scenario
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetFile("profile_img", "/Users/jeeva/test-img.png").
|
||||||
|
Post("http://myapp.com/upload")
|
||||||
|
|
||||||
|
// Multiple files scenario
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetFiles(map[string]string{
|
||||||
|
"profile_img": "/Users/jeeva/test-img.png",
|
||||||
|
"notes": "/Users/jeeva/text-file.txt",
|
||||||
|
}).
|
||||||
|
Post("http://myapp.com/upload")
|
||||||
|
|
||||||
|
// Multipart of form fields and files
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetFiles(map[string]string{
|
||||||
|
"profile_img": "/Users/jeeva/test-img.png",
|
||||||
|
"notes": "/Users/jeeva/text-file.txt",
|
||||||
|
}).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"first_name": "Jeevanandam",
|
||||||
|
"last_name": "M",
|
||||||
|
"zip_code": "00001",
|
||||||
|
"city": "my city",
|
||||||
|
"access_token": "C6A79608-782F-4ED0-A11D-BD82FAD829CD",
|
||||||
|
}).
|
||||||
|
Post("http://myapp.com/profile")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sample Form submision
|
||||||
|
```go
|
||||||
|
// just mentioning about POST as an example with simple flow
|
||||||
|
// User Login
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"username": "jeeva",
|
||||||
|
"password": "mypass",
|
||||||
|
}).
|
||||||
|
Post("http://myapp.com/login")
|
||||||
|
|
||||||
|
// Followed by profile update
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"first_name": "Jeevanandam",
|
||||||
|
"last_name": "M",
|
||||||
|
"zip_code": "00001",
|
||||||
|
"city": "new city update",
|
||||||
|
}).
|
||||||
|
Post("http://myapp.com/profile")
|
||||||
|
|
||||||
|
// Multi value form data
|
||||||
|
criteria := url.Values{
|
||||||
|
"search_criteria": []string{"book", "glass", "pencil"},
|
||||||
|
}
|
||||||
|
resp, err := resty.R().
|
||||||
|
SetMultiValueFormData(criteria).
|
||||||
|
Post("http://myapp.com/search")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Save HTTP Response into File
|
||||||
|
```go
|
||||||
|
// Setting output directory path, If directory not exists then resty creates one!
|
||||||
|
// This is optional one, if you're planning using absoule path in
|
||||||
|
// `Request.SetOutput` and can used together.
|
||||||
|
resty.SetOutputDirectory("/Users/jeeva/Downloads")
|
||||||
|
|
||||||
|
// HTTP response gets saved into file, similar to curl -o flag
|
||||||
|
_, err := resty.R().
|
||||||
|
SetOutput("plugin/ReplyWithHeader-v5.1-beta.zip").
|
||||||
|
Get("http://bit.ly/1LouEKr")
|
||||||
|
|
||||||
|
// OR using absolute path
|
||||||
|
// Note: output directory path is not used for absoulte path
|
||||||
|
_, err := resty.R().
|
||||||
|
SetOutput("/MyDownloads/plugin/ReplyWithHeader-v5.1-beta.zip").
|
||||||
|
Get("http://bit.ly/1LouEKr")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Request and Response Middleware
|
||||||
|
Resty provides middleware ability to manipulate for Request and Response. It is more flexible than callback approach.
|
||||||
|
```go
|
||||||
|
// Registering Request Middleware
|
||||||
|
resty.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
|
||||||
|
// Now you have access to Client and current Request object
|
||||||
|
// manipulate it as per your need
|
||||||
|
|
||||||
|
return nil // if its success otherwise return error
|
||||||
|
})
|
||||||
|
|
||||||
|
// Registering Response Middleware
|
||||||
|
resty.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
|
||||||
|
// Now you have access to Client and current Response object
|
||||||
|
// manipulate it as per your need
|
||||||
|
|
||||||
|
return nil // if its success otherwise return error
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Redirect Policy
|
||||||
|
Resty provides few ready to use redirect policy(s) also it supports multiple policies together.
|
||||||
|
```go
|
||||||
|
// Assign Client Redirect Policy. Create one as per you need
|
||||||
|
resty.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
|
||||||
|
|
||||||
|
// Wanna multiple policies such as redirect count, domain name check, etc
|
||||||
|
resty.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20),
|
||||||
|
resty.DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Custom Redirect Policy
|
||||||
|
Implement [RedirectPolicy](redirect.go#L20) interface and register it with resty client. Have a look [redirect.go](redirect.go) for more information.
|
||||||
|
```go
|
||||||
|
// Using raw func into resty.SetRedirectPolicy
|
||||||
|
resty.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||||
|
// Implement your logic here
|
||||||
|
|
||||||
|
// return nil for continue redirect otherwise return error to stop/prevent redirect
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
//---------------------------------------------------
|
||||||
|
|
||||||
|
// Using struct create more flexible redirect policy
|
||||||
|
type CustomRedirectPolicy struct {
|
||||||
|
// variables goes here
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomRedirectPolicy) Apply(req *http.Request, via []*http.Request) error {
|
||||||
|
// Implement your logic here
|
||||||
|
|
||||||
|
// return nil for continue redirect otherwise return error to stop/prevent redirect
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registering in resty
|
||||||
|
resty.SetRedirectPolicy(CustomRedirectPolicy{/* initialize variables */})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Root Certificates and Client Certifcates
|
||||||
|
```go
|
||||||
|
// Custom Root certificates, just supply .pem file.
|
||||||
|
// you can add one or more root certificates, its get appended
|
||||||
|
resty.SetRootCertificate("/path/to/root/pemFile1.pem")
|
||||||
|
resty.SetRootCertificate("/path/to/root/pemFile2.pem")
|
||||||
|
// ... and so on!
|
||||||
|
|
||||||
|
// Adding Client Certificates, you add one or more certificates
|
||||||
|
// Sample for creating certificate object
|
||||||
|
// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
|
||||||
|
cert1, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("ERROR client certificate: %s", err)
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// You add one or more certificates
|
||||||
|
resty.SetCertificates(cert1, cert2, cert3)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Proxy Settings - Client as well as at Request Level
|
||||||
|
Default `Go` supports Proxy via environment variable `HTTP_PROXY`. Resty provides support via `SetProxy` & `RemoveProxy`.
|
||||||
|
Choose as per your need.
|
||||||
|
|
||||||
|
**Client Level Proxy** settings applied to all the request
|
||||||
|
```go
|
||||||
|
// Setting a Proxy URL and Port
|
||||||
|
resty.SetProxy("http://proxyserver:8888")
|
||||||
|
|
||||||
|
// Want to remove proxy setting
|
||||||
|
resty.RemoveProxy()
|
||||||
|
```
|
||||||
|
**Request Level Proxy** settings, gives control to override at individal request level
|
||||||
|
```go
|
||||||
|
// Set proxy for current request
|
||||||
|
resp, err := c.R().
|
||||||
|
SetProxy("http://sampleproxy:8888").
|
||||||
|
Get("http://httpbin.org/get")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Choose REST or HTTP mode
|
||||||
|
```go
|
||||||
|
// REST mode. This is Default.
|
||||||
|
resty.SetRESTMode()
|
||||||
|
|
||||||
|
// HTTP mode
|
||||||
|
resty.SetHTTPMode()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wanna Multiple Clients
|
||||||
|
```go
|
||||||
|
// Here you go!
|
||||||
|
// Client 1
|
||||||
|
client1 := resty.New()
|
||||||
|
client1.R().Get("http://httpbin.org")
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Client 2
|
||||||
|
client2 := resty.New()
|
||||||
|
client1.R().Head("http://httpbin.org")
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Bend it as per your need!!!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Remaining Client Settings & its Options
|
||||||
|
```go
|
||||||
|
// Unique settings at Client level
|
||||||
|
//--------------------------------
|
||||||
|
// Enable debug mode
|
||||||
|
resty.SetDebug(true)
|
||||||
|
|
||||||
|
// Using you custom log writer
|
||||||
|
logFile, _ := os.OpenFile("/Users/jeeva/go-resty.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
resty.SetLogger(logFile)
|
||||||
|
|
||||||
|
// Assign Client TLSClientConfig
|
||||||
|
// One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
|
||||||
|
resty.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
|
||||||
|
|
||||||
|
// or One can disable security check (https)
|
||||||
|
resty.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
|
||||||
|
|
||||||
|
// Set client timeout as per your need
|
||||||
|
resty.SetTimeout(time.Duration(1 * time.Minute))
|
||||||
|
|
||||||
|
|
||||||
|
// You can override all below settings and options at request level if you want to
|
||||||
|
//--------------------------------------------------------------------------------
|
||||||
|
// Host URL for all request. So you can use relative URL in the request
|
||||||
|
resty.SetHostURL("http://httpbin.org")
|
||||||
|
|
||||||
|
// Headers for all request
|
||||||
|
resty.SetHeader("Accept", "application/json")
|
||||||
|
resty.SetHeaders(map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "My custom User Agent String",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cookies for all request
|
||||||
|
resty.SetCookie(&http.Cookie{
|
||||||
|
Name:"go-resty",
|
||||||
|
Value:"This is cookie value",
|
||||||
|
Path: "/",
|
||||||
|
Domain: "sample.com",
|
||||||
|
MaxAge: 36000,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: false,
|
||||||
|
})
|
||||||
|
resty.SetCookies(cookies)
|
||||||
|
|
||||||
|
// URL query parameters for all request
|
||||||
|
resty.SetQueryParam("user_id", "00001")
|
||||||
|
resty.SetQueryParams(map[string]string{ // sample of those who use this manner
|
||||||
|
"api_key": "api-key-here",
|
||||||
|
"api_secert": "api-secert",
|
||||||
|
})
|
||||||
|
resty.R().SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
|
||||||
|
|
||||||
|
// Form data for all request. Typically used with POST and PUT
|
||||||
|
resty.SetFormData(map[string]string{
|
||||||
|
"access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Basic Auth for all request
|
||||||
|
resty.SetBasicAuth("myuser", "mypass")
|
||||||
|
|
||||||
|
// Bearer Auth Token for all request
|
||||||
|
resty.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
|
||||||
|
|
||||||
|
// Enabling Content length value for all request
|
||||||
|
resty.SetContentLength(true)
|
||||||
|
|
||||||
|
// Registering global Error object structure for JSON/XML request
|
||||||
|
resty.SetError(&Error{}) // or resty.SetError(Error{})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Unix Socket
|
||||||
|
|
||||||
|
```go
|
||||||
|
unixSocket := "unix:///var/run/my_socket.sock"
|
||||||
|
|
||||||
|
// Create a Go's http.Transport so we can set it in resty.
|
||||||
|
transport := http.Transport{
|
||||||
|
Dial: func(_, _ string) (net.Conn, error) {
|
||||||
|
return net.Dial("unix", unixSocket)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the previous transport that we created, set the scheme of the communication to the
|
||||||
|
// socket and set the unixSocket as the HostURL.
|
||||||
|
r := resty.New().SetTransport(transport).SetScheme("http").SetHostURL(unixSocket)
|
||||||
|
|
||||||
|
// No need to write the host's URL on the request, just the path.
|
||||||
|
r.R().Get("/index.html")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
resty releases versions according to [Semantic Versioning](http://semver.org)
|
||||||
|
|
||||||
|
`gopkg.in/resty.vX` points to appropriate tag versions; `X` denotes version number and it's a stable release. It's recommended to use version, for eg. `gopkg.in/resty.v0`. Development takes place at the master branch. Although the code in master should always compile and test successfully, it might break API's. We aim to maintain backwards compatibility, but API's and behaviour might be changed to fix a bug.
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Welcome! If you find any improvement or issue you want to fix, feel free to send a pull request, I like pull requests that include test cases for fix/enhancement. I have done my best to bring pretty good code coverage. Feel free to write tests.
|
||||||
|
|
||||||
|
BTW, I'd like to know what you think about go-resty. Kindly open an issue or send me an email; it'd mean a lot to me.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
Jeevanandam M. - jeeva@myjeeva.com
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
Have a look on [Contributors](https://github.com/go-resty/resty/graphs/contributors) page.
|
||||||
|
|
||||||
|
## License
|
||||||
|
resty released under MIT license, refer [LICENSE](LICENSE) file.
|
|
@ -0,0 +1,931 @@
|
||||||
|
// Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package resty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// GET HTTP method
|
||||||
|
GET = "GET"
|
||||||
|
|
||||||
|
// POST HTTP method
|
||||||
|
POST = "POST"
|
||||||
|
|
||||||
|
// PUT HTTP method
|
||||||
|
PUT = "PUT"
|
||||||
|
|
||||||
|
// DELETE HTTP method
|
||||||
|
DELETE = "DELETE"
|
||||||
|
|
||||||
|
// PATCH HTTP method
|
||||||
|
PATCH = "PATCH"
|
||||||
|
|
||||||
|
// HEAD HTTP method
|
||||||
|
HEAD = "HEAD"
|
||||||
|
|
||||||
|
// OPTIONS HTTP method
|
||||||
|
OPTIONS = "OPTIONS"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent")
|
||||||
|
hdrAcceptKey = http.CanonicalHeaderKey("Accept")
|
||||||
|
hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type")
|
||||||
|
hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length")
|
||||||
|
hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization")
|
||||||
|
|
||||||
|
plainTextType = "text/plain; charset=utf-8"
|
||||||
|
jsonContentType = "application/json; charset=utf-8"
|
||||||
|
formContentType = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
|
jsonCheck = regexp.MustCompile("(?i:[application|text]/json)")
|
||||||
|
xmlCheck = regexp.MustCompile("(?i:[application|text]/xml)")
|
||||||
|
|
||||||
|
hdrUserAgentValue = "go-resty v%s - https://github.com/go-resty/resty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client type is used for HTTP/RESTful global values
|
||||||
|
// for all request raised from the client
|
||||||
|
type Client struct {
|
||||||
|
HostURL string
|
||||||
|
QueryParam url.Values
|
||||||
|
FormData url.Values
|
||||||
|
Header http.Header
|
||||||
|
UserInfo *User
|
||||||
|
Token string
|
||||||
|
Cookies []*http.Cookie
|
||||||
|
Error reflect.Type
|
||||||
|
Debug bool
|
||||||
|
DisableWarn bool
|
||||||
|
Log *log.Logger
|
||||||
|
RetryCount int
|
||||||
|
RetryConditions []RetryConditionFunc
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
transport *http.Transport
|
||||||
|
setContentLength bool
|
||||||
|
isHTTPMode bool
|
||||||
|
outputDirectory string
|
||||||
|
scheme string
|
||||||
|
proxyURL *url.URL
|
||||||
|
mutex *sync.Mutex
|
||||||
|
closeConnection bool
|
||||||
|
beforeRequest []func(*Client, *Request) error
|
||||||
|
afterResponse []func(*Client, *Response) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// User type is to hold an username and password information
|
||||||
|
type User struct {
|
||||||
|
Username, Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHostURL method is to set Host URL in the client instance. It will be used with request
|
||||||
|
// raised from this client with relative URL
|
||||||
|
// // Setting HTTP address
|
||||||
|
// resty.SetHostURL("http://myjeeva.com")
|
||||||
|
//
|
||||||
|
// // Setting HTTPS address
|
||||||
|
// resty.SetHostURL("https://myjeeva.com")
|
||||||
|
//
|
||||||
|
func (c *Client) SetHostURL(url string) *Client {
|
||||||
|
c.HostURL = strings.TrimRight(url, "/")
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader method sets a single header field and its value in the client instance.
|
||||||
|
// These headers will be applied to all requests raised from this client instance.
|
||||||
|
// Also it can be overridden at request level header options, see `resty.R().SetHeader`
|
||||||
|
// or `resty.R().SetHeaders`.
|
||||||
|
//
|
||||||
|
// Example: To set `Content-Type` and `Accept` as `application/json`
|
||||||
|
//
|
||||||
|
// resty.
|
||||||
|
// SetHeader("Content-Type", "application/json").
|
||||||
|
// SetHeader("Accept", "application/json")
|
||||||
|
//
|
||||||
|
func (c *Client) SetHeader(header, value string) *Client {
|
||||||
|
c.Header.Set(header, value)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeaders method sets multiple headers field and its values at one go in the client instance.
|
||||||
|
// These headers will be applied to all requests raised from this client instance. Also it can be
|
||||||
|
// overridden at request level headers options, see `resty.R().SetHeaders` or `resty.R().SetHeader`.
|
||||||
|
//
|
||||||
|
// Example: To set `Content-Type` and `Accept` as `application/json`
|
||||||
|
//
|
||||||
|
// resty.SetHeaders(map[string]string{
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// "Accept": "application/json",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (c *Client) SetHeaders(headers map[string]string) *Client {
|
||||||
|
for h, v := range headers {
|
||||||
|
c.Header.Set(h, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookie method sets a single cookie in the client instance.
|
||||||
|
// These cookies will be added to all the request raised from this client instance.
|
||||||
|
// resty.SetCookie(&http.Cookie{
|
||||||
|
// Name:"go-resty",
|
||||||
|
// Value:"This is cookie value",
|
||||||
|
// Path: "/",
|
||||||
|
// Domain: "sample.com",
|
||||||
|
// MaxAge: 36000,
|
||||||
|
// HttpOnly: true,
|
||||||
|
// Secure: false,
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (c *Client) SetCookie(hc *http.Cookie) *Client {
|
||||||
|
c.Cookies = append(c.Cookies, hc)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookies method sets an array of cookies in the client instance.
|
||||||
|
// These cookies will be added to all the request raised from this client instance.
|
||||||
|
// cookies := make([]*http.Cookie, 0)
|
||||||
|
//
|
||||||
|
// cookies = append(cookies, &http.Cookie{
|
||||||
|
// Name:"go-resty-1",
|
||||||
|
// Value:"This is cookie 1 value",
|
||||||
|
// Path: "/",
|
||||||
|
// Domain: "sample.com",
|
||||||
|
// MaxAge: 36000,
|
||||||
|
// HttpOnly: true,
|
||||||
|
// Secure: false,
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// cookies = append(cookies, &http.Cookie{
|
||||||
|
// Name:"go-resty-2",
|
||||||
|
// Value:"This is cookie 2 value",
|
||||||
|
// Path: "/",
|
||||||
|
// Domain: "sample.com",
|
||||||
|
// MaxAge: 36000,
|
||||||
|
// HttpOnly: true,
|
||||||
|
// Secure: false,
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// // Setting a cookies into resty
|
||||||
|
// resty.SetCookies(cookies)
|
||||||
|
//
|
||||||
|
func (c *Client) SetCookies(cs []*http.Cookie) *Client {
|
||||||
|
c.Cookies = append(c.Cookies, cs...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryParam method sets single paramater and its value in the client instance.
|
||||||
|
// It will be formed as query string for the request. For example: `search=kitchen%20papers&size=large`
|
||||||
|
// in the URL after `?` mark. These query params will be added to all the request raised from
|
||||||
|
// this client instance. Also it can be overridden at request level Query Param options,
|
||||||
|
// see `resty.R().SetQueryParam` or `resty.R().SetQueryParams`.
|
||||||
|
// resty.
|
||||||
|
// SetQueryParam("search", "kitchen papers").
|
||||||
|
// SetQueryParam("size", "large")
|
||||||
|
//
|
||||||
|
func (c *Client) SetQueryParam(param, value string) *Client {
|
||||||
|
c.QueryParam.Add(param, value)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryParams method sets multiple paramaters and its values at one go in the client instance.
|
||||||
|
// It will be formed as query string for the request. For example: `search=kitchen%20papers&size=large`
|
||||||
|
// in the URL after `?` mark. These query params will be added to all the request raised from this
|
||||||
|
// client instance. Also it can be overridden at request level Query Param options,
|
||||||
|
// see `resty.R().SetQueryParams` or `resty.R().SetQueryParam`.
|
||||||
|
// resty.SetQueryParams(map[string]string{
|
||||||
|
// "search": "kitchen papers",
|
||||||
|
// "size": "large",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (c *Client) SetQueryParams(params map[string]string) *Client {
|
||||||
|
for p, v := range params {
|
||||||
|
c.QueryParam.Add(p, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFormData method sets Form parameters and its values in the client instance.
|
||||||
|
// It's applicable only HTTP method `POST` and `PUT` and requets content type would be set as
|
||||||
|
// `application/x-www-form-urlencoded`. These form data will be added to all the request raised from
|
||||||
|
// this client instance. Also it can be overridden at request level form data, see `resty.R().SetFormData`.
|
||||||
|
// resty.SetFormData(map[string]string{
|
||||||
|
// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
|
||||||
|
// "user_id": "3455454545",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (c *Client) SetFormData(data map[string]string) *Client {
|
||||||
|
for k, v := range data {
|
||||||
|
c.FormData.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBasicAuth method sets the basic authentication header in the HTTP request. Example:
|
||||||
|
// Authorization: Basic <base64-encoded-value>
|
||||||
|
//
|
||||||
|
// Example: To set the header for username "go-resty" and password "welcome"
|
||||||
|
// resty.SetBasicAuth("go-resty", "welcome")
|
||||||
|
//
|
||||||
|
// This basic auth information gets added to all the request rasied from this client instance.
|
||||||
|
// Also it can be overridden or set one at the request level is supported, see `resty.R().SetBasicAuth`.
|
||||||
|
//
|
||||||
|
func (c *Client) SetBasicAuth(username, password string) *Client {
|
||||||
|
c.UserInfo = &User{Username: username, Password: password}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthToken method sets bearer auth token header in the HTTP request. Example:
|
||||||
|
// Authorization: Bearer <auth-token-value-comes-here>
|
||||||
|
//
|
||||||
|
// Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
|
||||||
|
//
|
||||||
|
// resty.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
|
||||||
|
//
|
||||||
|
// This bearer auth token gets added to all the request rasied from this client instance.
|
||||||
|
// Also it can be overridden or set one at the request level is supported, see `resty.R().SetAuthToken`.
|
||||||
|
//
|
||||||
|
func (c *Client) SetAuthToken(token string) *Client {
|
||||||
|
c.Token = token
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// R method creates a request instance, its used for Get, Post, Put, Delete, Patch, Head and Options.
|
||||||
|
func (c *Client) R() *Request {
|
||||||
|
r := &Request{
|
||||||
|
URL: "",
|
||||||
|
Method: "",
|
||||||
|
QueryParam: url.Values{},
|
||||||
|
FormData: url.Values{},
|
||||||
|
Header: http.Header{},
|
||||||
|
Body: nil,
|
||||||
|
Result: nil,
|
||||||
|
Error: nil,
|
||||||
|
RawRequest: nil,
|
||||||
|
client: c,
|
||||||
|
bodyBuf: nil,
|
||||||
|
proxyURL: nil,
|
||||||
|
multipartFiles: []*File{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnBeforeRequest method sets request middleware into the before request chain.
|
||||||
|
// Its gets applied after default `go-resty` request middlewares and before request
|
||||||
|
// been sent from `go-resty` to host server.
|
||||||
|
// resty.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
|
||||||
|
// // Now you have access to Client and Request instance
|
||||||
|
// // manipulate it as per your need
|
||||||
|
//
|
||||||
|
// return nil // if its success otherwise return error
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (c *Client) OnBeforeRequest(m func(*Client, *Request) error) *Client {
|
||||||
|
c.beforeRequest[len(c.beforeRequest)-1] = m
|
||||||
|
c.beforeRequest = append(c.beforeRequest, requestLogger)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnAfterResponse method sets response middleware into the after response chain.
|
||||||
|
// Once we receive response from host server, default `go-resty` response middleware
|
||||||
|
// gets applied and then user assigened response middlewares applied.
|
||||||
|
// resty.OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
|
||||||
|
// // Now you have access to Client and Response instance
|
||||||
|
// // manipulate it as per your need
|
||||||
|
//
|
||||||
|
// return nil // if its success otherwise return error
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (c *Client) OnAfterResponse(m func(*Client, *Response) error) *Client {
|
||||||
|
c.afterResponse = append(c.afterResponse, m)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebug method enables the debug mode on `go-resty` client. Client logs details of every request and response.
|
||||||
|
// For `Request` it logs information such as HTTP verb, Relative URL path, Host, Headers, Body if it has one.
|
||||||
|
// For `Response` it logs information such as Status, Response Time, Headers, Body if it has one.
|
||||||
|
// resty.SetDebug(true)
|
||||||
|
//
|
||||||
|
func (c *Client) SetDebug(d bool) *Client {
|
||||||
|
c.Debug = d
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDisableWarn method disables the warning message on `go-resty` client.
|
||||||
|
// For example: go-resty warns the user when BasicAuth used on HTTP mode.
|
||||||
|
// resty.SetDisableWarn(true)
|
||||||
|
//
|
||||||
|
func (c *Client) SetDisableWarn(d bool) *Client {
|
||||||
|
c.DisableWarn = d
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger method sets given writer for logging go-resty request and response details.
|
||||||
|
// Default is os.Stderr
|
||||||
|
// file, _ := os.OpenFile("/Users/jeeva/go-resty.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
//
|
||||||
|
// resty.SetLogger(file)
|
||||||
|
//
|
||||||
|
func (c *Client) SetLogger(w io.Writer) *Client {
|
||||||
|
c.Log = getLogger(w)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentLength method enables the HTTP header `Content-Length` value for every request.
|
||||||
|
// By default go-resty won't set `Content-Length`.
|
||||||
|
// resty.SetContentLength(true)
|
||||||
|
//
|
||||||
|
// Also you have an option to enable for particular request. See `resty.R().SetContentLength`
|
||||||
|
//
|
||||||
|
func (c *Client) SetContentLength(l bool) *Client {
|
||||||
|
c.setContentLength = l
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError method is to register the global or client common `Error` object into go-resty.
|
||||||
|
// It is used for automatic unmarshalling if response status code is greater than 399 and
|
||||||
|
// content type either JSON or XML. Can be pointer or non-pointer.
|
||||||
|
// resty.SetError(&Error{})
|
||||||
|
// // OR
|
||||||
|
// resty.SetError(Error{})
|
||||||
|
//
|
||||||
|
func (c *Client) SetError(err interface{}) *Client {
|
||||||
|
c.Error = typeOf(err)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRedirectPolicy method sets the client redirect poilicy. go-resty provides ready to use
|
||||||
|
// redirect policies. Wanna create one for yourself refer `redirect.go`.
|
||||||
|
//
|
||||||
|
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
|
||||||
|
//
|
||||||
|
// // Need multiple redirect policies together
|
||||||
|
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20), DomainCheckRedirectPolicy("host1.com", "host2.net"))
|
||||||
|
//
|
||||||
|
func (c *Client) SetRedirectPolicy(policies ...interface{}) *Client {
|
||||||
|
for _, p := range policies {
|
||||||
|
if _, ok := p.(RedirectPolicy); !ok {
|
||||||
|
c.Log.Printf("ERORR: %v does not implement resty.RedirectPolicy (missing Apply method)",
|
||||||
|
runtime.FuncForPC(reflect.ValueOf(p).Pointer()).Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
for _, p := range policies {
|
||||||
|
err := p.(RedirectPolicy).Apply(req, via)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil // looks good, go ahead
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRetryCount method enables retry on `go-resty` client and allows you
|
||||||
|
// to set no. of retry count. Resty uses a Backoff mechanism.
|
||||||
|
func (c *Client) SetRetryCount(count int) *Client {
|
||||||
|
c.RetryCount = count
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRetryCondition method adds a retry condition function to array of functions
|
||||||
|
// that are checked to determine if the request is retried. The request will
|
||||||
|
// retry if any of the functions return true and error is nil.
|
||||||
|
func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client {
|
||||||
|
c.RetryConditions = append(c.RetryConditions, condition)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHTTPMode method sets go-resty mode into HTTP
|
||||||
|
func (c *Client) SetHTTPMode() *Client {
|
||||||
|
return c.SetMode("http")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRESTMode method sets go-resty mode into RESTful
|
||||||
|
func (c *Client) SetRESTMode() *Client {
|
||||||
|
return c.SetMode("rest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMode method sets go-resty client mode to given value such as 'http' & 'rest'.
|
||||||
|
// RESTful:
|
||||||
|
// - No Redirect
|
||||||
|
// - Automatic response unmarshal if it is JSON or XML
|
||||||
|
// HTML:
|
||||||
|
// - Up to 10 Redirects
|
||||||
|
// - No automatic unmarshall. Response will be treated as `response.String()`
|
||||||
|
//
|
||||||
|
// If you want more redirects, use FlexibleRedirectPolicy
|
||||||
|
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
|
||||||
|
//
|
||||||
|
func (c *Client) SetMode(mode string) *Client {
|
||||||
|
if mode == "http" {
|
||||||
|
c.isHTTPMode = true
|
||||||
|
c.SetRedirectPolicy(FlexibleRedirectPolicy(10))
|
||||||
|
c.afterResponse = []func(*Client, *Response) error{
|
||||||
|
responseLogger,
|
||||||
|
saveResponseIntoFile,
|
||||||
|
}
|
||||||
|
} else { // RESTful
|
||||||
|
c.isHTTPMode = false
|
||||||
|
c.SetRedirectPolicy(NoRedirectPolicy())
|
||||||
|
c.afterResponse = []func(*Client, *Response) error{
|
||||||
|
responseLogger,
|
||||||
|
parseResponseBody,
|
||||||
|
saveResponseIntoFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode method returns the current client mode. Typically its a "http" or "rest".
|
||||||
|
// Default is "rest"
|
||||||
|
func (c *Client) Mode() string {
|
||||||
|
if c.isHTTPMode {
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "rest"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTLSClientConfig method sets TLSClientConfig for underling client Transport.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// // One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
|
||||||
|
// resty.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
|
||||||
|
//
|
||||||
|
// // or One can disable security check (https)
|
||||||
|
// resty.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
|
||||||
|
// Note: This method overwrites existing `TLSClientConfig`.
|
||||||
|
//
|
||||||
|
func (c *Client) SetTLSClientConfig(config *tls.Config) *Client {
|
||||||
|
c.transport.TLSClientConfig = config
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTimeout method sets timeout for request raised from client
|
||||||
|
// resty.SetTimeout(time.Duration(1 * time.Minute))
|
||||||
|
//
|
||||||
|
func (c *Client) SetTimeout(timeout time.Duration) *Client {
|
||||||
|
c.transport.Dial = func(network, addr string) (net.Conn, error) {
|
||||||
|
conn, err := net.DialTimeout(network, addr, timeout)
|
||||||
|
if err != nil {
|
||||||
|
c.Log.Printf("ERROR [%v]", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProxy method sets the Proxy URL and Port for resty client.
|
||||||
|
// resty.SetProxy("http://proxyserver:8888")
|
||||||
|
//
|
||||||
|
// Alternatives: At request level proxy, see `Request.SetProxy`. OR Without this `SetProxy` method,
|
||||||
|
// you can also set Proxy via environment variable. By default `Go` uses setting from `HTTP_PROXY`.
|
||||||
|
//
|
||||||
|
func (c *Client) SetProxy(proxyURL string) *Client {
|
||||||
|
if pURL, err := url.Parse(proxyURL); err == nil {
|
||||||
|
c.proxyURL = pURL
|
||||||
|
} else {
|
||||||
|
c.Log.Printf("ERROR [%v]", err)
|
||||||
|
c.proxyURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveProxy method removes the proxy configuration from resty client
|
||||||
|
// resty.RemoveProxy()
|
||||||
|
//
|
||||||
|
func (c *Client) RemoveProxy() *Client {
|
||||||
|
c.proxyURL = nil
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCertificates method helps to set client certificates into resty conveniently.
|
||||||
|
//
|
||||||
|
func (c *Client) SetCertificates(certs ...tls.Certificate) *Client {
|
||||||
|
config := c.getTLSConfig()
|
||||||
|
config.Certificates = append(config.Certificates, certs...)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRootCertificate method helps to add one or more root certificates into resty client
|
||||||
|
// resty.SetRootCertificate("/path/to/root/pemFile.pem")
|
||||||
|
//
|
||||||
|
func (c *Client) SetRootCertificate(pemFilePath string) *Client {
|
||||||
|
rootPemData, err := ioutil.ReadFile(pemFilePath)
|
||||||
|
if err != nil {
|
||||||
|
c.Log.Printf("ERROR [%v]", err)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
config := c.getTLSConfig()
|
||||||
|
if config.RootCAs == nil {
|
||||||
|
config.RootCAs = x509.NewCertPool()
|
||||||
|
}
|
||||||
|
|
||||||
|
config.RootCAs.AppendCertsFromPEM(rootPemData)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutputDirectory method sets output directory for saving HTTP response into file.
|
||||||
|
// If the output directory not exists then resty creates one. This setting is optional one,
|
||||||
|
// if you're planning using absoule path in `Request.SetOutput` and can used together.
|
||||||
|
// resty.SetOutputDirectory("/save/http/response/here")
|
||||||
|
//
|
||||||
|
func (c *Client) SetOutputDirectory(dirPath string) *Client {
|
||||||
|
err := createDirectory(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Log.Printf("ERROR [%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.outputDirectory = dirPath
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransport method sets custom *http.Transport in the resty client. Its way to override default.
|
||||||
|
//
|
||||||
|
// **Note:** It overwrites the default resty transport instance and its configurations.
|
||||||
|
// transport := &http.Transport{
|
||||||
|
// // somthing like Proxying to httptest.Server, etc...
|
||||||
|
// Proxy: func(req *http.Request) (*url.URL, error) {
|
||||||
|
// return url.Parse(server.URL)
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// resty.SetTransport(&transport)
|
||||||
|
//
|
||||||
|
func (c *Client) SetTransport(transport *http.Transport) *Client {
|
||||||
|
if transport != nil {
|
||||||
|
c.transport = transport
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetScheme method sets custom scheme in the resty client. Its way to override default.
|
||||||
|
// resty.SetScheme("http")
|
||||||
|
//
|
||||||
|
func (c *Client) SetScheme(scheme string) *Client {
|
||||||
|
if c.scheme == "" {
|
||||||
|
c.scheme = scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCloseConnection method sets variable Close in http request struct with the given
|
||||||
|
// value. More info: https://golang.org/src/net/http/request.go
|
||||||
|
func (c *Client) SetCloseConnection(close bool) *Client {
|
||||||
|
c.closeConnection = close
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// executes the given `Request` object and returns response
|
||||||
|
func (c *Client) execute(req *Request) (*Response, error) {
|
||||||
|
// Apply Request middleware
|
||||||
|
var err error
|
||||||
|
for _, f := range c.beforeRequest {
|
||||||
|
err = f(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
|
||||||
|
if req.proxyURL != nil {
|
||||||
|
c.transport.Proxy = http.ProxyURL(req.proxyURL)
|
||||||
|
} else if c.proxyURL != nil {
|
||||||
|
c.transport.Proxy = http.ProxyURL(c.proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Time = time.Now()
|
||||||
|
c.httpClient.Transport = c.transport
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req.RawRequest)
|
||||||
|
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
response := &Response{
|
||||||
|
Request: req,
|
||||||
|
RawResponse: resp,
|
||||||
|
receivedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.isSaveResponse {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
response.body, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response.size = int64(len(response.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Response middleware
|
||||||
|
for _, f := range c.afterResponse {
|
||||||
|
err = f(c, response)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// enables a log prefix
|
||||||
|
func (c *Client) enableLogPrefix() {
|
||||||
|
c.Log.SetFlags(log.LstdFlags)
|
||||||
|
c.Log.SetPrefix("RESTY ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// disables a log prefix
|
||||||
|
func (c *Client) disableLogPrefix() {
|
||||||
|
c.Log.SetFlags(0)
|
||||||
|
c.Log.SetPrefix("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getting TLS client config if not exists then create one
|
||||||
|
func (c *Client) getTLSConfig() *tls.Config {
|
||||||
|
if c.transport.TLSClientConfig == nil {
|
||||||
|
c.transport.TLSClientConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.transport.TLSClientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Response
|
||||||
|
//
|
||||||
|
|
||||||
|
// Response is an object represents executed request and its values.
|
||||||
|
type Response struct {
|
||||||
|
Request *Request
|
||||||
|
RawResponse *http.Response
|
||||||
|
|
||||||
|
body []byte
|
||||||
|
size int64
|
||||||
|
receivedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body method returns HTTP response as []byte array for the executed request.
|
||||||
|
// Note: `Response.Body` might be nil, if `Request.SetOutput` is used.
|
||||||
|
func (r *Response) Body() []byte {
|
||||||
|
return r.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status method returns the HTTP status string for the executed request.
|
||||||
|
// Example: 200 OK
|
||||||
|
func (r *Response) Status() string {
|
||||||
|
return r.RawResponse.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode method returns the HTTP status code for the executed request.
|
||||||
|
// Example: 200
|
||||||
|
func (r *Response) StatusCode() int {
|
||||||
|
return r.RawResponse.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result method returns the response value as an object if it has one
|
||||||
|
func (r *Response) Result() interface{} {
|
||||||
|
return r.Request.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error method returns the error object if it has one
|
||||||
|
func (r *Response) Error() interface{} {
|
||||||
|
return r.Request.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header method returns the response headers
|
||||||
|
func (r *Response) Header() http.Header {
|
||||||
|
return r.RawResponse.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookies method to access all the response cookies
|
||||||
|
func (r *Response) Cookies() []*http.Cookie {
|
||||||
|
return r.RawResponse.Cookies()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String method returns the body of the server response as String.
|
||||||
|
func (r *Response) String() string {
|
||||||
|
if r.body == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(string(r.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time method returns the time of HTTP response time that from request we sent and received a request.
|
||||||
|
// See `response.ReceivedAt` to know when client recevied response and see `response.Request.Time` to know
|
||||||
|
// when client sent a request.
|
||||||
|
func (r *Response) Time() time.Duration {
|
||||||
|
return r.receivedAt.Sub(r.Request.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReceivedAt method returns when response got recevied from server for the request.
|
||||||
|
func (r *Response) ReceivedAt() time.Time {
|
||||||
|
return r.receivedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size method returns the HTTP response size in bytes. Ya, you can relay on HTTP `Content-Length` header,
|
||||||
|
// however it won't be good for chucked transfer/compressed response. Since Resty calculates response size
|
||||||
|
// at the client end. You will get actual size of the http response.
|
||||||
|
func (r *Response) Size() int64 {
|
||||||
|
return r.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) fmtBodyString() string {
|
||||||
|
bodyStr := "***** NO CONTENT *****"
|
||||||
|
if r.body != nil {
|
||||||
|
ct := r.Header().Get(hdrContentTypeKey)
|
||||||
|
if IsJSONType(ct) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := json.Indent(&out, r.body, "", " "); err == nil {
|
||||||
|
bodyStr = string(out.Bytes())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyStr = r.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodyStr
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// File
|
||||||
|
//
|
||||||
|
|
||||||
|
// File represent file information for multipart request
|
||||||
|
type File struct {
|
||||||
|
Name string
|
||||||
|
ParamName string
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns string value of current file details
|
||||||
|
func (f *File) String() string {
|
||||||
|
return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Helper methods
|
||||||
|
//
|
||||||
|
|
||||||
|
// IsStringEmpty method tells whether given string is empty or not
|
||||||
|
func IsStringEmpty(str string) bool {
|
||||||
|
return (len(strings.TrimSpace(str)) == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectContentType method is used to figure out `Request.Body` content type for request header
|
||||||
|
func DetectContentType(body interface{}) string {
|
||||||
|
contentType := plainTextType
|
||||||
|
kind := kindOf(body)
|
||||||
|
switch kind {
|
||||||
|
case reflect.Struct, reflect.Map:
|
||||||
|
contentType = jsonContentType
|
||||||
|
case reflect.String:
|
||||||
|
contentType = plainTextType
|
||||||
|
default:
|
||||||
|
if b, ok := body.([]byte); ok {
|
||||||
|
contentType = http.DetectContentType(b)
|
||||||
|
} else if kind == reflect.Slice {
|
||||||
|
contentType = jsonContentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsJSONType method is to check JSON content type or not
|
||||||
|
func IsJSONType(ct string) bool {
|
||||||
|
return jsonCheck.MatchString(ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsXMLType method is to check XML content type or not
|
||||||
|
func IsXMLType(ct string) bool {
|
||||||
|
return xmlCheck.MatchString(ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal content into object from JSON or XML
|
||||||
|
func Unmarshal(ct string, b []byte, d interface{}) (err error) {
|
||||||
|
if IsJSONType(ct) {
|
||||||
|
err = json.Unmarshal(b, d)
|
||||||
|
} else if IsXMLType(ct) {
|
||||||
|
err = xml.Unmarshal(b, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogger(w io.Writer) *log.Logger {
|
||||||
|
return log.New(w, "RESTY ", log.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFile(w *multipart.Writer, fieldName, path string) error {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
part, err := w.CreateFormFile(fieldName, filepath.Base(path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(part, file)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFileReader(w *multipart.Writer, f *File) error {
|
||||||
|
part, err := w.CreateFormFile(f.ParamName, f.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(part, f.Reader)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPointer(v interface{}) interface{} {
|
||||||
|
vv := valueOf(v)
|
||||||
|
if vv.Kind() == reflect.Ptr {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return reflect.New(vv.Type()).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPayloadSupported(m string) bool {
|
||||||
|
return (m == POST || m == PUT || m == DELETE || m == PATCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeOf(i interface{}) reflect.Type {
|
||||||
|
return indirect(valueOf(i)).Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueOf(i interface{}) reflect.Value {
|
||||||
|
return reflect.ValueOf(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func indirect(v reflect.Value) reflect.Value {
|
||||||
|
return reflect.Indirect(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func kindOf(v interface{}) reflect.Kind {
|
||||||
|
return typeOf(v).Kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDirectory(dir string) (err error) {
|
||||||
|
if _, err = os.Stat(dir); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,244 @@
|
||||||
|
// Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package resty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultClient of resty
|
||||||
|
var DefaultClient *Client
|
||||||
|
|
||||||
|
// New method creates a new go-resty client
|
||||||
|
func New() *Client {
|
||||||
|
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
HostURL: "",
|
||||||
|
QueryParam: url.Values{},
|
||||||
|
FormData: url.Values{},
|
||||||
|
Header: http.Header{},
|
||||||
|
UserInfo: nil,
|
||||||
|
Token: "",
|
||||||
|
Cookies: make([]*http.Cookie, 0),
|
||||||
|
Debug: false,
|
||||||
|
Log: getLogger(os.Stderr),
|
||||||
|
httpClient: &http.Client{Jar: cookieJar},
|
||||||
|
transport: &http.Transport{},
|
||||||
|
mutex: &sync.Mutex{},
|
||||||
|
RetryCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default redirect policy
|
||||||
|
c.SetRedirectPolicy(NoRedirectPolicy())
|
||||||
|
|
||||||
|
// default before request middlewares
|
||||||
|
c.beforeRequest = []func(*Client, *Request) error{
|
||||||
|
parseRequestURL,
|
||||||
|
parseRequestHeader,
|
||||||
|
parseRequestBody,
|
||||||
|
createHTTPRequest,
|
||||||
|
addCredentials,
|
||||||
|
requestLogger,
|
||||||
|
}
|
||||||
|
|
||||||
|
// default after response middlewares
|
||||||
|
c.afterResponse = []func(*Client, *Response) error{
|
||||||
|
responseLogger,
|
||||||
|
parseResponseBody,
|
||||||
|
saveResponseIntoFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// R creates a new resty request object, it is used form a HTTP/RESTful request
|
||||||
|
// such as GET, POST, PUT, DELETE, HEAD, PATCH and OPTIONS.
|
||||||
|
func R() *Request {
|
||||||
|
return DefaultClient.R()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHostURL sets Host URL. See `Client.SetHostURL for more information.
|
||||||
|
func SetHostURL(url string) *Client {
|
||||||
|
return DefaultClient.SetHostURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader sets single header. See `Client.SetHeader` for more information.
|
||||||
|
func SetHeader(header, value string) *Client {
|
||||||
|
return DefaultClient.SetHeader(header, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeaders sets multiple headers. See `Client.SetHeaders` for more information.
|
||||||
|
func SetHeaders(headers map[string]string) *Client {
|
||||||
|
return DefaultClient.SetHeaders(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookie sets single cookie object. See `Client.SetCookie` for more information.
|
||||||
|
func SetCookie(hc *http.Cookie) *Client {
|
||||||
|
return DefaultClient.SetCookie(hc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookies sets multiple cookie object. See `Client.SetCookies` for more information.
|
||||||
|
func SetCookies(cs []*http.Cookie) *Client {
|
||||||
|
return DefaultClient.SetCookies(cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryParam method sets single paramater and its value. See `Client.SetQueryParam` for more information.
|
||||||
|
func SetQueryParam(param, value string) *Client {
|
||||||
|
return DefaultClient.SetQueryParam(param, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryParams method sets multiple paramaters and its value. See `Client.SetQueryParams` for more information.
|
||||||
|
func SetQueryParams(params map[string]string) *Client {
|
||||||
|
return DefaultClient.SetQueryParams(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFormData method sets Form parameters and its values. See `Client.SetFormData` for more information.
|
||||||
|
func SetFormData(data map[string]string) *Client {
|
||||||
|
return DefaultClient.SetFormData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBasicAuth method sets the basic authentication header. See `Client.SetBasicAuth` for more information.
|
||||||
|
func SetBasicAuth(username, password string) *Client {
|
||||||
|
return DefaultClient.SetBasicAuth(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthToken method sets bearer auth token header. See `Client.SetAuthToken` for more information.
|
||||||
|
func SetAuthToken(token string) *Client {
|
||||||
|
return DefaultClient.SetAuthToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnBeforeRequest method sets request middleware. See `Client.OnBeforeRequest` for more information.
|
||||||
|
func OnBeforeRequest(m func(*Client, *Request) error) *Client {
|
||||||
|
return DefaultClient.OnBeforeRequest(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnAfterResponse method sets response middleware. See `Client.OnAfterResponse` for more information.
|
||||||
|
func OnAfterResponse(m func(*Client, *Response) error) *Client {
|
||||||
|
return DefaultClient.OnAfterResponse(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebug method enables the debug mode. See `Client.SetDebug` for more information.
|
||||||
|
func SetDebug(d bool) *Client {
|
||||||
|
return DefaultClient.SetDebug(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRetryCount method set the retry count. See `Client.SetRetryCount` for more information.
|
||||||
|
func SetRetryCount(count int) *Client {
|
||||||
|
return DefaultClient.SetRetryCount(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRetryCondition method appends check function for retry. See `Client.AddRetryCondition` for more information.
|
||||||
|
func AddRetryCondition(condition RetryConditionFunc) *Client {
|
||||||
|
return DefaultClient.AddRetryCondition(condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDisableWarn method disables warning comes from `go-resty` client. See `Client.SetDisableWarn` for more information.
|
||||||
|
func SetDisableWarn(d bool) *Client {
|
||||||
|
return DefaultClient.SetDisableWarn(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger method sets given writer for logging. See `Client.SetLogger` for more information.
|
||||||
|
func SetLogger(w io.Writer) *Client {
|
||||||
|
return DefaultClient.SetLogger(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentLength method enables `Content-Length` value. See `Client.SetContentLength` for more information.
|
||||||
|
func SetContentLength(l bool) *Client {
|
||||||
|
return DefaultClient.SetContentLength(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError method is to register the global or client common `Error` object. See `Client.SetError` for more information.
|
||||||
|
func SetError(err interface{}) *Client {
|
||||||
|
return DefaultClient.SetError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRedirectPolicy method sets the client redirect poilicy. See `Client.SetRedirectPolicy` for more information.
|
||||||
|
func SetRedirectPolicy(policies ...interface{}) *Client {
|
||||||
|
return DefaultClient.SetRedirectPolicy(policies...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHTTPMode method sets go-resty mode into HTTP. See `Client.SetMode` for more information.
|
||||||
|
func SetHTTPMode() *Client {
|
||||||
|
return DefaultClient.SetHTTPMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRESTMode method sets go-resty mode into RESTful. See `Client.SetMode` for more information.
|
||||||
|
func SetRESTMode() *Client {
|
||||||
|
return DefaultClient.SetRESTMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode method returns the current client mode. See `Client.Mode` for more information.
|
||||||
|
func Mode() string {
|
||||||
|
return DefaultClient.Mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTLSClientConfig method sets TLSClientConfig for underling client Transport. See `Client.SetTLSClientConfig` for more information.
|
||||||
|
func SetTLSClientConfig(config *tls.Config) *Client {
|
||||||
|
return DefaultClient.SetTLSClientConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTimeout method sets timeout for request. See `Client.SetTimeout` for more information.
|
||||||
|
func SetTimeout(timeout time.Duration) *Client {
|
||||||
|
return DefaultClient.SetTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProxy method sets Proxy for request. See `Client.SetProxy` for more information.
|
||||||
|
func SetProxy(proxyURL string) *Client {
|
||||||
|
return DefaultClient.SetProxy(proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveProxy method removes the proxy configuration. See `Client.RemoveProxy` for more information.
|
||||||
|
func RemoveProxy() *Client {
|
||||||
|
return DefaultClient.RemoveProxy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCertificates method helps to set client certificates into resty conveniently.
|
||||||
|
// See `Client.SetCertificates` for more information and example.
|
||||||
|
func SetCertificates(certs ...tls.Certificate) *Client {
|
||||||
|
return DefaultClient.SetCertificates(certs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRootCertificate method helps to add one or more root certificates into resty client.
|
||||||
|
// See `Client.SetRootCertificate` for more information.
|
||||||
|
func SetRootCertificate(pemFilePath string) *Client {
|
||||||
|
return DefaultClient.SetRootCertificate(pemFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutputDirectory method sets output directory. See `Client.SetOutputDirectory` for more information.
|
||||||
|
func SetOutputDirectory(dirPath string) *Client {
|
||||||
|
return DefaultClient.SetOutputDirectory(dirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransport method sets custom *http.Transport in the resty client.
|
||||||
|
// See `Client.SetTransport` for more information.
|
||||||
|
func SetTransport(transport *http.Transport) *Client {
|
||||||
|
return DefaultClient.SetTransport(transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetScheme method sets custom scheme in the resty client.
|
||||||
|
// See `Client.SetScheme` for more information.
|
||||||
|
func SetScheme(scheme string) *Client {
|
||||||
|
return DefaultClient.SetScheme(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCloseConnection method sets close connection value in the resty client.
|
||||||
|
// See `Client.SetCloseConnection` for more information.
|
||||||
|
func SetCloseConnection(close bool) *Client {
|
||||||
|
return DefaultClient.SetCloseConnection(close)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultClient = New()
|
||||||
|
}
|
|
@ -0,0 +1,406 @@
|
||||||
|
// Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package resty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Request Middleware(s)
|
||||||
|
//
|
||||||
|
|
||||||
|
func parseRequestURL(c *Client, r *Request) error {
|
||||||
|
// Parsing request URL
|
||||||
|
reqURL, err := url.Parse(r.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Request.Url is relative path then added c.HostUrl into
|
||||||
|
// the request URL otherwise Request.Url will be used as-is
|
||||||
|
if !reqURL.IsAbs() {
|
||||||
|
if !strings.HasPrefix(r.URL, "/") {
|
||||||
|
r.URL = "/" + r.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL, err = url.Parse(c.HostURL + r.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding Query Param
|
||||||
|
query := reqURL.Query()
|
||||||
|
for k, v := range c.QueryParam {
|
||||||
|
for _, iv := range v {
|
||||||
|
query.Add(k, iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range r.QueryParam {
|
||||||
|
// remove query param from client level by key
|
||||||
|
// since overrides happens for that key in the request
|
||||||
|
query.Del(k)
|
||||||
|
|
||||||
|
for _, iv := range v {
|
||||||
|
query.Add(k, iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL.RawQuery = query.Encode()
|
||||||
|
r.URL = reqURL.String()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequestHeader(c *Client, r *Request) error {
|
||||||
|
hdr := http.Header{}
|
||||||
|
for k := range c.Header {
|
||||||
|
hdr.Set(k, c.Header.Get(k))
|
||||||
|
}
|
||||||
|
for k := range r.Header {
|
||||||
|
hdr.Set(k, r.Header.Get(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsStringEmpty(hdr.Get(hdrUserAgentKey)) {
|
||||||
|
hdr.Set(hdrUserAgentKey, fmt.Sprintf(hdrUserAgentValue, Version))
|
||||||
|
} else {
|
||||||
|
hdr.Set("X-"+hdrUserAgentKey, fmt.Sprintf(hdrUserAgentValue, Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(hdr.Get(hdrContentTypeKey)) {
|
||||||
|
hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header = hdr
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRequestBody(c *Client, r *Request) (err error) {
|
||||||
|
if isPayloadSupported(r.Method) {
|
||||||
|
|
||||||
|
// Handling Multipart
|
||||||
|
if r.isMultiPart && !(r.Method == PATCH) {
|
||||||
|
if err = handleMultipart(c, r); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goto CL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handling Form Data
|
||||||
|
if len(c.FormData) > 0 || len(r.FormData) > 0 {
|
||||||
|
handleFormData(c, r)
|
||||||
|
|
||||||
|
goto CL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handling Request body
|
||||||
|
if r.Body != nil {
|
||||||
|
handleContentType(c, r)
|
||||||
|
|
||||||
|
if err = handleRequestBody(c, r); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.Header.Del(hdrContentTypeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
CL:
|
||||||
|
// by default resty won't set content length, you can if you want to :)
|
||||||
|
if c.setContentLength || r.setContentLength {
|
||||||
|
r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createHTTPRequest(c *Client, r *Request) (err error) {
|
||||||
|
if r.bodyBuf == nil {
|
||||||
|
r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil)
|
||||||
|
} else {
|
||||||
|
r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign close connection option
|
||||||
|
r.RawRequest.Close = c.closeConnection
|
||||||
|
|
||||||
|
// Add headers into http request
|
||||||
|
r.RawRequest.Header = r.Header
|
||||||
|
|
||||||
|
// Add cookies into http request
|
||||||
|
for _, cookie := range c.Cookies {
|
||||||
|
r.RawRequest.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's for non-http scheme option
|
||||||
|
if r.RawRequest.URL != nil && r.RawRequest.URL.Scheme == "" {
|
||||||
|
r.RawRequest.URL.Scheme = c.scheme
|
||||||
|
r.RawRequest.URL.Host = r.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCredentials(c *Client, r *Request) error {
|
||||||
|
var isBasicAuth bool
|
||||||
|
// Basic Auth
|
||||||
|
if r.UserInfo != nil { // takes precedence
|
||||||
|
r.RawRequest.SetBasicAuth(r.UserInfo.Username, r.UserInfo.Password)
|
||||||
|
isBasicAuth = true
|
||||||
|
} else if c.UserInfo != nil {
|
||||||
|
r.RawRequest.SetBasicAuth(c.UserInfo.Username, c.UserInfo.Password)
|
||||||
|
isBasicAuth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.DisableWarn {
|
||||||
|
if isBasicAuth && !strings.HasPrefix(r.URL, "https") {
|
||||||
|
c.Log.Println("WARNING - Using Basic Auth in HTTP mode is not secure.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token Auth
|
||||||
|
if !IsStringEmpty(r.Token) { // takes precedence
|
||||||
|
r.RawRequest.Header.Set(hdrAuthorizationKey, "Bearer "+r.Token)
|
||||||
|
} else if !IsStringEmpty(c.Token) {
|
||||||
|
r.RawRequest.Header.Set(hdrAuthorizationKey, "Bearer "+c.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestLogger(c *Client, r *Request) error {
|
||||||
|
if c.Debug {
|
||||||
|
rr := r.RawRequest
|
||||||
|
c.Log.Println()
|
||||||
|
c.disableLogPrefix()
|
||||||
|
c.Log.Println("---------------------- REQUEST LOG -----------------------")
|
||||||
|
c.Log.Printf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto)
|
||||||
|
c.Log.Printf("HOST : %s", rr.URL.Host)
|
||||||
|
c.Log.Println("HEADERS:")
|
||||||
|
for h, v := range rr.Header {
|
||||||
|
c.Log.Printf("%25s: %v", h, strings.Join(v, ", "))
|
||||||
|
}
|
||||||
|
c.Log.Printf("BODY :\n%v", r.fmtBodyString())
|
||||||
|
c.Log.Println("----------------------------------------------------------")
|
||||||
|
c.enableLogPrefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Response Middleware(s)
|
||||||
|
//
|
||||||
|
|
||||||
|
func responseLogger(c *Client, res *Response) error {
|
||||||
|
if c.Debug {
|
||||||
|
c.Log.Println()
|
||||||
|
c.disableLogPrefix()
|
||||||
|
c.Log.Println("---------------------- RESPONSE LOG -----------------------")
|
||||||
|
c.Log.Printf("STATUS : %s", res.Status())
|
||||||
|
c.Log.Printf("RECEIVED AT : %v", res.ReceivedAt())
|
||||||
|
c.Log.Printf("RESPONSE TIME : %v", res.Time())
|
||||||
|
c.Log.Println("HEADERS:")
|
||||||
|
for h, v := range res.Header() {
|
||||||
|
c.Log.Printf("%30s: %v", h, strings.Join(v, ", "))
|
||||||
|
}
|
||||||
|
if res.Request.isSaveResponse {
|
||||||
|
c.Log.Printf("BODY :\n***** RESPONSE WRITTEN INTO FILE *****")
|
||||||
|
} else {
|
||||||
|
c.Log.Printf("BODY :\n%v", res.fmtBodyString())
|
||||||
|
}
|
||||||
|
c.Log.Println("----------------------------------------------------------")
|
||||||
|
c.enableLogPrefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponseBody(c *Client, res *Response) (err error) {
|
||||||
|
// Handles only JSON or XML content type
|
||||||
|
ct := res.Header().Get(hdrContentTypeKey)
|
||||||
|
if IsJSONType(ct) || IsXMLType(ct) {
|
||||||
|
// Considered as Result
|
||||||
|
if res.StatusCode() > 199 && res.StatusCode() < 300 {
|
||||||
|
if res.Request.Result != nil {
|
||||||
|
err = Unmarshal(ct, res.body, res.Request.Result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Considered as Error
|
||||||
|
if res.StatusCode() > 399 {
|
||||||
|
// global error interface
|
||||||
|
if res.Request.Error == nil && c.Error != nil {
|
||||||
|
res.Request.Error = reflect.New(c.Error).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Request.Error != nil {
|
||||||
|
err = Unmarshal(ct, res.body, res.Request.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMultipart(c *Client, r *Request) (err error) {
|
||||||
|
r.bodyBuf = &bytes.Buffer{}
|
||||||
|
w := multipart.NewWriter(r.bodyBuf)
|
||||||
|
|
||||||
|
for k, v := range c.FormData {
|
||||||
|
for _, iv := range v {
|
||||||
|
w.WriteField(k, iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range r.FormData {
|
||||||
|
for _, iv := range v {
|
||||||
|
if strings.HasPrefix(k, "@") { // file
|
||||||
|
err = addFile(w, k[1:], iv)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else { // form value
|
||||||
|
w.WriteField(k, iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #21 - adding io.Reader support
|
||||||
|
if len(r.multipartFiles) > 0 {
|
||||||
|
for _, f := range r.multipartFiles {
|
||||||
|
err = addFileReader(w, f)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set(hdrContentTypeKey, w.FormDataContentType())
|
||||||
|
err = w.Close()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFormData(c *Client, r *Request) {
|
||||||
|
formData := url.Values{}
|
||||||
|
|
||||||
|
for k, v := range c.FormData {
|
||||||
|
for _, iv := range v {
|
||||||
|
formData.Add(k, iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range r.FormData {
|
||||||
|
// remove form data field from client level by key
|
||||||
|
// since overrides happens for that key in the request
|
||||||
|
formData.Del(k)
|
||||||
|
|
||||||
|
for _, iv := range v {
|
||||||
|
formData.Add(k, iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode()))
|
||||||
|
r.Header.Set(hdrContentTypeKey, formContentType)
|
||||||
|
r.isFormData = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleContentType(c *Client, r *Request) {
|
||||||
|
contentType := r.Header.Get(hdrContentTypeKey)
|
||||||
|
if IsStringEmpty(contentType) {
|
||||||
|
contentType = DetectContentType(r.Body)
|
||||||
|
r.Header.Set(hdrContentTypeKey, contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRequestBody(c *Client, r *Request) (err error) {
|
||||||
|
var bodyBytes []byte
|
||||||
|
contentType := r.Header.Get(hdrContentTypeKey)
|
||||||
|
kind := kindOf(r.Body)
|
||||||
|
|
||||||
|
if reader, ok := r.Body.(io.Reader); ok {
|
||||||
|
r.bodyBuf = &bytes.Buffer{}
|
||||||
|
r.bodyBuf.ReadFrom(reader)
|
||||||
|
} else if b, ok := r.Body.([]byte); ok {
|
||||||
|
bodyBytes = b
|
||||||
|
} else if s, ok := r.Body.(string); ok {
|
||||||
|
bodyBytes = []byte(s)
|
||||||
|
} else if IsJSONType(contentType) &&
|
||||||
|
(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
|
||||||
|
bodyBytes, err = json.Marshal(r.Body)
|
||||||
|
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
|
||||||
|
bodyBytes, err = xml.Marshal(r.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bodyBytes == nil && r.bodyBuf == nil {
|
||||||
|
err = errors.New("Unsupported 'Body' type/value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if any errors during body bytes handling, return it
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// []byte into Buffer
|
||||||
|
if bodyBytes != nil && r.bodyBuf == nil {
|
||||||
|
r.bodyBuf = bytes.NewBuffer(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveResponseIntoFile(c *Client, res *Response) error {
|
||||||
|
if res.Request.isSaveResponse {
|
||||||
|
file := ""
|
||||||
|
|
||||||
|
if len(c.outputDirectory) > 0 && !filepath.IsAbs(res.Request.outputFile) {
|
||||||
|
file += c.outputDirectory + string(filepath.Separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
file = filepath.Clean(file + res.Request.outputFile)
|
||||||
|
err := createDirectory(filepath.Dir(file))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
// io.Copy reads maximum 32kb size, it is perfect for large file download too
|
||||||
|
defer res.RawResponse.Body.Close()
|
||||||
|
written, err := io.Copy(outFile, res.RawResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.size = written
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2015 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package resty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedirectPolicy to regulate the redirects in the resty client.
|
||||||
|
// Objects implementing the RedirectPolicy interface can be registered as
|
||||||
|
//
|
||||||
|
// Apply function should return nil to continue the redirect jounery, otherwise
|
||||||
|
// return error to stop the redirect.
|
||||||
|
type RedirectPolicy interface {
|
||||||
|
Apply(req *http.Request, via []*http.Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// The RedirectPolicyFunc type is an adapter to allow the use of ordinary functions as RedirectPolicy.
|
||||||
|
// If f is a function with the appropriate signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls f.
|
||||||
|
type RedirectPolicyFunc func(*http.Request, []*http.Request) error
|
||||||
|
|
||||||
|
// Apply calls f(req, via).
|
||||||
|
func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error {
|
||||||
|
return f(req, via)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoRedirectPolicy is used to disable redirects in the HTTP client
|
||||||
|
// resty.SetRedirectPolicy(NoRedirectPolicy())
|
||||||
|
func NoRedirectPolicy() RedirectPolicy {
|
||||||
|
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||||
|
return errors.New("Auto redirect is disabled")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlexibleRedirectPolicy is convenient method to create No of redirect policy for HTTP client.
|
||||||
|
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
|
||||||
|
func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy {
|
||||||
|
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= noOfRedirect {
|
||||||
|
return fmt.Errorf("Stopped after %d redirects", noOfRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkHostAndAddHeaders(req, via[0])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DomainCheckRedirectPolicy is convenient method to define domain name redirect rule in resty client.
|
||||||
|
// Redirect is allowed for only mentioned host in the policy.
|
||||||
|
// resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
|
||||||
|
func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy {
|
||||||
|
hosts := make(map[string]bool)
|
||||||
|
for _, h := range hostnames {
|
||||||
|
hosts[strings.ToLower(h)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||||
|
if ok := hosts[getHostname(req.URL.Host)]; !ok {
|
||||||
|
return errors.New("Redirect is not allowed as per DomainCheckRedirectPolicy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostname(host string) (hostname string) {
|
||||||
|
if strings.Index(host, ":") > 0 {
|
||||||
|
host, _, _ = net.SplitHostPort(host)
|
||||||
|
hostname = strings.ToLower(host)
|
||||||
|
} else {
|
||||||
|
hostname = strings.ToLower(host)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default Golang will not redirect request headers
|
||||||
|
// after go throughing various discussion commments from thread
|
||||||
|
// https://github.com/golang/go/issues/4800
|
||||||
|
// go-resty will add all the headers during a redirect for the same host
|
||||||
|
func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) {
|
||||||
|
curHostname := getHostname(cur.URL.Host)
|
||||||
|
preHostname := getHostname(pre.URL.Host)
|
||||||
|
if strings.EqualFold(curHostname, preHostname) {
|
||||||
|
for key, val := range pre.Header {
|
||||||
|
cur.Header[key] = val
|
||||||
|
}
|
||||||
|
} else { // only library User-Agent header is added
|
||||||
|
cur.Header.Set(hdrUserAgentKey, fmt.Sprintf(hdrUserAgentValue, Version))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,494 @@
|
||||||
|
// Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package resty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request type is used to compose and send individual request from client
|
||||||
|
// go-resty is provide option override client level settings such as
|
||||||
|
// Auth Token, Basic Auth credentials, Header, Query Param, Form Data, Error object
|
||||||
|
// and also you can add more options for that particular request
|
||||||
|
//
|
||||||
|
type Request struct {
|
||||||
|
URL string
|
||||||
|
Method string
|
||||||
|
QueryParam url.Values
|
||||||
|
FormData url.Values
|
||||||
|
Header http.Header
|
||||||
|
UserInfo *User
|
||||||
|
Token string
|
||||||
|
Body interface{}
|
||||||
|
Result interface{}
|
||||||
|
Error interface{}
|
||||||
|
Time time.Time
|
||||||
|
RawRequest *http.Request
|
||||||
|
|
||||||
|
client *Client
|
||||||
|
bodyBuf *bytes.Buffer
|
||||||
|
isMultiPart bool
|
||||||
|
isFormData bool
|
||||||
|
setContentLength bool
|
||||||
|
isSaveResponse bool
|
||||||
|
outputFile string
|
||||||
|
proxyURL *url.URL
|
||||||
|
multipartFiles []*File
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader method is to set a single header field and its value in the current request.
|
||||||
|
// Example: To set `Content-Type` and `Accept` as `application/json`.
|
||||||
|
// resty.R().
|
||||||
|
// SetHeader("Content-Type", "application/json").
|
||||||
|
// SetHeader("Accept", "application/json")
|
||||||
|
//
|
||||||
|
// Also you can override header value, which was set at client instance level.
|
||||||
|
//
|
||||||
|
func (r *Request) SetHeader(header, value string) *Request {
|
||||||
|
r.Header.Set(header, value)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeaders method sets multiple headers field and its values at one go in the current request.
|
||||||
|
// Example: To set `Content-Type` and `Accept` as `application/json`
|
||||||
|
//
|
||||||
|
// resty.R().
|
||||||
|
// SetHeaders(map[string]string{
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// "Accept": "application/json",
|
||||||
|
// })
|
||||||
|
// Also you can override header value, which was set at client instance level.
|
||||||
|
//
|
||||||
|
func (r *Request) SetHeaders(headers map[string]string) *Request {
|
||||||
|
for h, v := range headers {
|
||||||
|
r.Header.Set(h, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryParam method sets single paramater and its value in the current request.
|
||||||
|
// It will be formed as query string for the request.
|
||||||
|
// Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
|
||||||
|
// resty.R().
|
||||||
|
// SetQueryParam("search", "kitchen papers").
|
||||||
|
// SetQueryParam("size", "large")
|
||||||
|
// Also you can override query params value, which was set at client instance level
|
||||||
|
//
|
||||||
|
func (r *Request) SetQueryParam(param, value string) *Request {
|
||||||
|
r.QueryParam.Add(param, value)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryParams method sets multiple paramaters and its values at one go in the current request.
|
||||||
|
// It will be formed as query string for the request.
|
||||||
|
// Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
|
||||||
|
// resty.R().
|
||||||
|
// SetQueryParams(map[string]string{
|
||||||
|
// "search": "kitchen papers",
|
||||||
|
// "size": "large",
|
||||||
|
// })
|
||||||
|
// Also you can override query params value, which was set at client instance level
|
||||||
|
//
|
||||||
|
func (r *Request) SetQueryParams(params map[string]string) *Request {
|
||||||
|
for p, v := range params {
|
||||||
|
r.QueryParam.Add(p, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMultiValueQueryParams method sets multiple paramaters with multi-value
|
||||||
|
// at one go in the current request. It will be formed as query string for the request.
|
||||||
|
// Example: `status=pending&status=approved&status=open` in the URL after `?` mark.
|
||||||
|
// resty.R().
|
||||||
|
// SetMultiValueQueryParams(url.Values{
|
||||||
|
// "status": []string{"pending", "approved", "open"},
|
||||||
|
// })
|
||||||
|
// Also you can override query params value, which was set at client instance level
|
||||||
|
//
|
||||||
|
func (r *Request) SetMultiValueQueryParams(params url.Values) *Request {
|
||||||
|
for p, v := range params {
|
||||||
|
for _, pv := range v {
|
||||||
|
r.QueryParam.Add(p, pv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQueryString method provides ability to use string as an input to set URL query string for the request.
|
||||||
|
//
|
||||||
|
// Using String as an input
|
||||||
|
// resty.R().
|
||||||
|
// SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
|
||||||
|
//
|
||||||
|
func (r *Request) SetQueryString(query string) *Request {
|
||||||
|
values, err := url.ParseQuery(strings.TrimSpace(query))
|
||||||
|
if err == nil {
|
||||||
|
for k := range values {
|
||||||
|
r.QueryParam.Add(k, values.Get(k))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.client.Log.Printf("ERROR [%v]", err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFormData method sets Form parameters and its values in the current request.
|
||||||
|
// It's applicable only HTTP method `POST` and `PUT` and requets content type would be set as
|
||||||
|
// `application/x-www-form-urlencoded`.
|
||||||
|
// resty.R().
|
||||||
|
// SetFormData(map[string]string{
|
||||||
|
// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
|
||||||
|
// "user_id": "3455454545",
|
||||||
|
// })
|
||||||
|
// Also you can override form data value, which was set at client instance level
|
||||||
|
//
|
||||||
|
func (r *Request) SetFormData(data map[string]string) *Request {
|
||||||
|
for k, v := range data {
|
||||||
|
r.FormData.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMultiValueFormData method sets multiple form paramaters with multi-value
|
||||||
|
// at one go in the current request.
|
||||||
|
// resty.R().
|
||||||
|
// SetMultiValueFormData(url.Values{
|
||||||
|
// "search_criteria": []string{"book", "glass", "pencil"},
|
||||||
|
// })
|
||||||
|
// Also you can override form data value, which was set at client instance level
|
||||||
|
//
|
||||||
|
func (r *Request) SetMultiValueFormData(params url.Values) *Request {
|
||||||
|
for k, v := range params {
|
||||||
|
for _, kv := range v {
|
||||||
|
r.FormData.Add(k, kv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBody method sets the request body for the request. It supports various realtime need easy.
|
||||||
|
// We can say its quite handy or powerful. Supported request body data types is `string`, `[]byte`,
|
||||||
|
// `struct` and `map`. Body value can be pointer or non-pointer. Automatic marshalling
|
||||||
|
// for JSON and XML content type, if it is `struct` or `map`.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// Struct as a body input, based on content type, it will be marshalled.
|
||||||
|
// resty.R().
|
||||||
|
// SetBody(User{
|
||||||
|
// Username: "jeeva@myjeeva.com",
|
||||||
|
// Password: "welcome2resty",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Map as a body input, based on content type, it will be marshalled.
|
||||||
|
// resty.R().
|
||||||
|
// SetBody(map[string]interface{}{
|
||||||
|
// "username": "jeeva@myjeeva.com",
|
||||||
|
// "password": "welcome2resty",
|
||||||
|
// "address": &Address{
|
||||||
|
// Address1: "1111 This is my street",
|
||||||
|
// Address2: "Apt 201",
|
||||||
|
// City: "My City",
|
||||||
|
// State: "My State",
|
||||||
|
// ZipCode: 00000,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// String as a body input. Suitable for any need as a string input.
|
||||||
|
// resty.R().
|
||||||
|
// SetBody(`{
|
||||||
|
// "username": "jeeva@getrightcare.com",
|
||||||
|
// "password": "admin"
|
||||||
|
// }`)
|
||||||
|
//
|
||||||
|
// []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc.
|
||||||
|
// resty.R().
|
||||||
|
// SetBody([]byte("This is my raw request, sent as-is"))
|
||||||
|
//
|
||||||
|
func (r *Request) SetBody(body interface{}) *Request {
|
||||||
|
r.Body = body
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetResult method is to register the response `Result` object for automatic unmarshalling in the RESTful mode
|
||||||
|
// if response status code is between 200 and 299 and content type either JSON or XML.
|
||||||
|
//
|
||||||
|
// Note: Result object can be pointer or non-pointer.
|
||||||
|
// resty.R().SetResult(&AuthToken{})
|
||||||
|
// // OR
|
||||||
|
// resty.R().SetResult(AuthToken{})
|
||||||
|
//
|
||||||
|
// Accessing a result value
|
||||||
|
// response.Result().(*AuthToken)
|
||||||
|
//
|
||||||
|
func (r *Request) SetResult(res interface{}) *Request {
|
||||||
|
r.Result = getPointer(res)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError method is to register the request `Error` object for automatic unmarshalling in the RESTful mode
|
||||||
|
// if response status code is greater than 399 and content type either JSON or XML.
|
||||||
|
//
|
||||||
|
// Note: Error object can be pointer or non-pointer.
|
||||||
|
// resty.R().SetError(&AuthError{})
|
||||||
|
// // OR
|
||||||
|
// resty.R().SetError(AuthError{})
|
||||||
|
//
|
||||||
|
// Accessing a error value
|
||||||
|
// response.Error().(*AuthError)
|
||||||
|
//
|
||||||
|
func (r *Request) SetError(err interface{}) *Request {
|
||||||
|
r.Error = getPointer(err)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFile method is to set single file field name and its path for multipart upload.
|
||||||
|
// resty.R().
|
||||||
|
// SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf")
|
||||||
|
//
|
||||||
|
func (r *Request) SetFile(param, filePath string) *Request {
|
||||||
|
r.isMultiPart = true
|
||||||
|
r.FormData.Set("@"+param, filePath)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFiles method is to set multiple file field name and its path for multipart upload.
|
||||||
|
// resty.R().
|
||||||
|
// SetFiles(map[string]string{
|
||||||
|
// "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf",
|
||||||
|
// "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf",
|
||||||
|
// "my_file3": "/Users/jeeva/Water Bill - Sep.pdf",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
func (r *Request) SetFiles(files map[string]string) *Request {
|
||||||
|
r.isMultiPart = true
|
||||||
|
|
||||||
|
for f, fp := range files {
|
||||||
|
r.FormData.Set("@"+f, fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFileReader method is to set single file using io.Reader for multipart upload.
|
||||||
|
// resty.R().
|
||||||
|
// SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)).
|
||||||
|
// SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes))
|
||||||
|
//
|
||||||
|
func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request {
|
||||||
|
r.isMultiPart = true
|
||||||
|
|
||||||
|
r.multipartFiles = append(r.multipartFiles, &File{
|
||||||
|
Name: fileName,
|
||||||
|
ParamName: param,
|
||||||
|
Reader: reader,
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentLength method sets the HTTP header `Content-Length` value for current request.
|
||||||
|
// By default go-resty won't set `Content-Length`. Also you have an option to enable for every
|
||||||
|
// request. See `resty.SetContentLength`
|
||||||
|
// resty.R().SetContentLength(true)
|
||||||
|
//
|
||||||
|
func (r *Request) SetContentLength(l bool) *Request {
|
||||||
|
r.setContentLength = true
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBasicAuth method sets the basic authentication header in the current HTTP request.
|
||||||
|
// For Header example:
|
||||||
|
// Authorization: Basic <base64-encoded-value>
|
||||||
|
//
|
||||||
|
// To set the header for username "go-resty" and password "welcome"
|
||||||
|
// resty.R().SetBasicAuth("go-resty", "welcome")
|
||||||
|
//
|
||||||
|
// This method overrides the credentials set by method `resty.SetBasicAuth`.
|
||||||
|
//
|
||||||
|
func (r *Request) SetBasicAuth(username, password string) *Request {
|
||||||
|
r.UserInfo = &User{Username: username, Password: password}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthToken method sets bearer auth token header in the current HTTP request. Header example:
|
||||||
|
// Authorization: Bearer <auth-token-value-comes-here>
|
||||||
|
//
|
||||||
|
// Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
|
||||||
|
//
|
||||||
|
// resty.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
|
||||||
|
//
|
||||||
|
// This method overrides the Auth token set by method `resty.SetAuthToken`.
|
||||||
|
//
|
||||||
|
func (r *Request) SetAuthToken(token string) *Request {
|
||||||
|
r.Token = token
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput method sets the output file for current HTTP request. Current HTTP response will be
|
||||||
|
// saved into given file. It is similar to `curl -o` flag. Absoulte path or relative path can be used.
|
||||||
|
// If is it relative path then output file goes under the output directory, as mentioned
|
||||||
|
// in the `Client.SetOutputDirectory`.
|
||||||
|
// resty.R().
|
||||||
|
// SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip").
|
||||||
|
// Get("http://bit.ly/1LouEKr")
|
||||||
|
//
|
||||||
|
// Note: In this scenario `Response.Body` might be nil.
|
||||||
|
func (r *Request) SetOutput(file string) *Request {
|
||||||
|
r.outputFile = file
|
||||||
|
r.isSaveResponse = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProxy method sets the Proxy URL for current Request. It does not affect client level
|
||||||
|
// proxy settings. Request level proxy settings takes higher priority, even though client
|
||||||
|
// level proxy settings exists.
|
||||||
|
// resty.R().
|
||||||
|
// SetProxy("http://proxyserver:8888").
|
||||||
|
// Get("http://httpbin.org/get")
|
||||||
|
//
|
||||||
|
func (r *Request) SetProxy(proxyURL string) *Request {
|
||||||
|
if pURL, err := url.Parse(proxyURL); err == nil {
|
||||||
|
r.proxyURL = pURL
|
||||||
|
} else {
|
||||||
|
r.client.Log.Printf("ERROR [%v]", err)
|
||||||
|
r.proxyURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// HTTP verb method starts here
|
||||||
|
//
|
||||||
|
|
||||||
|
// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231.
|
||||||
|
func (r *Request) Get(url string) (*Response, error) {
|
||||||
|
return r.Execute(GET, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231.
|
||||||
|
func (r *Request) Head(url string) (*Response, error) {
|
||||||
|
return r.Execute(HEAD, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231.
|
||||||
|
func (r *Request) Post(url string) (*Response, error) {
|
||||||
|
return r.Execute(POST, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231.
|
||||||
|
func (r *Request) Put(url string) (*Response, error) {
|
||||||
|
return r.Execute(PUT, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231.
|
||||||
|
func (r *Request) Delete(url string) (*Response, error) {
|
||||||
|
return r.Execute(DELETE, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231.
|
||||||
|
func (r *Request) Options(url string) (*Response, error) {
|
||||||
|
return r.Execute(OPTIONS, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789.
|
||||||
|
func (r *Request) Patch(url string) (*Response, error) {
|
||||||
|
return r.Execute(PATCH, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute method performs the HTTP request with given HTTP method and URL
|
||||||
|
// for current `Request`.
|
||||||
|
// resp, err := resty.R().Execute(resty.GET, "http://httpbin.org/get")
|
||||||
|
//
|
||||||
|
func (r *Request) Execute(method, url string) (*Response, error) {
|
||||||
|
if r.isMultiPart && !(method == POST || method == PUT) {
|
||||||
|
return nil, fmt.Errorf("Multipart content is not allowed in HTTP verb [%v]", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Method = method
|
||||||
|
r.URL = url
|
||||||
|
|
||||||
|
if r.client.RetryCount == 0 {
|
||||||
|
return r.client.execute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *Response
|
||||||
|
var err error
|
||||||
|
attempt := 0
|
||||||
|
_ = Backoff(func() (*Response, error) {
|
||||||
|
attempt++
|
||||||
|
resp, err = r.client.execute(r)
|
||||||
|
if err != nil {
|
||||||
|
r.client.Log.Printf("ERROR [%v] Attempt [%v]", err, attempt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}, Retries(r.client.RetryCount), RetryConditions(r.client.RetryConditions))
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) fmtBodyString() (body string) {
|
||||||
|
body = "***** NO CONTENT *****"
|
||||||
|
if isPayloadSupported(r.Method) {
|
||||||
|
// multipart or form-data
|
||||||
|
if r.isMultiPart || r.isFormData {
|
||||||
|
body = string(r.bodyBuf.Bytes())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// request body data
|
||||||
|
if r.Body != nil {
|
||||||
|
var prtBodyBytes []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
contentType := r.Header.Get(hdrContentTypeKey)
|
||||||
|
kind := kindOf(r.Body)
|
||||||
|
if IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map) {
|
||||||
|
prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ")
|
||||||
|
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
|
||||||
|
prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ")
|
||||||
|
} else if b, ok := r.Body.(string); ok {
|
||||||
|
if IsJSONType(contentType) {
|
||||||
|
bodyBytes := []byte(b)
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err = json.Indent(&out, bodyBytes, "", " "); err == nil {
|
||||||
|
prtBodyBytes = out.Bytes()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body = b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if b, ok := r.Body.([]byte); ok {
|
||||||
|
body = base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prtBodyBytes != nil {
|
||||||
|
body = string(prtBodyBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package resty provides simple HTTP and REST client for Go inspired by Ruby rest-client.
|
||||||
|
package resty
|
||||||
|
|
||||||
|
// Version # of go-resty
|
||||||
|
var Version = "0.9"
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright (c) 2015-2016 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||||
|
// resty source code and usage is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package resty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMaxRetries = 3
|
||||||
|
defaultWaitTime = 100 // base Milliseconds
|
||||||
|
defaultMaxWaitTime = 2000 // cap level Milliseconds
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option is to create convenient retry options like wait time, max retries, etc.
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// RetryConditionFunc type is for retry condition function
|
||||||
|
type RetryConditionFunc func(*Response) (bool, error)
|
||||||
|
|
||||||
|
// Options to hold go-resty retry values
|
||||||
|
type Options struct {
|
||||||
|
maxRetries int
|
||||||
|
waitTime int
|
||||||
|
maxWaitTime int
|
||||||
|
retryConditions []RetryConditionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retries sets the max number of retries
|
||||||
|
func Retries(value int) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.maxRetries = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitTime sets the default wait time to sleep between requests
|
||||||
|
func WaitTime(value int) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.waitTime = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxWaitTime sets the max wait time to sleep between requests
|
||||||
|
func MaxWaitTime(value int) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.maxWaitTime = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryConditions sets the conditions that will be checked for retry.
|
||||||
|
func RetryConditions(conditions []RetryConditionFunc) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.retryConditions = conditions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoff retries with increasing timeout duration up until X amount of retries
|
||||||
|
// (Default is 3 attempts, Override with option Retries(n))
|
||||||
|
func Backoff(operation func() (*Response, error), options ...Option) error {
|
||||||
|
// Defaults
|
||||||
|
opts := Options{
|
||||||
|
maxRetries: defaultMaxRetries,
|
||||||
|
waitTime: defaultWaitTime,
|
||||||
|
maxWaitTime: defaultMaxWaitTime,
|
||||||
|
retryConditions: []RetryConditionFunc{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range options {
|
||||||
|
o(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
resp *Response
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
base := float64(opts.waitTime) // Time to wait between each attempt
|
||||||
|
capLevel := float64(opts.maxWaitTime) // Maximum amount of wait time for the retry
|
||||||
|
for attempt := 0; attempt < opts.maxRetries; attempt++ {
|
||||||
|
resp, err = operation()
|
||||||
|
|
||||||
|
var needsRetry bool
|
||||||
|
var conditionErr error
|
||||||
|
for _, condition := range opts.retryConditions {
|
||||||
|
needsRetry, conditionErr = condition(resp)
|
||||||
|
if needsRetry || conditionErr != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the operation returned no error, there was no condition satisfied and
|
||||||
|
// there was no error caused by the conditional functions.
|
||||||
|
if err == nil && !needsRetry && conditionErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Adding capped exponential backup with jitter
|
||||||
|
// See the following article...
|
||||||
|
// http://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||||
|
temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
|
||||||
|
sleepTime := int(temp/2) + rand.Intn(int(temp/2))
|
||||||
|
|
||||||
|
sleepDuration := time.Duration(sleepTime) * time.Millisecond
|
||||||
|
time.Sleep(sleepDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -2139,6 +2139,12 @@
|
||||||
"revision": "7cd5fed006859e86dd5641a6cf9812e855b7574a",
|
"revision": "7cd5fed006859e86dd5641a6cf9812e855b7574a",
|
||||||
"revisionTime": "2016-08-11T16:27:25Z"
|
"revisionTime": "2016-08-11T16:27:25Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "lOEkLP94OsQSLFp+38rY1GjnMtk=",
|
||||||
|
"path": "github.com/paultyng/go-newrelic/api",
|
||||||
|
"revision": "81a8e05b0e494285f1322f99f3c6f93c8f1192b1",
|
||||||
|
"revisionTime": "2016-11-29T00:49:55Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "github.com/pborman/uuid",
|
"path": "github.com/pborman/uuid",
|
||||||
"revision": "dee7705ef7b324f27ceb85a121c61f2c2e8ce988"
|
"revision": "dee7705ef7b324f27ceb85a121c61f2c2e8ce988"
|
||||||
|
@ -2232,6 +2238,12 @@
|
||||||
"path": "github.com/tent/http-link-go",
|
"path": "github.com/tent/http-link-go",
|
||||||
"revision": "ac974c61c2f990f4115b119354b5e0b47550e888"
|
"revision": "ac974c61c2f990f4115b119354b5e0b47550e888"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "y1hkty5dgBN9elK4gP1TtVjT4e8=",
|
||||||
|
"path": "github.com/tomnomnom/linkheader",
|
||||||
|
"revision": "236df730ed7334edb33cb10ba79407491fb4e147",
|
||||||
|
"revisionTime": "2016-06-19T23:20:27Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "jfIUoeCY4uLz1zCgnCxndi5/UNE=",
|
"checksumSHA1": "jfIUoeCY4uLz1zCgnCxndi5/UNE=",
|
||||||
"path": "github.com/ugorji/go/codec",
|
"path": "github.com/ugorji/go/codec",
|
||||||
|
@ -2365,6 +2377,18 @@
|
||||||
"path": "golang.org/x/net/context/ctxhttp",
|
"path": "golang.org/x/net/context/ctxhttp",
|
||||||
"revision": "04b9de9b512f58addf28c9853d50ebef61c3953e"
|
"revision": "04b9de9b512f58addf28c9853d50ebef61c3953e"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "GIGmSrYACByf5JDIP9ByBZksY80=",
|
||||||
|
"path": "golang.org/x/net/idna",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "AmZIW67T/HUlTTflTmOIy6jdq74=",
|
||||||
|
"path": "golang.org/x/net/publicsuffix",
|
||||||
|
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
|
||||||
|
"revisionTime": "2016-11-15T21:05:04Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "golang.org/x/oauth2",
|
"path": "golang.org/x/oauth2",
|
||||||
"revision": "2897dcade18a126645f1368de827f1e613a60049"
|
"revision": "2897dcade18a126645f1368de827f1e613a60049"
|
||||||
|
@ -2505,6 +2529,12 @@
|
||||||
"comment": "v1.8.5",
|
"comment": "v1.8.5",
|
||||||
"path": "gopkg.in/ini.v1",
|
"path": "gopkg.in/ini.v1",
|
||||||
"revision": "77178f22699a4ecafce485fb8d86b7afeb7e3e28"
|
"revision": "77178f22699a4ecafce485fb8d86b7afeb7e3e28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "mkLQOQwQwoUc9Kr9+PaVGrKUzI4=",
|
||||||
|
"path": "gopkg.in/resty.v0",
|
||||||
|
"revision": "24dc7ba4bc1ef9215048b28e7248f99c42901db5",
|
||||||
|
"revisionTime": "2016-11-01T17:03:53Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rootPath": "github.com/hashicorp/terraform"
|
"rootPath": "github.com/hashicorp/terraform"
|
||||||
|
|
Loading…
Reference in New Issue