DigitalOcean Functions Challenge를 완료하기 위한 Go CLI 작성

도전



DigitalOcean은 최근 serverless functionsreleased a challenge 버전을 출시하여 선보이기도 했습니다. 도전을 거절하는 것을 좋아하지 않는 사람으로서 저는 Go CLI를 사용하여 DigitalOcean의 도전을 받아들였습니다.

시작하기



첫째, challenge website에 대한 챌린지를 읽는 것은 매우 간단했지만 몇 가지 질문에 답해야 했습니다.
  • Content-TypeAccepted 헤더는 이 응용 프로그램이 JSON 데이터를 수락하고 다시 전송한다고 알려주지만 전체 문서의 지침에는 매개 변수 파일이 JSON 문서라는 것 외에는 실제로 언급되어 있지 않습니다. 계속해서 API가 JSON을 수락하고 다시 보낸다고 생각하여 CLI를 작성했습니다. 나는 옳았지만 여기의 문서는 완전히 명확하지 않았습니다.
  • 일단 "새미"(또는 DigitalOcean 마스코트)를 배치했습니다. 그것을 잡으려면 예리한 눈이 있어야합니다. 그것이 거기에 있다는 것을 믿을 수 없다면, 당신이 만든 Sammy처럼 보이는 것이 있는지 페이지 요소를 쉽게 검사할 수 있습니다.

  • 코드



    이 경우 이것은 그냥 버리는 앱이었기 때문에 모든 것을 main.go 파일에 작성하고 go mod init <github.com/your_handle/project_name> 가 있는 모듈을 켭니다.

    가장 먼저 할 일은 지침에 설명된 대로 nametype 매개변수를 허용하는 CLI로 시작하는 것이었습니다. 저는 이 플래그를 init 함수에 넣고 몇 가지 모듈 변수를 로드하기로 했습니다. 그래서 실제로 많은 작업을 수행할 필요가 없었고 앱이 실행될 때마다 플래그가 실행될 것으로 예상했습니다.

    // main.go
    
    package main
    
    import (
        "flag"
    )
    
    var (
        sammyname string
        sammytype string
    )
    
    const (
        API_URL = "https://functionschallenge.digitalocean.com/api/sammy"
    )
    
    func init() {
        flag.StringVar(&sammyname, "name", "", "The name to give your new Sammy.")
        flag.StringVar(&sammytype, "type", "", "The type to assign to your new Sammy.")
        flag.Parse()
    }
    
    func main() {}
    

    go run main.go로 앱을 실행하면 이제 명령줄 플래그가 제공됩니다.

    문서를 다시 살펴보면 Sammy Type 매개변수는 실제로 열거형이므로 유형과 몇 가지 도우미 함수를 만들고 변수를 적절하게 설정하는 데 사용했습니다.

    // main.go
    ...
    
    import (
        "fmt"
        "log"
        "flag"
        "strings"
    )
    
    var (
        sammyname string
        sammytype sammyType
    )
    
    type (
        sammyType string
    )
    
    const (
        API_URL = "https://functionschallenge.digitalocean.com/api/sammy"
    
        sammyType_Sammy sammyType = "sammy"
        sammyType_Punk sammyType = "punk"
        sammyType_Dinosaur sammyType = "dinosaur"
        sammyType_Retro sammyType = "retro"
        sammyType_Pizza sammyType = "pizza"
        sammyType_Robot sammyType = "robot"
        sammyType_Pony sammyType = "pony"
        sammyType_Bootcamp sammyType = "bootcamp"
        sammyType_XRay sammyType = "xray"
    )
    
    func NewSammyType(s string) (sammyType, error) {
        var t sammyType
        switch strings.ToLower(s) {
        case "sammy":
            t = sammyType_Sammy
        case "punk":
            t = sammyType_Punk
        case "dinosaur":
            t = sammyType_Dinosaur
        case "retro":
            t = sammyType_Retro
        case "pizza":
            t = sammyType_Pizza
        case "robot":
            t = sammyType_Robot
        case "pony":
            t = sammyType_Pony
        case "bootcamp":
            t = sammyType_Bootcamp
        case "xray":
            t = sammyType_XRay
        default:
            return "", fmt.Errorf("%s is an invalid sammyType", s)
        }
        return t, nil
    }
    
    func (s sammyType) String() string {
        return string(s)
    }
    
    func init() {
        flag.StringVar(&sammyname, "name", "", "The name to give your new Sammy.")
        st := flag.String("type", "", "The type to assign to your new Sammy.")
        flag.Parse()
    
        sammyType, err := NewSammyType(*st)
        if err != nil {
            log.Fatal(err)
        }
    }
    
    ...
    


    이제 API에 맞는 유형이 있습니다.
    다음으로 요청을 나타내는 구조체와 거기에 몇 가지 도우미 함수를 간단히 작성했습니다.

    // main.go
    ...
    
    import (
        "fmt"
        "log"
        "flag"
        "strings"
        "bytes"
        "encoding/json"
        "io"
        "io/ioutil"
    )
    
    ...
    
    type (
        sammyType string
    
        sharksRequest struct {
            Name string `json:"name"`
            Type string `json:"name"`
        }
    )
    
    // func NewSharksRequest() is something that I normally write, but there was really no point in this case.
    
    func (req *sharksRequest) setName(name string) *sharksRequest  {
        req.Name = name
    }
    
    func (req *sharksRequest) setType(t sammyType) *sharksRequest {
        req.Type = t.String()
    }
    
    func (req *sharksRequest) marshalJSON() ([]byte, error) {
        return json.Marshal(req)
    } 
    
    ...
    


    이제 API로 보낼 것이 생겼습니다. 따라서 요청을 보내고 출력이 전혀 문서화되지 않았기 때문에 출력이 무엇인지 확인하는 문제였습니다(내가 볼 수 있음).

    // main.go
    ...
    
    func main() {
        // Set some variables to use:
        // Mostly we need the set up the base request struct and
        // the http client.
        var (
            c := http.DefaultClient
            r := &sharksRequest{
                Name: sammyname,
            }
        )
    
        // We can use our helper function to make sure we are
        // getting correct values here.
        r.setType(NewSammyType(sammytype))
    
        // marshal the struct to JSON
        rb, err := r.marshalJSON()
        if err != nil {
            log.Fatal(err)
        }
    
        // Build the new http request
        req, err := http.NewRequest(http.MethodPost, API_URL, bytes.NewBuffer(rb))
        if err != nil {
            log.Fatal(err)
        }
    
        // Set the required headers (from the docs).
        req.Header = map[string][]string{
            "Accept": []string{"application/json"},
            "Content-Type": []string{"application/json"},
        }
    
        // Send the request
        resp, err := c.Do(req)
        if err != nil {
            log.Fatal(err)
        }
    
        // Because we don't know the structure of the output
        // we can just output the response to the terminal.
        defer resp.Body.Close()
    
        out, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            log.Fatal(err)
        }
        log.Println(string(out))
    }
    


    go rungo run main.go -name <myname> -type <mytype>을 통해 코드를 실행하면 이 시점에서 실제로 작동하지만 출력하는 원시 바이트 문자열이 마음에 들지 않습니다. 실제 API 처리기에서 우리는 응답을 구조체에 넣기를 원하므로 이제 일부 출력을 볼 수 있으므로 구조체를 빌드할 수 있습니다.

    나를 위한 출력은 다음과 같이 성공했습니다.
    {"message":"Shark created successfully! Congrats on successfully completing the functions challenge!"}
    이것은 실패에 대한 것입니다.
    {"message":"The name has already been taken.","errors":{"name":["The name has already been taken."]}}
    따라서 이 정보를 염두에 두고 해당 정보를 로드하기 위한 또 다른 구조체와 도우미 메서드를 작성했습니다.

    // main.go
    ...
    
    type (
        sammyType string
    
        sharksRequest struct {
            Name string `json:"name"`
            Type string `json:"name"`
        }
    
        sharksResponse struct {
            // Message seems pretty strait forward and exists 
            // in both the responses, so this is a pretty safe
            // bet
            Message string `json:"message"`
            // Errors on the other hand, I don't have enough
            // info to strongly type the response, but the
            // map type here works for now. I can change it
            // later if there is more I want to do with the
            // errors.
            Errors map[string][]string `json:"errors"`
        }
    
        // func NewSharksResponse() is something again that I would normally write, but it isn't necessary in this example
    
        func (resp *sharksResponse) unmarshalJSON(body io.ReadCloser) error {
            b, err := ioutil.ReadAll(body)
            if err != nil {
                return fmt.Errorf("unable to read http body: %w", err)
            }
    
            return json.Unmarshal(b, resp)
        }
    )
    
    ...
    


    이제 이것이 있으므로 기본 기능에 추가할 수 있습니다.

    // main.go
    ...
    
    func main() {
        // Set some variables to use:
        // Mostly we need the set up the base request struct, 
        // the response struct, and the http client.
        var (
            c := http.DefaultClient
            r := &sharksRequest{
                Name: sammyname,
            }
            R := &sharksResponse{}
        )
    
        // We can use our helper function to make sure we are
        // getting correct values here.
        r.setType(NewSammyType(sammytype))
    
        // marshal the struct to JSON
        rb, err := r.marshalJSON()
        if err != nil {
            log.Fatal(err)
        }
    
        // Build the new http request
        req, err := http.NewRequest(http.MethodPost, API_URL, bytes.NewBuffer(rb))
        if err != nil {
            log.Fatal(err)
        }
    
        // Set the required headers (from the docs).
        req.Header = map[string][]string{
            "Accept": []string{"application/json"},
            "Content-Type": []string{"application/json"},
        }
    
        // Send the request
        resp, err := c.Do(req)
        if err != nil {
            log.Fatal(err)
        }
    
        // We have a structure to unmarshal to now, so lets use
        // it.
        defer resp.Body.Close()
    
        if err := R.unmarshalJSON(resp.Body); err != nil {
            log.Fatal(err)
        }
    
        // Now we can send some better info to the terminal
        if len(R.Errors) > 0 {
            log.Fatalf("An error occurred. Message: %s, Errors: %v", R.Message, R.Errors)
        }
    
        log.Println(R.Message)
    }
    
    


    그리고 그것으로 우리는 성공적인 응용 프로그램을 가지고 있다고 말하고 싶습니다. 전체 코드를 보려면 여기에서 도전 과제에 대한 내 저장소를 확인하십시오: https://github.com/j4ng5y/digitalocean-functions-challenge

    이 리포지토리에도 더 많은 언어를 추가할 예정이므로 README의 지침에 따라 다른 예제를 살펴보세요(제가 만들 때). 다른 언어에 대해서도 비슷한 글을 쓸 것입니다.

    좋은 웹페이지 즐겨찾기