package main import ( "fmt" "github.com/PuerkitoBio/goquery" "github.com/benbjohnson/phantomjs" "log" "math" "net/http" "strconv" "strings" ) const validNumberOfURLParams = 5 const sizeParamIndex = 3 const countParamIndex = 4 const validNumberOfSizeParams = 2 const maxExpectedNumberOfQuantities = 15 const numberOfTabsToSizeGroup = 12 const numberOfArrowDownToCustomSize = 4 const validMinWidth = 25.4 const validMaxWidth = 914.4 const validMinHeight = 25.4 const validMaxHeigh = 609.59 const epsilon = 0.1 const stickerMuleURL = "https://www.stickermule.com/uk/products/die-cut-stickers" const ( invalidURL = "URL is not valid" invalidNumberOfParamsError = "Number of params not correct, expected /WIDTHxLENGTH/count" invalidNumberOfSizeParamError = "Size not correct, expected /WIDTHxLENGTH/count" invalidParamsType = "Params should be numbers" invalidParamsSize = "Params should be in range" errorSimulatingInput = "Error with simulating user input" tooManyQuantities = "Too many quantities. Didn't expect this" failedToGetPrice = "Failed to get price" ) var listOfValidQuantities []int16 var stickerMuleWebPage *phantomjs.WebPage var stickerMuleWebPageDocument *goquery.Document func getListOfQuantities() error { result := make([]int16, maxExpectedNumberOfQuantities) resultCount := 0 wasError := false whichError := error(nil) stickerMuleWebPageDocument.Find("label.quantity").Each(func(_ int, s *goquery.Selection) { if wasError { return } numberWithoutComma := strings.Replace(s.Text(), ",", "", -1) trimmedNumber := strings.TrimSpace(numberWithoutComma) if sizeInt64, err := strconv.ParseInt(trimmedNumber, 10, 16); err != nil { wasError = true whichError = err } else { result[resultCount] = int16(sizeInt64) } resultCount += 1 if resultCount == 15 { wasError = true whichError = fmt.Errorf(tooManyQuantities) } }) if wasError { return whichError } listOfValidQuantities = result[:resultCount] return nil } func floatToString(number float64) string { return strconv.FormatFloat(number, 'f', 1, 64) } func getStickerPrice(width, height float64, quantity int) (int, error) { //simulate click on "Custom size radio button" if _, err := stickerMuleWebPage.Evaluate("function(){document.getElementById('variant_77').click();}"); err != nil { return 0, err } //set value for width if _, err := stickerMuleWebPage.Evaluate("function(){document.getElementById('width').value = " + floatToString(width) + ";}"); err != nil { return 0, err } //set value for height if _, err := stickerMuleWebPage.Evaluate("function(){document.getElementById('height').value = " + floatToString(height) + ";}"); err != nil { return 0, err } //check if input boxes accepted passed values valuesFromInputBoxes, err := stickerMuleWebPage.Evaluate("function(){return {width: document.getElementById('width').value, height: document.getElementById('height').value};}") if err != nil { return 0, err } valuesMap, ok := valuesFromInputBoxes.(map[string]interface{}) if !ok { return 0, fmt.Errorf(invalidParamsType) } redWidthString, ok := valuesMap["width"].(string) if !ok { return 0, fmt.Errorf(invalidParamsType) } redHeightString, ok := valuesMap["height"].(string) if !ok { return 0, fmt.Errorf(invalidParamsType) } widthValue, err := strconv.ParseFloat(redWidthString, 64) if err != nil { return 0, fmt.Errorf(invalidParamsType) } heightValue, err := strconv.ParseFloat(redHeightString, 64) if err != nil { return 0, fmt.Errorf(invalidParamsType) } if math.Abs(widthValue-width) > epsilon || math.Abs(heightValue-height) > epsilon { return 0, fmt.Errorf(invalidParamsType) } //Now read price for selected quantity priceValue, err := stickerMuleWebPage.Evaluate("function(){return {price: document.getElementById('price_200_id').innerHTML};}") //TODO : price is empty because setting value for width and height doesn't fire event listener //on input fields and prices are not loaded. Find another way to simulate user input priceMap, ok := priceValue.(map[string]interface{}) if !ok { return 0, fmt.Errorf(failedToGetPrice) } _, ok = priceMap["price"].(string) if !ok { return 0, fmt.Errorf(failedToGetPrice) } return 100, nil } func errorResponse(w http.ResponseWriter, errType string) { fmt.Fprintf(w, errType) } func diecutHandler(w http.ResponseWriter, r *http.Request) { if params := strings.Split(r.URL.Path, "/"); len(params) == validNumberOfURLParams { if size := strings.Split(params[sizeParamIndex], "x"); len(size) == validNumberOfSizeParams { count := params[countParamIndex] width, wOk := strconv.ParseFloat(size[0], 64) height, hOk := strconv.ParseFloat(size[1], 64) countNumber, cOk := strconv.ParseInt(count, 10, 16) countNumber16 := int16(countNumber) foundValidQuantity := false for _, value := range listOfValidQuantities { if value == countNumber16 { foundValidQuantity = true break } } validWidth := width >= validMinWidth && width <= validMaxWidth validHeight := height >= validMinHeight && height <= validMaxHeigh if wOk != nil || hOk != nil || cOk != nil || !foundValidQuantity || !validWidth || !validHeight { errorResponse(w, invalidParamsType) } else { price, err := getStickerPrice(width, height, int(countNumber)) if err != nil { errorResponse(w, failedToGetPrice) } else { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, "{price:%d, currency: \"USD\"}", price) } } } else { errorResponse(w, invalidNumberOfSizeParamError) } } else { errorResponse(w, invalidNumberOfParamsError) } } func defaultHandler(w http.ResponseWriter, r *http.Request) { errorResponse(w, invalidURL) } func initWebPage() error { p := phantomjs.DefaultProcess localStickerMuleWebPage, err := p.CreateWebPage() if err != nil { return err } //keep pointer to web page in global space, so page is not re-created every time API is called stickerMuleWebPage = localStickerMuleWebPage // Open a URL. if err := stickerMuleWebPage.Open(stickerMuleURL); err != nil { return err } content, err := stickerMuleWebPage.Content() if err != nil { return err } stickerMuleWebPageDocument, err = goquery.NewDocumentFromReader(strings.NewReader(content)) if err != nil { return err } return nil } func closingCleanUp() { phantomjs.DefaultProcess.Close() stickerMuleWebPage.Close() } func main() { //init phantomJS if err := phantomjs.DefaultProcess.Open(); err != nil { fmt.Println("Error : ", err) return } defer closingCleanUp() //init web page and document if err := initWebPage(); err != nil { fmt.Println("Error : ", err) return } //init valid values for stickers quantity if err := getListOfQuantities(); err != nil { fmt.Println("Error : ", err) return } http.HandleFunc("/prices/diecut/", diecutHandler) http.HandleFunc("/", defaultHandler) fmt.Println("Server running on localhost:8080") log.Fatal(http.ListenAndServe(":8080", nil)) }