Closed ysahil97 closed 3 years ago
Here is my view of things:
package zoho
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"github.com/schmorrison/go-querystring/query" // small change from 'github.com/google/go-querystring/query'
)
// Endpoint defines the data required to interact with most Zoho REST api endpoints
type Endpoint struct {
Method HTTPMethod
URL string
Name string
ResponseData interface{}
RequestBody interface{}
URLParameters map[string]Parameter
Headers map[string]string
BodyFormat BodyFormat
Attachment string
}
// Parameter is used to provide URL Parameters to zoho endpoints
type Parameter string
type BodyFormat string
const (
JSON = ""
JSON_STRING = "jsonString"
FILE = "file"
URL = "url" // Added new BodyFormat option
)
// HTTPRequest is the function which actually performs the request to a Zoho endpoint as specified by the provided endpoint
func (z *Zoho) HTTPRequest(endpoint *Endpoint) (err error) {
if reflect.TypeOf(endpoint.ResponseData).Kind() != reflect.Ptr {
return fmt.Errorf("Failed, you must pass a pointer in the ResponseData field of endpoint")
}
// Load and renew access token if expired
err = z.CheckForSavedTokens()
if err == ErrTokenExpired {
err := z.RefreshTokenRequest()
if err != nil {
return fmt.Errorf("Failed to refresh the access token: %s: %s", endpoint.Name, err)
}
}
// Retrieve URL parameters
endpointURL := endpoint.URL
q := url.Values{}
for k, v := range endpoint.URLParameters {
if v != "" {
q.Set(k, string(v))
}
}
var (
req *http.Request
reqBody io.Reader
contentType string
)
// Has a body, likely a CRUD operation (still possibly JSONString)
if endpoint.BodyFormat == JSON || endpoint.BodyFormat == JSON_STRING {
if endpoint.RequestBody != nil {
// JSON Marshal the body
marshalledBody, err := json.Marshal(endpoint.RequestBody)
if err != nil {
return fmt.Errorf("Failed to create json from request body")
}
reqBody = bytes.NewReader(marshalledBody)
contentType = "application/x-www-form-urlencoded; charset=UTF-8"
}
}
if endpoint.BodyFormat == JSON_STRING || endpoint.BodyFormat == FILE {
// Create a multipart form
var b bytes.Buffer
w := multipart.NewWriter(&b)
switch endpoint.BodyFormat {
case JSON_STRING:
// Use the form to create the proper field
fw, err := w.CreateFormField("JSONString")
if err != nil {
return err
}
// Copy the request body JSON into the field
if _, err = io.Copy(fw, reqBody); err != nil {
return err
}
// Close the multipart writer to set the terminating boundary
err = w.Close()
if err != nil {
return err
}
case FILE:
// Retreive the file contents
fileReader, err := os.Open(endpoint.Attachment)
if err != nil {
return err
}
defer fileReader.Close()
// Create the correct form field
part, err := w.CreateFormFile("attachment", filepath.Base(endpoint.Attachment))
if err != nil {
return err
}
// copy the file contents to the form
if _, err = io.Copy(part, fileReader); err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
}
reqBody = &b
contentType = w.FormDataContentType()
}
// New BodyFormat encoding option
if endpoint.BodyFormat == URL {
body, err := query.Values(endpoint.RequestBody) // send struct into the newly imported package
if err != nil {
return err
}
reqBody = strings.NewReader(body.Encode()) // write to body
contentType = "application/x-www-form-urlencoded; charset=UTF-8"
}
/// NOTHING CHANGED PAST THIS POINT
req, err = http.NewRequest(string(endpoint.Method), fmt.Sprintf("%s?%s", endpointURL, q.Encode()), reqBody)
if err != nil {
return fmt.Errorf("Failed to create a request for %s: %s", endpoint.Name, err)
}
req.Header.Set("Content-Type", contentType)
// Add global authorization header
req.Header.Add("Authorization", "Zoho-oauthtoken "+z.oauth.token.AccessToken)
// Add specific endpoint headers
for k, v := range endpoint.Headers {
req.Header.Add(k, v)
}
resp, err := z.client.Do(req)
if err != nil {
return fmt.Errorf("Failed to perform request for %s: %s", endpoint.Name, err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Failed to read body of response for %s: got status %s: %s", endpoint.Name, resolveStatus(resp), err)
}
dataType := reflect.TypeOf(endpoint.ResponseData).Elem()
data := reflect.New(dataType).Interface()
err = json.Unmarshal(body, data)
if err != nil {
return fmt.Errorf("Failed to unmarshal data from response for %s: got status %s: %s", endpoint.Name, resolveStatus(resp), err)
}
endpoint.ResponseData = data
return nil
}
..............................................................................
Please review this and let me know what you think. Test it out to make sure it works with your project and endpoints.
Thanks again for the PR!
Forgot to show how to use the url encoding on the struct.
type CustomerDetails struct {
Name string `json:"name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
}
// Note the change to tag `url` instead of `json`
type BookAppointmentData struct {
ServiceId string `url:"service_id"`
StaffId string `url:"staff_id,omitempty"`
ResourceId string `url:"resource_id,omitempty"`
FromTime string `url:"from_time"`
TimeZone string `url:"time_zone,omitempty"`
Customer_Details CustomerDetails `url:"customer_details,json,omitempty"` // Note the option `json` before `omitempty`, the order shouldn't matter
}
// The struct as the given request body
func (c *API) BookAppointment(request BookAppointmentData) (data AppointmentResponse, err error) {
endpoint := zoho.Endpoint{
Name: BookAppointmentModule,
URL: fmt.Sprintf("https://www.zohoapis.%s/bookings/v1/json/%s",c.ZohoTLD,BookAppointmentModule),
Method: zoho.HTTPPost,
ResponseData: &AppointmentResponse{},
RequestBody: request,
BodyFormat: zoho.URL, // Use the URL BodyFormat
}
err = c.Zoho.HTTPRequest(&endpoint)
if err != nil {
return AppointmentResponse{}, fmt.Errorf("Failed to book appointment: %s", err)
}
if v, ok := endpoint.ResponseData.(*AppointmentResponse); ok {
return *v, nil
}
return AppointmentResponse{}, fmt.Errorf("Data retrieved was not 'AppointmentResponse'")
}
Hi Sam
The above suggested workaround to accommodate Zoho Booking requests seems to be good for me. I have tested with all the post API's present in Zoho Bookings API, and they seem to work fine.
I have an additional point to make towards one of the API's, namely BookAppointment api. In this API, we also need to enter some more personal information but customer_details only provides fields for name, email and phone number. As a workaround, Zoho Bookings has created a custom field API (link) which would solve this problem. The issue with incorporating custom fields in our PR is that, because of their nature of the fields, these are created separately for each project case and therefore there can't be any standardized library implementation of it. For our use case, we are planning to maintain a fork of your library and we would be using that fork as the library for our use cases. I will also add an README note for the same.
Could we finalize on your changes and bring closure to this PR? PS: In your fork of go-querystring, the go.mod file needs to be changed to reflect proper owner name.
Best Regards Sahil Yerawar
On Sun, Jul 25, 2021 at 12:11 AM Sam Morrison @.***> wrote:
Forgot to show how to use the url encoding on the struct.
type CustomerDetails struct { Name string
json:"name"
Email stringjson:"email"
PhoneNumber stringjson:"phone_number"
}// Note the change to tag
url
instead ofjson
type BookAppointmentData struct { ServiceId stringurl:"service_id"
StaffId stringurl:"staff_id,omitempty"
ResourceId stringurl:"resource_id,omitempty"
FromTime stringurl:"from_time"
TimeZone stringurl:"time_zone,omitempty"
Customer_Details CustomerDetailsurl:"customer_details,json,omitempty"
// Note the optionjson
beforeomitempty
, the order shouldn't matter }// The struct as the given request body func (c *API) BookAppointment(request BookAppointmentData) (data AppointmentResponse, err error) { endpoint := zoho.Endpoint{ Name: BookAppointmentModule, URL: fmt.Sprintf("https://www.zohoapis.%s/bookings/v1/json/%s",c.ZohoTLD,BookAppointmentModule), Method: zoho.HTTPPost, ResponseData: &AppointmentResponse{}, RequestBody: request, BodyFormat: zoho.URL, // Use the URL BodyFormat }
err = c.Zoho.HTTPRequest(&endpoint) if err != nil { return AppointmentResponse{}, fmt.Errorf("Failed to book appointment: %s", err) } if v, ok := endpoint.ResponseData.(*AppointmentResponse); ok {
return *v, nil
} return AppointmentResponse{}, fmt.Errorf("Data retrieved was not 'AppointmentResponse'") }
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/schmorrison/Zoho/pull/28#issuecomment-886095062, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEWHICSPCSILPKZ445ZCTB3TZMCPFANCNFSM476O4GFA .
-- Thanking You Sahil Yerawar
Sorry, I forgot to add the link to the custom field API documentation: https://www.zoho.com/bookings/help/api/v1/book-appointment.html
On Mon, Jul 26, 2021 at 4:11 PM Sahil Yerawar @.***> wrote:
Hi Sam
The above suggested workaround to accommodate Zoho Booking requests seems to be good for me. I have tested with all the post API's present in Zoho Bookings API, and they seem to work fine.
I have an additional point to make towards one of the API's, namely BookAppointment api. In this API, we also need to enter some more personal information but customer_details only provides fields for name, email and phone number. As a workaround, Zoho Bookings has created a custom field API (link) which would solve this problem. The issue with incorporating custom fields in our PR is that, because of their nature of the fields, these are created separately for each project case and therefore there can't be any standardized library implementation of it. For our use case, we are planning to maintain a fork of your library and we would be using that fork as the library for our use cases. I will also add an README note for the same.
Could we finalize on your changes and bring closure to this PR? PS: In your fork of go-querystring, the go.mod file needs to be changed to reflect proper owner name.
Best Regards Sahil Yerawar
On Sun, Jul 25, 2021 at 12:11 AM Sam Morrison @.***> wrote:
Forgot to show how to use the url encoding on the struct.
type CustomerDetails struct { Name string
json:"name"
Email stringjson:"email"
PhoneNumber stringjson:"phone_number"
}// Note the change to tag
url
instead ofjson
type BookAppointmentData struct { ServiceId stringurl:"service_id"
StaffId stringurl:"staff_id,omitempty"
ResourceId stringurl:"resource_id,omitempty"
FromTime stringurl:"from_time"
TimeZone stringurl:"time_zone,omitempty"
Customer_Details CustomerDetailsurl:"customer_details,json,omitempty"
// Note the optionjson
beforeomitempty
, the order shouldn't matter }// The struct as the given request body func (c *API) BookAppointment(request BookAppointmentData) (data AppointmentResponse, err error) { endpoint := zoho.Endpoint{ Name: BookAppointmentModule, URL: fmt.Sprintf("https://www.zohoapis.%s/bookings/v1/json/%s",c.ZohoTLD,BookAppointmentModule), Method: zoho.HTTPPost, ResponseData: &AppointmentResponse{}, RequestBody: request, BodyFormat: zoho.URL, // Use the URL BodyFormat }
err = c.Zoho.HTTPRequest(&endpoint) if err != nil { return AppointmentResponse{}, fmt.Errorf("Failed to book appointment: %s", err) } if v, ok := endpoint.ResponseData.(*AppointmentResponse); ok {
return *v, nil
} return AppointmentResponse{}, fmt.Errorf("Data retrieved was not 'AppointmentResponse'") }
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/schmorrison/Zoho/pull/28#issuecomment-886095062, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEWHICSPCSILPKZ445ZCTB3TZMCPFANCNFSM476O4GFA .
-- Thanking You Sahil Yerawar
-- Thanking You Sahil Yerawar
I can't review/merge right now (ill try during this week), but take a look at this with regard to custom fields, struct embedding is almost exactly that. Its been on the roadmap forever.
https://play.golang.org/p/pH4iB-6VJ-a
It does require a bit of looking at, most Zoho responses come wrapped in something, for bookings you get {"response":{"returnValue": {......}, logMessage: [], status: ""}}
and we would really want to do the embedding at the returnValue
level. There are a couple ways to reconfigure the structs to allow it, but right now most structs in most packages are just faithful conversions from JSON.
If you create an issue about this we can decide the best way in a separate thread.
Your welcome! Could you please merge the changes in the main branch?
Hi Sam
As discussed, I have written some API's for Zoho Bookings API which we currently want to use. The api's implemented here are related to appointments, staffs, resources, services and workspaces. Please let me know if there are any doubts or concerns in this pull request.