<commit_before>package rs

import (
	"fmt"
	"io"
	"net/http"
	"reflect"
	"strings"

	"github.com/justinsb/gova/assert"
	"github.com/justinsb/gova/log"
)

type HttpErrorObject struct {
	Status  int
	Message string

	Headers map[string]string
}

func HttpError(status int) *HttpErrorObject {
	e := &HttpErrorObject{}
	e.Status = status
	e.Headers = make(map[string]string)
	return e
}

func (self *HttpErrorObject) Error() string {
	return ""
}

func ErrNotFound() *HttpErrorObject {
	err := HttpError(http.StatusNotFound)
	return err
}

type HttpResponse struct {
	Status  int
	Content interface{}
	Headers map[string]string
}

type RestEndpointHandler struct {
	server *RestServer
	path   string

	ptrT    reflect.Type
	structT reflect.Type
}

func newRestEndpoint(server *RestServer, path string, object interface{}) *RestEndpointHandler {
	self := &RestEndpointHandler{}

	self.server = server
	self.path = path

	self.ptrT = reflect.TypeOf(object)
	self.structT = self.ptrT.Elem()

	http.HandleFunc(path, self.httpHandler)

	return self
}

func parseReturn(out []reflect.Value) (reflect.Value, error) {
	var value reflect.Value
	var err error

	if len(out) >= 2 {
		// TODO: Don't assume position 1?
		errValue := out[1]
		if !errValue.IsNil() {
			var ok bool
			log.Debug("Got error value: %v", errValue)
			err, ok = errValue.Interface().(error)
			if !ok {
				err = fmt.Errorf("Unable to cast value to error")
			}
		}
	}

	if err == nil && len(out) > 0 {
		// TODO: Don't assume position 0
		value = out[0]

		if !value.IsValid() {
			value = reflect.ValueOf(nil)
		}
	}

	return value, err
}

func (self *RestEndpointHandler) resolveEndpoint(res http.ResponseWriter, req *http.Request) (*reflect.Value, error) {
	requestUri := req.RequestURI
	suffix := requestUri[len(self.path):]

	if len(suffix) > 0 && suffix[0] == '/' {
		suffix = suffix[1:]
	}

	if len(suffix) > 0 && suffix[len(suffix)-1] == '/' {
		suffix = suffix[:len(suffix)-1]
	}

	var err error

	endpoint := reflect.New(self.structT)

	if suffix != "" {
		pathComponents := strings.Split(suffix, "/")

		//log.Debug("Path components:  %v", pathComponents)

		for _, pathComponent := range pathComponents {

			methodName := "Item" + strings.ToUpper(pathComponent[:1]) + strings.ToLower(pathComponent)[1:]
			method := endpoint.MethodByName(methodName)
			if !method.IsValid() {
				method = endpoint.MethodByName("Item")
				if !method.IsValid() {
					log.Debug("Items method not found (nor %v)", methodName)
					return nil, nil
				}
			}

			injector := self.server.injector
			methodType := method.Type()
			numIn := method.Type().NumIn()
			args := make([]reflect.Value, numIn, numIn)
			for i := 0; i < numIn; i++ {
				argType := methodType.In(i)
				if argType.Kind() == reflect.String {
					args[i] = reflect.ValueOf(pathComponent)
				} else {
					v, err := injector.Get(argType)
					if err != nil {
						log.Warn("Error injecting argument of type: %v", argType, err)
						return nil, err
					}
					args[i] = reflect.ValueOf(v)
				}
			}

			out := method.Call(args)
			endpoint, err = parseReturn(out)
			if err != nil {
				return nil, err
			}
			if endpoint.IsNil() {
				return nil, nil
			}
			if endpoint.Kind() == reflect.Interface {
				endpoint = endpoint.Elem()
			}
		}
	}

	return &endpoint, nil
}

func (self *RestEndpointHandler) makeResponse(val reflect.Value) (*HttpResponse, error) {
	var ok bool
	var response *HttpResponse

	if val.IsNil() {
		response = &HttpResponse{Status: http.StatusNoContent}
		response.Content = nil
	} else {
		response, ok = val.Interface().(*HttpResponse)
		if !ok {
			response = &HttpResponse{Status: http.StatusOK}
			response.Content = val.Interface()
		}
	}

	if response == nil {
		log.Warn("Unable to build response for %v", val)
		return nil, fmt.Errorf("Unable to build response")
	}

	return response, nil
}

func getMediaType(req *http.Request) (*MediaType, error) {
	v := req.Header.Get("Content-Type")
	if v == "" {
		return nil, nil
	}

	mediaType, err := ParseMediaType(v)
	if err != nil {
		log.Warn("Error parsing mime type: %v", v, err)
		return nil, err
	}

	return mediaType, nil
}

func (self *RestEndpointHandler) buildArg(res http.ResponseWriter, req *http.Request, t reflect.Type) (interface{}, error) {
	v, err := self.server.injector.Get(t)
	if err == nil && v != nil {
		return v, nil
	}

	// TODO: Fail if two args...

	// TODO: Only if has content?
	mediaType, err := getMediaType(req)
	if err != nil {
		return nil, err
	}

	if mediaType == nil {
		// Go does have a function to guess the media type, but that seems risky
		// Instead, use a fixed default
		mediaType = self.server.defaultMediaType
	}

	v, err = self.server.readMessageBody(t, req, mediaType)
	if err != nil {
		if err == io.EOF {
			log.Debug("Error reading message body (EOF)")
		} else {
			log.Debug("Error reading message body", err)
		}
		err = HttpError(http.StatusBadRequest)
		return nil, err
	}

	if v == nil && err == nil {
		err = HttpError(http.StatusUnsupportedMediaType)
		return nil, err
	}

	if v != nil {
		assert.Equal(reflect.TypeOf(v), t)
		return v, nil
	}

	log.Warn("Unable to bind parameter: %v", t)
	return nil, fmt.Errorf("Unable to bind parameter: %v", t)
}

func (self *RestEndpointHandler) buildArgs(res http.ResponseWriter, req *http.Request, method *reflect.Value) ([]reflect.Value, error) {
	numIn := method.Type().NumIn()
	args := make([]reflect.Value, numIn)
	methodType := method.Type()
	for i := 0; i < methodType.NumIn(); i++ {
		val, err := self.buildArg(res, req, methodType.In(i))
		if err != nil {
			return nil, err
		}
		if val != nil {
			args[i] = reflect.ValueOf(val)
		}
	}

	return args, nil
}

func (self *RestEndpointHandler) httpHandler(res http.ResponseWriter, req *http.Request) {
	requestUrl := req.URL
	requestMethod := req.Method

	log.Debug("%v %v", requestMethod, requestUrl)

	endpoint, err := self.resolveEndpoint(res, req)

	if endpoint == nil {
		err = HttpError(http.StatusNotFound)
	}

	var method reflect.Value

	if err == nil {
		httpMethod := req.Method
		methodName := "Http" + httpMethod[0:1] + strings.ToLower(httpMethod[1:])

		method = endpoint.MethodByName(methodName)
		if !method.IsValid() {
			log.Debug("Method not found: %v on %v", methodName, endpoint.Type())

			err = HttpError(http.StatusNotFound)
		}
	}

	var args []reflect.Value
	if err == nil {
		args, err = self.buildArgs(res, req, &method)
	}

	var val reflect.Value

	if err == nil {
		var out []reflect.Value
		out = method.Call(args)
		//		fmt.Fprintf(w, "Returned %v", out)

		val, err = parseReturn(out)
	}

	if err == nil {
		if val.IsNil() {
			err = HttpError(http.StatusNotFound)
		}
	}

	var response *HttpResponse
	var mbw MessageBodyWriter

	if err == nil {
		response, err = self.makeResponse(val)
	}
	if err == nil {
		assert.That(response != nil)

		if response.Headers == nil {
			response.Headers = make(map[string]string)
		}

		if response.Content == nil {
			mbw = &NoResponseMessageBodyWriter{}
		} else {
			contentType := response.Headers["content-type"]
			if contentType == "" {
				contentType = "application/json; charset=utf-8"
				response.Headers["content-type"] = contentType
			}

			var mediaType *MediaType
			if contentType != "" {
				mediaType, err = ParseMediaType(contentType)
			}

			if err == nil {
				assert.That(mediaType != nil)

				mbw = self.server.findMessageBodyWriter(response.Content, req, mediaType)
				if mbw == nil {
					log.Warn("Unable to find media type: %v", contentType)
					err = HttpError(http.StatusUnsupportedMediaType)
				}
			}
		}
	}

	if err == nil {
		assert.That(response != nil)
		assert.That(mbw != nil)

		log.Info("%v %v %v", response.Status, requestMethod, requestUrl)

		if response.Headers != nil {
			for name, value := range response.Headers {
				res.Header().Set(name, value)
			}
		}

		res.WriteHeader(response.Status)

		mbw.Write(response.Content, reflect.TypeOf(response.Content), req, res)
	} else {
		httpError, ok := err.(*HttpErrorObject)
		if !ok {
			log.Warn("Internal error serving request", err)
			httpError = HttpError(http.StatusInternalServerError)
		}

		status := httpError.Status
		message := httpError.Message
		if message == "" {
			message = http.StatusText(status)
			if message == "" {
				message = "Error"
			}
		}

		for k, v := range httpError.Headers {
			res.Header().Add(k, v)
		}

		log.Info("%v %v %v", status, requestMethod, requestUrl)

		http.Error(res, message, status)
	}
}
<commit_msg>Support arg-injection of http.Request<commit_after>package rs

import (
	"fmt"
	"io"
	"net/http"
	"reflect"
	"strings"

	"github.com/justinsb/gova/assert"
	"github.com/justinsb/gova/log"
)

type HttpErrorObject struct {
	Status  int
	Message string

	Headers map[string]string
}

func HttpError(status int) *HttpErrorObject {
	e := &HttpErrorObject{}
	e.Status = status
	e.Headers = make(map[string]string)
	return e
}

func (self *HttpErrorObject) Error() string {
	return ""
}

func ErrNotFound() *HttpErrorObject {
	err := HttpError(http.StatusNotFound)
	return err
}

type HttpResponse struct {
	Status  int
	Content interface{}
	Headers map[string]string
}

type RestEndpointHandler struct {
	server *RestServer
	path   string

	ptrT    reflect.Type
	structT reflect.Type
}

func newRestEndpoint(server *RestServer, path string, object interface{}) *RestEndpointHandler {
	self := &RestEndpointHandler{}

	self.server = server
	self.path = path

	self.ptrT = reflect.TypeOf(object)
	self.structT = self.ptrT.Elem()

	http.HandleFunc(path, self.httpHandler)

	return self
}

func parseReturn(out []reflect.Value) (reflect.Value, error) {
	var value reflect.Value
	var err error

	if len(out) >= 2 {
		// TODO: Don't assume position 1?
		errValue := out[1]
		if !errValue.IsNil() {
			var ok bool
			log.Debug("Got error value: %v", errValue)
			err, ok = errValue.Interface().(error)
			if !ok {
				err = fmt.Errorf("Unable to cast value to error")
			}
		}
	}

	if err == nil && len(out) > 0 {
		// TODO: Don't assume position 0
		value = out[0]

		if !value.IsValid() {
			value = reflect.ValueOf(nil)
		}
	}

	return value, err
}

func (self *RestEndpointHandler) resolveEndpoint(res http.ResponseWriter, req *http.Request) (*reflect.Value, error) {
	requestUri := req.RequestURI
	suffix := requestUri[len(self.path):]

	if len(suffix) > 0 && suffix[0] == '/' {
		suffix = suffix[1:]
	}

	if len(suffix) > 0 && suffix[len(suffix)-1] == '/' {
		suffix = suffix[:len(suffix)-1]
	}

	var err error

	endpoint := reflect.New(self.structT)

	if suffix != "" {
		pathComponents := strings.Split(suffix, "/")

		//log.Debug("Path components:  %v", pathComponents)

		for _, pathComponent := range pathComponents {

			methodName := "Item" + strings.ToUpper(pathComponent[:1]) + strings.ToLower(pathComponent)[1:]
			method := endpoint.MethodByName(methodName)
			if !method.IsValid() {
				method = endpoint.MethodByName("Item")
				if !method.IsValid() {
					log.Debug("Items method not found (nor %v)", methodName)
					return nil, nil
				}
			}

			injector := self.server.injector
			methodType := method.Type()
			numIn := method.Type().NumIn()
			args := make([]reflect.Value, numIn, numIn)
			for i := 0; i < numIn; i++ {
				argType := methodType.In(i)
				if argType.Kind() == reflect.String {
					args[i] = reflect.ValueOf(pathComponent)
				} else if argType == reflect.TypeOf(req) {
					args[i] = reflect.ValueOf(req)
				} else {
					v, err := injector.Get(argType)
					if err != nil {
						log.Warn("Error injecting argument of type: %v", argType, err)
						return nil, err
					}
					args[i] = reflect.ValueOf(v)
				}
			}

			out := method.Call(args)
			endpoint, err = parseReturn(out)
			if err != nil {
				return nil, err
			}
			if endpoint.IsNil() {
				return nil, nil
			}
			if endpoint.Kind() == reflect.Interface {
				endpoint = endpoint.Elem()
			}
		}
	}

	return &endpoint, nil
}

func (self *RestEndpointHandler) makeResponse(val reflect.Value) (*HttpResponse, error) {
	var ok bool
	var response *HttpResponse

	if val.IsNil() {
		response = &HttpResponse{Status: http.StatusNoContent}
		response.Content = nil
	} else {
		response, ok = val.Interface().(*HttpResponse)
		if !ok {
			response = &HttpResponse{Status: http.StatusOK}
			response.Content = val.Interface()
		}
	}

	if response == nil {
		log.Warn("Unable to build response for %v", val)
		return nil, fmt.Errorf("Unable to build response")
	}

	return response, nil
}

func getMediaType(req *http.Request) (*MediaType, error) {
	v := req.Header.Get("Content-Type")
	if v == "" {
		return nil, nil
	}

	mediaType, err := ParseMediaType(v)
	if err != nil {
		log.Warn("Error parsing mime type: %v", v, err)
		return nil, err
	}

	return mediaType, nil
}

func (self *RestEndpointHandler) buildArg(res http.ResponseWriter, req *http.Request, t reflect.Type) (interface{}, error) {
	v, err := self.server.injector.Get(t)
	if err == nil && v != nil {
		return v, nil
	}

	// TODO: Fail if two args...

	// TODO: Only if has content?
	mediaType, err := getMediaType(req)
	if err != nil {
		return nil, err
	}

	if mediaType == nil {
		// Go does have a function to guess the media type, but that seems risky
		// Instead, use a fixed default
		mediaType = self.server.defaultMediaType
	}

	v, err = self.server.readMessageBody(t, req, mediaType)
	if err != nil {
		if err == io.EOF {
			log.Debug("Error reading message body (EOF)")
		} else {
			log.Debug("Error reading message body", err)
		}
		err = HttpError(http.StatusBadRequest)
		return nil, err
	}

	if v == nil && err == nil {
		err = HttpError(http.StatusUnsupportedMediaType)
		return nil, err
	}

	if v != nil {
		assert.Equal(reflect.TypeOf(v), t)
		return v, nil
	}

	log.Warn("Unable to bind parameter: %v", t)
	return nil, fmt.Errorf("Unable to bind parameter: %v", t)
}

func (self *RestEndpointHandler) buildArgs(res http.ResponseWriter, req *http.Request, method *reflect.Value) ([]reflect.Value, error) {
	numIn := method.Type().NumIn()
	args := make([]reflect.Value, numIn)
	methodType := method.Type()
	for i := 0; i < methodType.NumIn(); i++ {
		val, err := self.buildArg(res, req, methodType.In(i))
		if err != nil {
			return nil, err
		}
		if val != nil {
			args[i] = reflect.ValueOf(val)
		}
	}

	return args, nil
}

func (self *RestEndpointHandler) httpHandler(res http.ResponseWriter, req *http.Request) {
	requestUrl := req.URL
	requestMethod := req.Method

	log.Debug("%v %v", requestMethod, requestUrl)

	endpoint, err := self.resolveEndpoint(res, req)

	if endpoint == nil {
		err = HttpError(http.StatusNotFound)
	}

	var method reflect.Value

	if err == nil {
		httpMethod := req.Method
		methodName := "Http" + httpMethod[0:1] + strings.ToLower(httpMethod[1:])

		method = endpoint.MethodByName(methodName)
		if !method.IsValid() {
			log.Debug("Method not found: %v on %v", methodName, endpoint.Type())

			err = HttpError(http.StatusNotFound)
		}
	}

	var args []reflect.Value
	if err == nil {
		args, err = self.buildArgs(res, req, &method)
	}

	var val reflect.Value

	if err == nil {
		var out []reflect.Value
		out = method.Call(args)
		//		fmt.Fprintf(w, "Returned %v", out)

		val, err = parseReturn(out)
	}

	if err == nil {
		if val.IsNil() {
			err = HttpError(http.StatusNotFound)
		}
	}

	var response *HttpResponse
	var mbw MessageBodyWriter

	if err == nil {
		response, err = self.makeResponse(val)
	}
	if err == nil {
		assert.That(response != nil)

		if response.Headers == nil {
			response.Headers = make(map[string]string)
		}

		if response.Content == nil {
			mbw = &NoResponseMessageBodyWriter{}
		} else {
			contentType := response.Headers["content-type"]
			if contentType == "" {
				contentType = "application/json; charset=utf-8"
				response.Headers["content-type"] = contentType
			}

			var mediaType *MediaType
			if contentType != "" {
				mediaType, err = ParseMediaType(contentType)
			}

			if err == nil {
				assert.That(mediaType != nil)

				mbw = self.server.findMessageBodyWriter(response.Content, req, mediaType)
				if mbw == nil {
					log.Warn("Unable to find media type: %v", contentType)
					err = HttpError(http.StatusUnsupportedMediaType)
				}
			}
		}
	}

	if err == nil {
		assert.That(response != nil)
		assert.That(mbw != nil)

		log.Info("%v %v %v", response.Status, requestMethod, requestUrl)

		if response.Headers != nil {
			for name, value := range response.Headers {
				res.Header().Set(name, value)
			}
		}

		res.WriteHeader(response.Status)

		mbw.Write(response.Content, reflect.TypeOf(response.Content), req, res)
	} else {
		httpError, ok := err.(*HttpErrorObject)
		if !ok {
			log.Warn("Internal error serving request", err)
			httpError = HttpError(http.StatusInternalServerError)
		}

		status := httpError.Status
		message := httpError.Message
		if message == "" {
			message = http.StatusText(status)
			if message == "" {
				message = "Error"
			}
		}

		for k, v := range httpError.Headers {
			res.Header().Add(k, v)
		}

		log.Info("%v %v %v", status, requestMethod, requestUrl)

		http.Error(res, message, status)
	}
}
<|endoftext|>


<commit_before>package rs

import (
	"fmt"
	"io"
	"net/http"
	"reflect"
	"strings"

	"github.com/justinsb/gova/assert"
	"github.com/justinsb/gova/log"
)

type HttpErrorObject struct {
	Status  int
	Message string
}

func HttpError(status int) *HttpErrorObject {
	e := &HttpErrorObject{}
	e.Status = status
	return e
}

func (self *HttpErrorObject) Error() string {
	return ""
}

func ErrNotFound() *HttpErrorObject {
	err := HttpError(http.StatusNotFound)
	return err
}

type HttpResponse struct {
	Status  int
	Content interface{}
	Headers map[string]string
}

type RestEndpointHandler struct {
	server *RestServer
	path   string

	ptrT    reflect.Type
	structT reflect.Type
}

func newRestEndpoint(server *RestServer, path string, object interface{}) *RestEndpointHandler {
	self := &RestEndpointHandler{}

	self.server = server
	self.path = path

	self.ptrT = reflect.TypeOf(object)
	self.structT = self.ptrT.Elem()

	http.HandleFunc(path, self.httpHandler)

	return self
}

func parseReturn(out []reflect.Value) (reflect.Value, error) {
	var value reflect.Value
	var err error

	if len(out) >= 2 {
		// TODO: Don't assume position 1?
		errValue := out[1]
		if !errValue.IsNil() {
			var ok bool
			log.Debug("Got error value: %v", errValue)
			err, ok = errValue.Interface().(error)
			if !ok {
				err = fmt.Errorf("Unable to cast value to error")
			}
		}
	}

	if err == nil && len(out) > 0 {
		// TODO: Don't assume position 0
		value = out[0]

		if !value.IsValid() {
			value = reflect.ValueOf(nil)
		}
	}

	return value, err
}

func (self *RestEndpointHandler) resolveEndpoint(res http.ResponseWriter, req *http.Request) (*reflect.Value, error) {
	requestUri := req.RequestURI
	suffix := requestUri[len(self.path):]

	if len(suffix) > 0 && suffix[0] == '/' {
		suffix = suffix[1:]
	}

	if len(suffix) > 0 && suffix[len(suffix)-1] == '/' {
		suffix = suffix[:len(suffix)-1]
	}

	var err error

	endpoint := reflect.New(self.structT)

	if suffix != "" {
		pathComponents := strings.Split(suffix, "/")

		//log.Debug("Path components:  %v", pathComponents)

		for _, pathComponent := range pathComponents {

			methodName := "Item" + strings.ToUpper(pathComponent[:1]) + strings.ToLower(pathComponent)[1:]
			method := endpoint.MethodByName(methodName)
			if !method.IsValid() {
				method = endpoint.MethodByName("Item")
				if !method.IsValid() {
					log.Debug("Items method not found (nor %v)", methodName)
					return nil, nil
				}
			}

			injector := self.server.injector
			methodType := method.Type()
			numIn := method.Type().NumIn()
			args := make([]reflect.Value, numIn, numIn)
			for i := 0; i < numIn; i++ {
				argType := methodType.In(i)
				if argType.Kind() == reflect.String {
					args[i] = reflect.ValueOf(pathComponent)
				} else {
					v, err := injector.Get(argType)
					if err != nil {
						log.Warn("Error injecting argument of type: %v", argType, err)
						return nil, err
					}
					args[i] = reflect.ValueOf(v)
				}
			}

			out := method.Call(args)
			endpoint, err = parseReturn(out)
			if err != nil {
				return nil, err
			}
			if endpoint.IsNil() {
				return nil, nil
			}
			if endpoint.Kind() == reflect.Interface {
				endpoint = endpoint.Elem()
			}
		}
	}

	return &endpoint, nil
}

func (self *RestEndpointHandler) makeResponse(val reflect.Value) (*HttpResponse, error) {
	var ok bool
	var response *HttpResponse

	if val.IsNil() {
		response = &HttpResponse{Status: http.StatusNoContent}
		response.Content = nil
	} else {
		response, ok = val.Interface().(*HttpResponse)
		if !ok {
			response = &HttpResponse{Status: http.StatusOK}
			response.Content = val.Interface()
		}
	}

	if response == nil {
		log.Warn("Unable to build response for %v", val)
		return nil, fmt.Errorf("Unable to build response")
	}

	return response, nil
}

func getMediaType(req *http.Request) (*MediaType, error) {
	v := req.Header.Get("Content-Type")
	if v == "" {
		return nil, nil
	}

	mediaType, err := ParseMediaType(v)
	if err != nil {
		log.Warn("Error parsing mime type: %v", v, err)
		return nil, err
	}

	return mediaType, nil
}

func (self *RestEndpointHandler) buildArg(res http.ResponseWriter, req *http.Request, t reflect.Type) (interface{}, error) {
	v, err := self.server.injector.Get(t)
	if err == nil && v != nil {
		return v, nil
	}

	// TODO: Fail if two args...

	// TODO: Only if has content?
	mediaType, err := getMediaType(req)
	if err != nil {
		return nil, err
	}

	if mediaType == nil {
		// Go does have a function to guess the media type, but that seems risky
		// Instead, use a fixed default
		mediaType = self.server.defaultMediaType
	}

	v, err = self.server.readMessageBody(t, req, mediaType)
	if err != nil {
		if err == io.EOF {
			log.Debug("Error reading message body (EOF)")
		} else {
			log.Debug("Error reading message body", err)
		}
		err = HttpError(http.StatusBadRequest)
		return nil, err
	}

	if v == nil && err == nil {
		err = HttpError(http.StatusUnsupportedMediaType)
		return nil, err
	}

	if v != nil {
		assert.Equal(reflect.TypeOf(v), t)
		return v, nil
	}

	log.Warn("Unable to bind parameter: %v", t)
	return nil, fmt.Errorf("Unable to bind parameter: %v", t)
}

func (self *RestEndpointHandler) buildArgs(res http.ResponseWriter, req *http.Request, method *reflect.Value) ([]reflect.Value, error) {
	numIn := method.Type().NumIn()
	args := make([]reflect.Value, numIn)
	methodType := method.Type()
	for i := 0; i < methodType.NumIn(); i++ {
		val, err := self.buildArg(res, req, methodType.In(i))
		if err != nil {
			return nil, err
		}
		if val != nil {
			args[i] = reflect.ValueOf(val)
		}
	}

	return args, nil
}

func (self *RestEndpointHandler) httpHandler(res http.ResponseWriter, req *http.Request) {
	requestUrl := req.URL
	requestMethod := req.Method

	log.Debug("%v %v", requestMethod, requestUrl)

	endpoint, err := self.resolveEndpoint(res, req)

	if endpoint == nil {
		err = HttpError(http.StatusNotFound)
	}

	var method reflect.Value

	if err == nil {
		httpMethod := req.Method
		methodName := "Http" + httpMethod[0:1] + strings.ToLower(httpMethod[1:])

		method = endpoint.MethodByName(methodName)
		if !method.IsValid() {
			log.Debug("Method not found: %v on %v", methodName, endpoint.Type())

			err = HttpError(http.StatusNotFound)
		}
	}

	var args []reflect.Value
	if err == nil {
		args, err = self.buildArgs(res, req, &method)
	}

	var val reflect.Value

	if err == nil {
		var out []reflect.Value
		out = method.Call(args)
		//		fmt.Fprintf(w, "Returned %v", out)

		val, err = parseReturn(out)
	}

	if err == nil {
		if val.IsNil() {
			err = HttpError(http.StatusNotFound)
		}
	}

	var response *HttpResponse
	var mbw MessageBodyWriter

	if err == nil {
		response, err = self.makeResponse(val)
	}
	if err == nil {
		assert.That(response != nil)

		if response.Headers == nil {
			response.Headers = make(map[string]string)
		}

		if response.Content == nil {
			mbw = &NoResponseMessageBodyWriter{}
		} else {
			contentType := response.Headers["content-type"]
			if contentType == "" {
				contentType = "application/json; charset=utf-8"
				response.Headers["content-type"] = contentType
			}

			var mediaType *MediaType
			if contentType != "" {
				mediaType, err = ParseMediaType(contentType)
			}

			if err == nil {
				assert.That(mediaType != nil)

				mbw = self.server.findMessageBodyWriter(response.Content, req, mediaType)
				if mbw == nil {
					log.Warn("Unable to find media type: %v", contentType)
					err = HttpError(http.StatusUnsupportedMediaType)
				}
			}
		}
	}

	if err == nil {
		assert.That(response != nil)
		assert.That(mbw != nil)

		log.Info("%v %v %v", response.Status, requestMethod, requestUrl)

		if response.Headers != nil {
			for name, value := range response.Headers {
				res.Header().Set(name, value)
			}
		}

		res.WriteHeader(response.Status)

		mbw.Write(response.Content, reflect.TypeOf(response.Content), req, res)
	} else {
		httpError, ok := err.(*HttpErrorObject)
		if !ok {
			log.Warn("Internal error serving request", err)
			httpError = HttpError(http.StatusInternalServerError)
		}

		status := httpError.Status
		message := httpError.Message
		if message == "" {
			message = http.StatusText(status)
			if message == "" {
				message = "Error"
			}
		}

		log.Info("%v %v %v", status, requestMethod, requestUrl)

		http.Error(res, message, status)
	}
}
<commit_msg>Support for headers in errors<commit_after>package rs

import (
	"fmt"
	"io"
	"net/http"
	"reflect"
	"strings"

	"github.com/justinsb/gova/assert"
	"github.com/justinsb/gova/log"
)

type HttpErrorObject struct {
	Status  int
	Message string

	Headers map[string]string
}

func HttpError(status int) *HttpErrorObject {
	e := &HttpErrorObject{}
	e.Status = status
	e.Headers = make(map[string]string)
	return e
}

func (self *HttpErrorObject) Error() string {
	return ""
}

func ErrNotFound() *HttpErrorObject {
	err := HttpError(http.StatusNotFound)
	return err
}

type HttpResponse struct {
	Status  int
	Content interface{}
	Headers map[string]string
}

type RestEndpointHandler struct {
	server *RestServer
	path   string

	ptrT    reflect.Type
	structT reflect.Type
}

func newRestEndpoint(server *RestServer, path string, object interface{}) *RestEndpointHandler {
	self := &RestEndpointHandler{}

	self.server = server
	self.path = path

	self.ptrT = reflect.TypeOf(object)
	self.structT = self.ptrT.Elem()

	http.HandleFunc(path, self.httpHandler)

	return self
}

func parseReturn(out []reflect.Value) (reflect.Value, error) {
	var value reflect.Value
	var err error

	if len(out) >= 2 {
		// TODO: Don't assume position 1?
		errValue := out[1]
		if !errValue.IsNil() {
			var ok bool
			log.Debug("Got error value: %v", errValue)
			err, ok = errValue.Interface().(error)
			if !ok {
				err = fmt.Errorf("Unable to cast value to error")
			}
		}
	}

	if err == nil && len(out) > 0 {
		// TODO: Don't assume position 0
		value = out[0]

		if !value.IsValid() {
			value = reflect.ValueOf(nil)
		}
	}

	return value, err
}

func (self *RestEndpointHandler) resolveEndpoint(res http.ResponseWriter, req *http.Request) (*reflect.Value, error) {
	requestUri := req.RequestURI
	suffix := requestUri[len(self.path):]

	if len(suffix) > 0 && suffix[0] == '/' {
		suffix = suffix[1:]
	}

	if len(suffix) > 0 && suffix[len(suffix)-1] == '/' {
		suffix = suffix[:len(suffix)-1]
	}

	var err error

	endpoint := reflect.New(self.structT)

	if suffix != "" {
		pathComponents := strings.Split(suffix, "/")

		//log.Debug("Path components:  %v", pathComponents)

		for _, pathComponent := range pathComponents {

			methodName := "Item" + strings.ToUpper(pathComponent[:1]) + strings.ToLower(pathComponent)[1:]
			method := endpoint.MethodByName(methodName)
			if !method.IsValid() {
				method = endpoint.MethodByName("Item")
				if !method.IsValid() {
					log.Debug("Items method not found (nor %v)", methodName)
					return nil, nil
				}
			}

			injector := self.server.injector
			methodType := method.Type()
			numIn := method.Type().NumIn()
			args := make([]reflect.Value, numIn, numIn)
			for i := 0; i < numIn; i++ {
				argType := methodType.In(i)
				if argType.Kind() == reflect.String {
					args[i] = reflect.ValueOf(pathComponent)
				} else {
					v, err := injector.Get(argType)
					if err != nil {
						log.Warn("Error injecting argument of type: %v", argType, err)
						return nil, err
					}
					args[i] = reflect.ValueOf(v)
				}
			}

			out := method.Call(args)
			endpoint, err = parseReturn(out)
			if err != nil {
				return nil, err
			}
			if endpoint.IsNil() {
				return nil, nil
			}
			if endpoint.Kind() == reflect.Interface {
				endpoint = endpoint.Elem()
			}
		}
	}

	return &endpoint, nil
}

func (self *RestEndpointHandler) makeResponse(val reflect.Value) (*HttpResponse, error) {
	var ok bool
	var response *HttpResponse

	if val.IsNil() {
		response = &HttpResponse{Status: http.StatusNoContent}
		response.Content = nil
	} else {
		response, ok = val.Interface().(*HttpResponse)
		if !ok {
			response = &HttpResponse{Status: http.StatusOK}
			response.Content = val.Interface()
		}
	}

	if response == nil {
		log.Warn("Unable to build response for %v", val)
		return nil, fmt.Errorf("Unable to build response")
	}

	return response, nil
}

func getMediaType(req *http.Request) (*MediaType, error) {
	v := req.Header.Get("Content-Type")
	if v == "" {
		return nil, nil
	}

	mediaType, err := ParseMediaType(v)
	if err != nil {
		log.Warn("Error parsing mime type: %v", v, err)
		return nil, err
	}

	return mediaType, nil
}

func (self *RestEndpointHandler) buildArg(res http.ResponseWriter, req *http.Request, t reflect.Type) (interface{}, error) {
	v, err := self.server.injector.Get(t)
	if err == nil && v != nil {
		return v, nil
	}

	// TODO: Fail if two args...

	// TODO: Only if has content?
	mediaType, err := getMediaType(req)
	if err != nil {
		return nil, err
	}

	if mediaType == nil {
		// Go does have a function to guess the media type, but that seems risky
		// Instead, use a fixed default
		mediaType = self.server.defaultMediaType
	}

	v, err = self.server.readMessageBody(t, req, mediaType)
	if err != nil {
		if err == io.EOF {
			log.Debug("Error reading message body (EOF)")
		} else {
			log.Debug("Error reading message body", err)
		}
		err = HttpError(http.StatusBadRequest)
		return nil, err
	}

	if v == nil && err == nil {
		err = HttpError(http.StatusUnsupportedMediaType)
		return nil, err
	}

	if v != nil {
		assert.Equal(reflect.TypeOf(v), t)
		return v, nil
	}

	log.Warn("Unable to bind parameter: %v", t)
	return nil, fmt.Errorf("Unable to bind parameter: %v", t)
}

func (self *RestEndpointHandler) buildArgs(res http.ResponseWriter, req *http.Request, method *reflect.Value) ([]reflect.Value, error) {
	numIn := method.Type().NumIn()
	args := make([]reflect.Value, numIn)
	methodType := method.Type()
	for i := 0; i < methodType.NumIn(); i++ {
		val, err := self.buildArg(res, req, methodType.In(i))
		if err != nil {
			return nil, err
		}
		if val != nil {
			args[i] = reflect.ValueOf(val)
		}
	}

	return args, nil
}

func (self *RestEndpointHandler) httpHandler(res http.ResponseWriter, req *http.Request) {
	requestUrl := req.URL
	requestMethod := req.Method

	log.Debug("%v %v", requestMethod, requestUrl)

	endpoint, err := self.resolveEndpoint(res, req)

	if endpoint == nil {
		err = HttpError(http.StatusNotFound)
	}

	var method reflect.Value

	if err == nil {
		httpMethod := req.Method
		methodName := "Http" + httpMethod[0:1] + strings.ToLower(httpMethod[1:])

		method = endpoint.MethodByName(methodName)
		if !method.IsValid() {
			log.Debug("Method not found: %v on %v", methodName, endpoint.Type())

			err = HttpError(http.StatusNotFound)
		}
	}

	var args []reflect.Value
	if err == nil {
		args, err = self.buildArgs(res, req, &method)
	}

	var val reflect.Value

	if err == nil {
		var out []reflect.Value
		out = method.Call(args)
		//		fmt.Fprintf(w, "Returned %v", out)

		val, err = parseReturn(out)
	}

	if err == nil {
		if val.IsNil() {
			err = HttpError(http.StatusNotFound)
		}
	}

	var response *HttpResponse
	var mbw MessageBodyWriter

	if err == nil {
		response, err = self.makeResponse(val)
	}
	if err == nil {
		assert.That(response != nil)

		if response.Headers == nil {
			response.Headers = make(map[string]string)
		}

		if response.Content == nil {
			mbw = &NoResponseMessageBodyWriter{}
		} else {
			contentType := response.Headers["content-type"]
			if contentType == "" {
				contentType = "application/json; charset=utf-8"
				response.Headers["content-type"] = contentType
			}

			var mediaType *MediaType
			if contentType != "" {
				mediaType, err = ParseMediaType(contentType)
			}

			if err == nil {
				assert.That(mediaType != nil)

				mbw = self.server.findMessageBodyWriter(response.Content, req, mediaType)
				if mbw == nil {
					log.Warn("Unable to find media type: %v", contentType)
					err = HttpError(http.StatusUnsupportedMediaType)
				}
			}
		}
	}

	if err == nil {
		assert.That(response != nil)
		assert.That(mbw != nil)

		log.Info("%v %v %v", response.Status, requestMethod, requestUrl)

		if response.Headers != nil {
			for name, value := range response.Headers {
				res.Header().Set(name, value)
			}
		}

		res.WriteHeader(response.Status)

		mbw.Write(response.Content, reflect.TypeOf(response.Content), req, res)
	} else {
		httpError, ok := err.(*HttpErrorObject)
		if !ok {
			log.Warn("Internal error serving request", err)
			httpError = HttpError(http.StatusInternalServerError)
		}

		status := httpError.Status
		message := httpError.Message
		if message == "" {
			message = http.StatusText(status)
			if message == "" {
				message = "Error"
			}
		}

		for k, v := range httpError.Headers {
			res.Header().Add(k, v)
		}

		log.Info("%v %v %v", status, requestMethod, requestUrl)

		http.Error(res, message, status)
	}
}
<|endoftext|>


<commit_before>package gotwilio

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"
)

// MediaRegion is the locations of Twilio's
// TURN servers
type MediaRegion string

const (
	Australia   MediaRegion = "au1"
	Brazil      MediaRegion = "br1"
	Germany     MediaRegion = "de1"
	Ireland     MediaRegion = "ie1"
	India       MediaRegion = "in1"
	Japan       MediaRegion = "jp1"
	Singapore   MediaRegion = "sg1"
	USEastCoast MediaRegion = "us1"
	USWestCoast MediaRegion = "us2"
)

// VideoStatus is the status of a video room
type VideoStatus string

const (
	InProgress VideoStatus = "in-progress"
	Failed     VideoStatus = "failed"
	Completed  VideoStatus = "completed"
)

// VideoRoomType is how the participants connect
// to each other, whether peer-to-peer of routed
// through a TURN server.
type VideoRoomType string

const (
	PeerToPeer VideoRoomType = "peer-to-peer"
	Group      VideoRoomType = "group"
)

// VideoCodecs are the supported codecs when
// publishing a track to the room.
type VideoCodecs string

const (
	VP8  VideoCodecs = "VP8"
	H264 VideoCodecs = "H264"
)

// VideoResponse is returned after a room is created
type VideoResponse struct {
	AccountSid                  string        `json:"account_sid"`
	DateCreated                 time.Time     `json:"date_created"`
	DateUpdated                 time.Time     `json:"date_updated"`
	Duration                    time.Duration `json:"duration"`
	EnableTurn                  bool          `json:"enable_turn"`
	EndTime                     time.Time     `json:"end_time"`
	MaxParticipants             int64         `json:"max_participants"`
	MediaRegion                 MediaRegion   `json:"media_region"`
	RecordParticipantsOnConnect bool          `json:"record_participants_on_connect"`
	Sid                         string        `json:"sid"`
	Status                      VideoStatus   `json:"status"`
	StatusCallback              string        `json:"status_callback"`
	StatusCallbackMethod        string        `json:"status_callback_method"`
	Type                        VideoRoomType `json:"type"`
	UniqueName                  string        `json:"unique_name"`
	URL                         string        `json:"url"`
}

type createRoomOptions struct {
	EnableTurn                  bool          `json:"EnableTurn"`
	MaxParticipants             int64         `json:"MaxParticipants"`
	MediaRegion                 MediaRegion   `json:"MediaRegion"`
	RecordParticipantsOnConnect bool          `json:"RecordParticipantsOnConnect"`
	StatusCallback              string        `json:"StatusCallback"`
	StatusCallbackMethod        string        `json:"StatusCallbackMethod"`
	Type                        VideoRoomType `json:"Type"`
	UniqueName                  string        `json:"UniqueName"`
	VideoCodecs                 []VideoCodecs `json:"VideoCodecs"`
}

// DefaultVideoRoomOptions are the default options
// for creating a room.
var DefaultVideoRoomOptions = &createRoomOptions{
	EnableTurn:                  true,
	MaxParticipants:             10,
	MediaRegion:                 USEastCoast,
	RecordParticipantsOnConnect: false,
	StatusCallback:              "",
	StatusCallbackMethod:        http.MethodPost,
	Type:                        Group,
	UniqueName:                  "",
	VideoCodecs:                 []VideoCodecs{H264},
}

// CreateVideoRoom creates a video communication session
// for participants to connect to.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) CreateVideoRoom(options *createRoomOptions) (videoResponse *VideoResponse, exception *Exception, err error) {
	twilioUrl := twilio.VideoUrl + "/v1/Rooms"
	formValues := createRoomOptionsToFormValues(options)

	res, err := twilio.post(formValues, twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusCreated {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(VideoResponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

// GetVideoRoom retrievs a single video session
// by name or by Sid.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) GetVideoRoom(nameOrSid string) (videoResponse *VideoResponse, exception *Exception, err error) {
	twilioUrl := twilio.VideoUrl + "/v1/Rooms/" + nameOrSid

	res, err := twilio.get(twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusOK {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(VideoResponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

// EndVideoRoom stops a single video session by name
// or by Sid, and disconnects all participants.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) EndVideoRoom(nameOrSid string) (videoResponse *VideoResponse, exception *Exception, err error) {
	twilioUrl := twilio.VideoUrl + "/v1/Rooms/" + nameOrSid
	formValues := url.Values{}
	formValues.Set("Status", fmt.Sprintf("%s", Completed))

	res, err := twilio.post(formValues, twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusOK {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(VideoResponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

func createRoomOptionsToFormValues(options *createRoomOptions) url.Values {
	formValues := url.Values{}
	formValues.Set("EnableTurn", fmt.Sprintf("%t", options.EnableTurn))
	formValues.Set("MaxParticipants", fmt.Sprintf("%d", options.MaxParticipants))
	formValues.Set("MediaRegion", fmt.Sprintf("%s", options.MediaRegion))
	formValues.Set("RecordParticipantsOnConnect", fmt.Sprintf("%t", options.RecordParticipantsOnConnect))
	formValues.Set("StatusCallback", options.StatusCallback)
	formValues.Set("StatusCallbackMethod", options.StatusCallbackMethod)
	formValues.Set("Type", fmt.Sprintf("%s", options.Type))
	formValues.Set("UniqueName", options.UniqueName)
	formValues.Del("VideoCodecs")
	for _, codec := range options.VideoCodecs {
		formValues.Add("VideoCodecs", fmt.Sprintf("%v", codec))
	}
	return formValues
}
<commit_msg>Add list video rooms method<commit_after>package gotwilio

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"
)

// MediaRegion is the locations of Twilio's
// TURN servers
type MediaRegion string

const (
	Australia   MediaRegion = "au1"
	Brazil      MediaRegion = "br1"
	Germany     MediaRegion = "de1"
	Ireland     MediaRegion = "ie1"
	India       MediaRegion = "in1"
	Japan       MediaRegion = "jp1"
	Singapore   MediaRegion = "sg1"
	USEastCoast MediaRegion = "us1"
	USWestCoast MediaRegion = "us2"
)

// VideoStatus is the status of a video room
type VideoStatus string

const (
	InProgress VideoStatus = "in-progress"
	Failed     VideoStatus = "failed"
	Completed  VideoStatus = "completed"
)

// VideoRoomType is how the participants connect
// to each other, whether peer-to-peer of routed
// through a TURN server.
type VideoRoomType string

const (
	PeerToPeer VideoRoomType = "peer-to-peer"
	Group      VideoRoomType = "group"
)

// VideoCodecs are the supported codecs when
// publishing a track to the room.
type VideoCodecs string

const (
	VP8  VideoCodecs = "VP8"
	H264 VideoCodecs = "H264"
)

// ListVideoResponse is returned when listing rooms
type ListVideoReponse struct {
	Rooms []*VideoResponse `json:"rooms"`
	Meta  struct {
		Page            int64  `json:"page"`
		PageSize        int64  `json:"page_size"`
		FirstPageUrl    string `json:"first_page_url"`
		PreviousPageUrl string `json:"previous_page_url"`
		NextPageUrl     string `json:"next_page_url"`
		Url             string `json:"url"`
		Key             string `json:"key"`
	} `json:"meta"`
}

// VideoResponse is returned for a single room
type VideoResponse struct {
	AccountSid                  string        `json:"account_sid"`
	DateCreated                 time.Time     `json:"date_created"`
	DateUpdated                 time.Time     `json:"date_updated"`
	Duration                    time.Duration `json:"duration"`
	EnableTurn                  bool          `json:"enable_turn"`
	EndTime                     time.Time     `json:"end_time"`
	MaxParticipants             int64         `json:"max_participants"`
	MediaRegion                 MediaRegion   `json:"media_region"`
	RecordParticipantsOnConnect bool          `json:"record_participants_on_connect"`
	Sid                         string        `json:"sid"`
	Status                      VideoStatus   `json:"status"`
	StatusCallback              string        `json:"status_callback"`
	StatusCallbackMethod        string        `json:"status_callback_method"`
	Type                        VideoRoomType `json:"type"`
	UniqueName                  string        `json:"unique_name"`
	URL                         string        `json:"url"`
}

type createRoomOptions struct {
	EnableTurn                  bool          `json:"EnableTurn"`
	MaxParticipants             int64         `json:"MaxParticipants"`
	MediaRegion                 MediaRegion   `json:"MediaRegion"`
	RecordParticipantsOnConnect bool          `json:"RecordParticipantsOnConnect"`
	StatusCallback              string        `json:"StatusCallback"`
	StatusCallbackMethod        string        `json:"StatusCallbackMethod"`
	Type                        VideoRoomType `json:"Type"`
	UniqueName                  string        `json:"UniqueName"`
	VideoCodecs                 []VideoCodecs `json:"VideoCodecs"`
}

// DefaultVideoRoomOptions are the default options
// for creating a room.
var DefaultVideoRoomOptions = &createRoomOptions{
	EnableTurn:                  true,
	MaxParticipants:             10,
	MediaRegion:                 USEastCoast,
	RecordParticipantsOnConnect: false,
	StatusCallback:              "",
	StatusCallbackMethod:        http.MethodPost,
	Type:                        Group,
	UniqueName:                  "",
	VideoCodecs:                 []VideoCodecs{H264},
}

// ListVideoRoomOptions are the options to query
// for a list of video rooms.
type ListVideoRoomOptions struct {
	DateCreatedAfter  time.Time   `json:"DateCreatedAfter"`
	DateCreatedBefore time.Time   `json:"DateCreatedBefore"`
	Status            VideoStatus `json:"Status"`
	UniqueName        string      `json:"EnableUniqueNameTurn"`
}

// CreateVideoRoom creates a video communication session
// for participants to connect to.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) CreateVideoRoom(options *createRoomOptions) (videoResponse *VideoResponse, exception *Exception, err error) {
	twilioUrl := twilio.VideoUrl + "/v1/Rooms"
	formValues := createRoomOptionsToFormValues(options)

	res, err := twilio.post(formValues, twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusCreated {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(VideoResponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

// DateCreatedAfter  time.Time   `json:"DateCreatedAfter"`
// DateCreatedBefore time.Time   `json:"DateCreatedBefore"`
// Status            VideoStatus `json:"Status"`
// UniqueName        string      `json:"EnableUniqueNameTurn"`

// ListVideoRooms returns a list of all video rooms.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) ListVideoRooms(options *ListVideoRoomOptions) (videoResponse *ListVideoReponse, exception *Exception, err error) {
	q := &url.Values{}
	if !options.DateCreatedAfter.Equal(time.Time{}) {
		q.Set("DateCreatedAfter", options.DateCreatedAfter.Format(time.RFC3339))
	}
	if !options.DateCreatedBefore.Equal(time.Time{}) {
		q.Set("DateCreatedBefore", options.DateCreatedBefore.Format(time.RFC3339))
	}
	if options.Status != "" {
		q.Set("Status", fmt.Sprintf("%s", options.Status))
	}
	if options.UniqueName != "" {
		q.Set("UniqueName", options.UniqueName)
	}

	twilioUrl := twilio.VideoUrl + "/v1/Rooms?" + q.Encode()

	res, err := twilio.get(twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusOK {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(ListVideoReponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

// GetVideoRoom retrievs a single video session
// by name or by Sid.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) GetVideoRoom(nameOrSid string) (videoResponse *VideoResponse, exception *Exception, err error) {
	twilioUrl := twilio.VideoUrl + "/v1/Rooms/" + nameOrSid

	res, err := twilio.get(twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusOK {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(VideoResponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

// EndVideoRoom stops a single video session by name
// or by Sid, and disconnects all participants.
// See https://www.twilio.com/docs/video/api/rooms-resource
// for more information.
func (twilio *Twilio) EndVideoRoom(nameOrSid string) (videoResponse *VideoResponse, exception *Exception, err error) {
	twilioUrl := twilio.VideoUrl + "/v1/Rooms/" + nameOrSid
	formValues := url.Values{}
	formValues.Set("Status", fmt.Sprintf("%s", Completed))

	res, err := twilio.post(formValues, twilioUrl)
	if err != nil {
		return videoResponse, exception, err
	}
	defer res.Body.Close()

	responseBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return videoResponse, exception, err
	}

	if res.StatusCode != http.StatusOK {
		exception = new(Exception)
		err = json.Unmarshal(responseBody, exception)

		// We aren't checking the error because we don't actually care.
		// It's going to be passed to the client either way.
		return videoResponse, exception, err
	}

	videoResponse = new(VideoResponse)
	err = json.Unmarshal(responseBody, videoResponse)
	return videoResponse, exception, err
}

func createRoomOptionsToFormValues(options *createRoomOptions) url.Values {
	formValues := url.Values{}
	formValues.Set("EnableTurn", fmt.Sprintf("%t", options.EnableTurn))
	formValues.Set("MaxParticipants", fmt.Sprintf("%d", options.MaxParticipants))
	formValues.Set("MediaRegion", fmt.Sprintf("%s", options.MediaRegion))
	formValues.Set("RecordParticipantsOnConnect", fmt.Sprintf("%t", options.RecordParticipantsOnConnect))
	formValues.Set("StatusCallback", options.StatusCallback)
	formValues.Set("StatusCallbackMethod", options.StatusCallbackMethod)
	formValues.Set("Type", fmt.Sprintf("%s", options.Type))
	formValues.Set("UniqueName", options.UniqueName)
	formValues.Del("VideoCodecs")
	for _, codec := range options.VideoCodecs {
		formValues.Add("VideoCodecs", fmt.Sprintf("%v", codec))
	}
	return formValues
}
<|endoftext|>


<commit_before>// Copyright 2015 Datajin Technologies, Inc. All rights reserved.

package commands

import "github.com/codegangsta/cli"

// This software is provided "AS IS" but the service it connects to has its own
// terms and polcies.
const notices = `
Use of the mktmpio service is subject to the the following:
 * mktmpio Privacy Policy
   https://mktmp.io/privacy-policy
 * mktmpio Terms of Service
   https://mktmp.io/terms-of-service
`

// The dependencies list below is manually populated using the output from:
// go list -f {{.Deps}} ./...
const thirdparty = `
This binary includes 3rd party software:

 * github.com/codegangsta/cli
   MIT license
   Copyright (C) 2013 Jeremy Saenz

 * github.com/mitchellh/go-homedir
   MIT license
   Copyright (c) 2013 Mitchell Hashimoto

 * github.com/mktmpio/go-mktmpio
   Artistic-2.0 license
   Copyright (c) 2015 Datajin Technologies, Inc.

 * golang.org/x/crypto/ssh/terminal
   golang.org/x/net/websocket
   BSD-2 license
   Copyright (c) 2009 The Go Authors. All rights reserved.

 * gopkg.in/yaml.v2
   LGPL3 license
   Copyright (c) 2011-2014 - Canonical Inc.
`

// This software is provided "AS IS"
const warranties = `
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
`

// Contact info
const contact = `
For more information contact Datajin Technologies, Inc.:
 w: https://mktmp.io
 e: mktmpio@datajin.com
`

// Definition for the 'mktmpio config' command
var LegalCommand = cli.Command{
	Name:   "legal",
	Usage:  "display licensing, copyright, and warranty notices",
	Action: licenseAction,
}

func licenseAction(c *cli.Context) {
	c.App.Writer.Write([]byte("\nNOTICES:\n" + notices))
	c.App.Writer.Write([]byte("\nWARRANTIES:\n" + warranties))
	c.App.Writer.Write([]byte("\n3RD PARTY SOFTWARE:\n" + thirdparty))
	c.App.Writer.Write([]byte("\nCONTACT:\n" + contact))
}
<commit_msg>meta: update list of bundled dependencies<commit_after>// Copyright 2015 Datajin Technologies, Inc. All rights reserved.

package commands

import "github.com/codegangsta/cli"

// This software is provided "AS IS" but the service it connects to has its own
// terms and polcies.
const notices = `
Use of the mktmpio service is subject to the the following:
 * mktmpio Privacy Policy
   https://mktmp.io/privacy-policy
 * mktmpio Terms of Service
   https://mktmp.io/terms-of-service
`

// The dependencies list below is manually populated using the output from:
// go list -f {{.Deps}} ./...
const thirdparty = `
This binary includes 3rd party software:

 * github.com/codegangsta/cli
   MIT license
   Copyright (C) 2013 Jeremy Saenz

 * github.com/docker/docker/pkg/stdcopy
   Apache License 2.0
   Copyright 2013-2015 Docker, Inc.

 * github.com/mitchellh/go-homedir
   MIT license
   Copyright (c) 2013 Mitchell Hashimoto

 * github.com/mktmpio/go-mktmpio
   Artistic-2.0 license
   Copyright (c) 2015 Datajin Technologies, Inc.

 * golang.org/x/crypto/ssh/terminal
   golang.org/x/net/websocket
   BSD-2 license
   Copyright (c) 2009 The Go Authors. All rights reserved.

 * gopkg.in/yaml.v2
   LGPL3 license
   Copyright (c) 2011-2014 - Canonical Inc.

 * github.com/Sirupsen/logrus
   MIT License
   Copyright (c) 2014 Simon Eskildsen
`

// This software is provided "AS IS"
const warranties = `
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
`

// Contact info
const contact = `
For more information contact Datajin Technologies, Inc.:
 w: https://mktmp.io
 e: mktmpio@datajin.com
`

// Definition for the 'mktmpio config' command
var LegalCommand = cli.Command{
	Name:   "legal",
	Usage:  "display licensing, copyright, and warranty notices",
	Action: licenseAction,
}

func licenseAction(c *cli.Context) {
	c.App.Writer.Write([]byte("\nNOTICES:\n" + notices))
	c.App.Writer.Write([]byte("\nWARRANTIES:\n" + warranties))
	c.App.Writer.Write([]byte("\n3RD PARTY SOFTWARE:\n" + thirdparty))
	c.App.Writer.Write([]byte("\nCONTACT:\n" + contact))
}
<|endoftext|>


<commit_before>package logpeck

import (
	"errors"
	log "github.com/Sirupsen/logrus"
	sjson "github.com/bitly/go-simplejson"
)

type ExtractorConfig struct {
	Name   string
	Config interface{}
}

type Extractor interface {
	Extract(content string) (map[string]interface{}, error)
	Close()
}

func NewExtractorConfig(configStr string) (ExtractorConfig, error) {
	c := ExtractorConfig{}
	j, err := sjson.NewJson([]byte(configStr))
	name, err := j.Get("Name").String()
	cJ := j.Get("Config")
	if err != nil || name == "" {
		return c, errors.New("extractor unmarshal error")
	}
	jbyte, err := cJ.MarshalJSON()
	if err != nil {
		return c, err
	}
	switch name {
	case "lua":
		c.Config, err = NewLuaExtractorConfig(jbyte)
	case "json":
		c.Config, err = NewJsonExtractorConfig(jbyte)
	case "text":
		c.Config, err = NewTextExtractorConfig(jbyte)
	default:
		err = errors.New("extractor name error: " + c.Name)
	}
	c.Name = name
	log.Infof("[ExtractorConfig] Init finish %#v, %#v", c, err)
	return c, err
}

func NewExtractor(c ExtractorConfig, fields []PeckField) (e Extractor, err error) {
	switch c.Name {
	case "lua":
		e, err = NewLuaExtractor(c.Config)
	case "json":
		e, err = NewJsonExtractor(c.Config, fields)
	case "text":
		e, err = NewTextExtractor(c.Config, fields)
	default:
		err = errors.New("extractor name error: " + c.Name)
	}
	return e, err
}
<commit_msg>bugfix in new extractor config<commit_after>package logpeck

import (
	"errors"
	log "github.com/Sirupsen/logrus"
	sjson "github.com/bitly/go-simplejson"
)

type ExtractorConfig struct {
	Name   string
	Config interface{}
}

type Extractor interface {
	Extract(content string) (map[string]interface{}, error)
	Close()
}

func NewExtractorConfig(configStr string) (ExtractorConfig, error) {
	c := ExtractorConfig{}
	j, err := sjson.NewJson([]byte(configStr))
	name, err := j.Get("Name").String()
	cJ := j.Get("Config")
	if err != nil || name == "" {
		return c, nil
	}
	jbyte, err := cJ.MarshalJSON()
	if err != nil {
		return c, err
	}
	switch name {
	case "lua":
		c.Config, err = NewLuaExtractorConfig(jbyte)
	case "json":
		c.Config, err = NewJsonExtractorConfig(jbyte)
	case "text":
		c.Config, err = NewTextExtractorConfig(jbyte)
	default:
		err = errors.New("extractor name error: " + c.Name)
	}
	c.Name = name
	log.Infof("[ExtractorConfig] Init finish %#v, %#v", c, err)
	return c, err
}

func NewExtractor(c ExtractorConfig, fields []PeckField) (e Extractor, err error) {
	switch c.Name {
	case "lua":
		e, err = NewLuaExtractor(c.Config)
	case "json":
		e, err = NewJsonExtractor(c.Config, fields)
	case "text":
		e, err = NewTextExtractor(c.Config, fields)
	default:
		err = errors.New("extractor name error: " + c.Name)
	}
	return e, err
}
<|endoftext|>


<commit_before>// Copyright 2018 The Terraformer Authors.
//
// 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.

package aws

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/GoogleCloudPlatform/terraformer/terraform_utils"

	"github.com/aws/aws-sdk-go-v2/service/iam"

	"github.com/aws/aws-sdk-go-v2/aws"
)

var IamAllowEmptyValues = []string{"tags."}

var IamAdditionalFields = map[string]interface{}{}

type IamGenerator struct {
	AWSService
}

func (g *IamGenerator) InitResources() error {
	config, e := g.generateConfig()
	if e != nil {
		return e
	}
	svc := iam.New(config)
	g.Resources = []terraform_utils.Resource{}
	err := g.getUsers(svc)
	if err != nil {
		log.Println(err)
	}

	err = g.getGroups(svc)
	if err != nil {
		log.Println(err)
	}

	err = g.getPolicies(svc)
	if err != nil {
		log.Println(err)
	}

	err = g.getRoles(svc)
	if err != nil {
		log.Println(err)
	}

	return nil
}

func (g *IamGenerator) getRoles(svc *iam.Client) error {
	p := iam.NewListRolesPaginator(svc.ListRolesRequest(&iam.ListRolesInput{}))
	for p.Next(context.Background()) {
		for _, role := range p.CurrentPage().Roles {
			roleID := aws.StringValue(role.RoleId)
			roleName := aws.StringValue(role.RoleName)
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				roleID,
				roleName,
				"aws_iam_role",
				"aws",
				IamAllowEmptyValues))
			rolePoliciesPage := iam.NewListRolePoliciesPaginator(svc.ListRolePoliciesRequest(&iam.ListRolePoliciesInput{RoleName: role.RoleName}))
			for rolePoliciesPage.Next(context.Background()) {
				for _, policyName := range rolePoliciesPage.CurrentPage().PolicyNames {
					g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
						roleName+":"+policyName,
						roleName+"_"+policyName,
						"aws_iam_role_policy",
						"aws",
						IamAllowEmptyValues))
				}
			}
			if err := rolePoliciesPage.Err(); err != nil {
				log.Println(err)
				continue
			}
		}
	}
	return p.Err()
}

func (g *IamGenerator) getUsers(svc *iam.Client) error {
	p := iam.NewListUsersPaginator(svc.ListUsersRequest(&iam.ListUsersInput{}))
	for p.Next(context.Background()) {
		for _, user := range p.CurrentPage().Users {
			resourceName := aws.StringValue(user.UserName)
			g.Resources = append(g.Resources, terraform_utils.NewResource(
				resourceName,
				aws.StringValue(user.UserId),
				"aws_iam_user",
				"aws",
				map[string]string{
					"force_destroy": "false",
				},
				IamAllowEmptyValues,
				map[string]interface{}{}))
			err := g.getUserPolices(svc, user.UserName)
			if err != nil {
				log.Println(err)
			}
			//g.getUserGroup(svc, user.UserName) //not work maybe terraform-aws bug
		}
	}
	return p.Err()
}

func (g *IamGenerator) getUserGroup(svc *iam.Client, userName *string) error {
	p := iam.NewListGroupsForUserPaginator(svc.ListGroupsForUserRequest(&iam.ListGroupsForUserInput{UserName: userName}))
	for p.Next(context.Background()) {
		for _, group := range p.CurrentPage().Groups {
			resourceName := aws.StringValue(group.GroupName)
			groupIDAttachment := aws.StringValue(group.GroupName)
			g.Resources = append(g.Resources, terraform_utils.NewResource(
				groupIDAttachment,
				resourceName,
				"aws_iam_user_group_membership",
				"aws",
				map[string]string{"user": aws.StringValue(userName)},
				IamAllowEmptyValues,
				IamAdditionalFields,
			))
		}
	}
	return p.Err()
}

func (g *IamGenerator) getUserPolices(svc *iam.Client, userName *string) error {
	p := iam.NewListUserPoliciesPaginator(svc.ListUserPoliciesRequest(&iam.ListUserPoliciesInput{UserName: userName}))
	for p.Next(context.Background()) {
		for _, policy := range p.CurrentPage().PolicyNames {
			resourceName := aws.StringValue(userName) + "_" + policy
			resourceName = strings.Replace(resourceName, "@", "", -1)
			policyID := aws.StringValue(userName) + ":" + policy
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				policyID,
				resourceName,
				"aws_iam_user_policy",
				"aws",
				IamAllowEmptyValues))
		}
	}
	return p.Err()
}

func (g *IamGenerator) getPolicies(svc *iam.Client) error {
	p := iam.NewListPoliciesPaginator(svc.ListPoliciesRequest(&iam.ListPoliciesInput{}))
	for p.Next(context.Background()) {
		for _, policy := range p.CurrentPage().Policies {
			resourceName := aws.StringValue(policy.PolicyName)
			policyARN := aws.StringValue(policy.Arn)

			g.Resources = append(g.Resources, terraform_utils.NewResource(
				policyARN,
				resourceName,
				"aws_iam_policy_attachment",
				"aws",
				map[string]string{
					"policy_arn": policyARN,
					"name":       resourceName,
				},
				IamAllowEmptyValues,
				IamAdditionalFields))
			// not use AWS main policy
			if strings.HasPrefix(policyARN, "arn:aws:iam::aws:policy") {
				continue
			}
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				policyARN,
				resourceName,
				"aws_iam_policy",
				"aws",
				IamAllowEmptyValues))

		}
	}
	return p.Err()
}

func (g *IamGenerator) getGroups(svc *iam.Client) error {
	p := iam.NewListGroupsPaginator(svc.ListGroupsRequest(&iam.ListGroupsInput{}))
	for p.Next(context.Background()) {
		for _, group := range p.CurrentPage().Groups {
			resourceName := aws.StringValue(group.GroupName)
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				resourceName,
				resourceName,
				"aws_iam_group",
				"aws",
				IamAllowEmptyValues))
			g.Resources = append(g.Resources, terraform_utils.NewResource(
				resourceName,
				resourceName,
				"aws_iam_group_membership",
				"aws",
				map[string]string{
					"group": resourceName,
					"name":  resourceName,
				},
				[]string{"tags.", "users."},
				IamAdditionalFields))
			groupPoliciesPage := iam.NewListGroupPoliciesPaginator(svc.ListGroupPoliciesRequest(&iam.ListGroupPoliciesInput{GroupName: group.GroupName}))
			for groupPoliciesPage.Next(context.Background()) {
				for _, policy := range groupPoliciesPage.CurrentPage().PolicyNames {
					id := resourceName + ":" + policy
					groupPolicyName := resourceName + "_" + policy
					g.Resources = append(g.Resources, terraform_utils.NewResource(
						id,
						groupPolicyName,
						"aws_iam_group_policy",
						"aws",
						map[string]string{},
						IamAllowEmptyValues,
						IamAdditionalFields))
				}
			}
			if err := groupPoliciesPage.Err(); err != nil {
				log.Println(err)
			}
		}
	}
	return p.Err()
}

// PostGenerateHook for add policy json as heredoc
func (g *IamGenerator) PostConvertHook() error {
	for i, resource := range g.Resources {
		if resource.InstanceInfo.Type == "aws_iam_policy" ||
			resource.InstanceInfo.Type == "aws_iam_user_policy" ||
			resource.InstanceInfo.Type == "aws_iam_group_policy" ||
			resource.InstanceInfo.Type == "aws_iam_role_policy" {
			policy := resource.Item["policy"].(string)
			resource.Item["policy"] = fmt.Sprintf(`<<POLICY
%s
POLICY`, policy)
		} else if resource.InstanceInfo.Type == "aws_iam_role" {
			policy := resource.Item["assume_role_policy"].(string)
			g.Resources[i].Item["assume_role_policy"] = fmt.Sprintf(`<<POLICY
%s
POLICY`, policy)
		}
	}
	return nil
}
<commit_msg>fixed listing only local IAM policies<commit_after>// Copyright 2018 The Terraformer Authors.
//
// 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.

package aws

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/GoogleCloudPlatform/terraformer/terraform_utils"

	"github.com/aws/aws-sdk-go-v2/service/iam"

	"github.com/aws/aws-sdk-go-v2/aws"
)

var IamAllowEmptyValues = []string{"tags."}

var IamAdditionalFields = map[string]interface{}{}

type IamGenerator struct {
	AWSService
}

func (g *IamGenerator) InitResources() error {
	config, e := g.generateConfig()
	if e != nil {
		return e
	}
	svc := iam.New(config)
	g.Resources = []terraform_utils.Resource{}
	err := g.getUsers(svc)
	if err != nil {
		log.Println(err)
	}

	err = g.getGroups(svc)
	if err != nil {
		log.Println(err)
	}

	err = g.getPolicies(svc)
	if err != nil {
		log.Println(err)
	}

	err = g.getRoles(svc)
	if err != nil {
		log.Println(err)
	}

	return nil
}

func (g *IamGenerator) getRoles(svc *iam.Client) error {
	p := iam.NewListRolesPaginator(svc.ListRolesRequest(&iam.ListRolesInput{}))
	for p.Next(context.Background()) {
		for _, role := range p.CurrentPage().Roles {
			roleID := aws.StringValue(role.RoleId)
			roleName := aws.StringValue(role.RoleName)
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				roleID,
				roleName,
				"aws_iam_role",
				"aws",
				IamAllowEmptyValues))
			rolePoliciesPage := iam.NewListRolePoliciesPaginator(svc.ListRolePoliciesRequest(&iam.ListRolePoliciesInput{RoleName: role.RoleName}))
			for rolePoliciesPage.Next(context.Background()) {
				for _, policyName := range rolePoliciesPage.CurrentPage().PolicyNames {
					g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
						roleName+":"+policyName,
						roleName+"_"+policyName,
						"aws_iam_role_policy",
						"aws",
						IamAllowEmptyValues))
				}
			}
			if err := rolePoliciesPage.Err(); err != nil {
				log.Println(err)
				continue
			}
		}
	}
	return p.Err()
}

func (g *IamGenerator) getUsers(svc *iam.Client) error {
	p := iam.NewListUsersPaginator(svc.ListUsersRequest(&iam.ListUsersInput{}))
	for p.Next(context.Background()) {
		for _, user := range p.CurrentPage().Users {
			resourceName := aws.StringValue(user.UserName)
			g.Resources = append(g.Resources, terraform_utils.NewResource(
				resourceName,
				aws.StringValue(user.UserId),
				"aws_iam_user",
				"aws",
				map[string]string{
					"force_destroy": "false",
				},
				IamAllowEmptyValues,
				map[string]interface{}{}))
			err := g.getUserPolices(svc, user.UserName)
			if err != nil {
				log.Println(err)
			}
			//g.getUserGroup(svc, user.UserName) //not work maybe terraform-aws bug
		}
	}
	return p.Err()
}

func (g *IamGenerator) getUserGroup(svc *iam.Client, userName *string) error {
	p := iam.NewListGroupsForUserPaginator(svc.ListGroupsForUserRequest(&iam.ListGroupsForUserInput{UserName: userName}))
	for p.Next(context.Background()) {
		for _, group := range p.CurrentPage().Groups {
			resourceName := aws.StringValue(group.GroupName)
			groupIDAttachment := aws.StringValue(group.GroupName)
			g.Resources = append(g.Resources, terraform_utils.NewResource(
				groupIDAttachment,
				resourceName,
				"aws_iam_user_group_membership",
				"aws",
				map[string]string{"user": aws.StringValue(userName)},
				IamAllowEmptyValues,
				IamAdditionalFields,
			))
		}
	}
	return p.Err()
}

func (g *IamGenerator) getUserPolices(svc *iam.Client, userName *string) error {
	p := iam.NewListUserPoliciesPaginator(svc.ListUserPoliciesRequest(&iam.ListUserPoliciesInput{UserName: userName}))
	for p.Next(context.Background()) {
		for _, policy := range p.CurrentPage().PolicyNames {
			resourceName := aws.StringValue(userName) + "_" + policy
			resourceName = strings.Replace(resourceName, "@", "", -1)
			policyID := aws.StringValue(userName) + ":" + policy
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				policyID,
				resourceName,
				"aws_iam_user_policy",
				"aws",
				IamAllowEmptyValues))
		}
	}
	return p.Err()
}

func (g *IamGenerator) getPolicies(svc *iam.Client) error {
	p := iam.NewListPoliciesPaginator(svc.ListPoliciesRequest(&iam.ListPoliciesInput{Scope:iam.PolicyScopeTypeLocal}))
	for p.Next(context.Background()) {
		for _, policy := range p.CurrentPage().Policies {
			resourceName := aws.StringValue(policy.PolicyName)
			policyARN := aws.StringValue(policy.Arn)

			g.Resources = append(g.Resources, terraform_utils.NewResource(
				policyARN,
				resourceName,
				"aws_iam_policy_attachment",
				"aws",
				map[string]string{
					"policy_arn": policyARN,
					"name":       resourceName,
				},
				IamAllowEmptyValues,
				IamAdditionalFields))
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				policyARN,
				resourceName,
				"aws_iam_policy",
				"aws",
				IamAllowEmptyValues))

		}
	}
	return p.Err()
}

func (g *IamGenerator) getGroups(svc *iam.Client) error {
	p := iam.NewListGroupsPaginator(svc.ListGroupsRequest(&iam.ListGroupsInput{}))
	for p.Next(context.Background()) {
		for _, group := range p.CurrentPage().Groups {
			resourceName := aws.StringValue(group.GroupName)
			g.Resources = append(g.Resources, terraform_utils.NewSimpleResource(
				resourceName,
				resourceName,
				"aws_iam_group",
				"aws",
				IamAllowEmptyValues))
			g.Resources = append(g.Resources, terraform_utils.NewResource(
				resourceName,
				resourceName,
				"aws_iam_group_membership",
				"aws",
				map[string]string{
					"group": resourceName,
					"name":  resourceName,
				},
				[]string{"tags.", "users."},
				IamAdditionalFields))
			groupPoliciesPage := iam.NewListGroupPoliciesPaginator(svc.ListGroupPoliciesRequest(&iam.ListGroupPoliciesInput{GroupName: group.GroupName}))
			for groupPoliciesPage.Next(context.Background()) {
				for _, policy := range groupPoliciesPage.CurrentPage().PolicyNames {
					id := resourceName + ":" + policy
					groupPolicyName := resourceName + "_" + policy
					g.Resources = append(g.Resources, terraform_utils.NewResource(
						id,
						groupPolicyName,
						"aws_iam_group_policy",
						"aws",
						map[string]string{},
						IamAllowEmptyValues,
						IamAdditionalFields))
				}
			}
			if err := groupPoliciesPage.Err(); err != nil {
				log.Println(err)
			}
		}
	}
	return p.Err()
}

// PostGenerateHook for add policy json as heredoc
func (g *IamGenerator) PostConvertHook() error {
	for i, resource := range g.Resources {
		if resource.InstanceInfo.Type == "aws_iam_policy" ||
			resource.InstanceInfo.Type == "aws_iam_user_policy" ||
			resource.InstanceInfo.Type == "aws_iam_group_policy" ||
			resource.InstanceInfo.Type == "aws_iam_role_policy" {
			policy := resource.Item["policy"].(string)
			resource.Item["policy"] = fmt.Sprintf(`<<POLICY
%s
POLICY`, policy)
		} else if resource.InstanceInfo.Type == "aws_iam_role" {
			policy := resource.Item["assume_role_policy"].(string)
			g.Resources[i].Item["assume_role_policy"] = fmt.Sprintf(`<<POLICY
%s
POLICY`, policy)
		}
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package s3

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"sort"
	"strings"
	"time"

	"github.com/alecthomas/template"
	"github.com/blang/semver"
	"github.com/goamz/goamz/aws"
	"github.com/goamz/goamz/s3"
	keybase1 "github.com/keybase/client/go/protocol"
	"github.com/keybase/release/update"
	"github.com/keybase/release/version"
)

type Section struct {
	Header   string
	Releases []Release
}

type Release struct {
	Name       string
	Key        s3.Key
	URL        string
	Version    string
	DateString string
	Date       time.Time
	Commit     string
}

type ByRelease []Release

func (s ByRelease) Len() int {
	return len(s)
}

func (s ByRelease) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

func (s ByRelease) Less(i, j int) bool {
	// Reverse date order
	return s[j].Date.Before(s[i].Date)
}

type Client struct {
	s3 *s3.S3
}

func NewClient() (*Client, error) {
	auth, err := aws.EnvAuth()
	if err != nil {
		return nil, err
	}
	s3 := s3.New(auth, aws.USEast)
	return &Client{s3: s3}, nil
}

func convertEastern(t time.Time) time.Time {
	locationNewYork, err := time.LoadLocation("America/New_York")
	if err != nil {
		log.Printf("Couldn't load location: %s", err)
	}
	return t.In(locationNewYork)
}

func loadReleases(keys []s3.Key, bucketName string, prefix string, suffix string, truncate int) []Release {
	var releases []Release
	for _, k := range keys {
		if strings.HasSuffix(k.Key, suffix) {
			urlString, name := urlStringForKey(k, bucketName, prefix)
			version, date, commit, err := version.Parse(name)
			if err != nil {
				log.Printf("Couldn't get version from name: %s\n", name)
			}
			date = convertEastern(date)
			releases = append(releases,
				Release{
					Name:       name,
					Key:        k,
					URL:        urlString,
					Version:    version,
					Date:       date,
					DateString: date.Format("Mon Jan _2 15:04:05 MST 2006"),
					Commit:     commit,
				})
		}
	}
	// TODO: Should also sanity check that version sort is same as time sort
	// otherwise something got messed up
	sort.Sort(ByRelease(releases))
	if truncate > 0 && len(releases) > truncate {
		releases = releases[0:truncate]
	}
	return releases
}

func WriteHTML(path string, bucketName string, prefixes string, suffix string) error {
	client, err := NewClient()
	if err != nil {
		return err
	}
	bucket := client.s3.Bucket(bucketName)
	if bucket == nil {
		return fmt.Errorf("Bucket %s not found", bucketName)
	}

	var sections []Section
	for _, prefix := range strings.Split(prefixes, ",") {
		resp, err := bucket.List(prefix, "", "", 0)
		if err != nil {
			return err
		}

		releases := loadReleases(resp.Contents, bucketName, prefix, suffix, 20)
		if len(releases) > 0 {
			log.Printf("Found %d release(s) at %s\n", len(releases), prefix)
			for _, release := range releases {
				log.Printf(" %s %s %s\n", release.Name, release.Version, release.DateString)
			}
		}
		sections = append(sections, Section{
			Header:   prefix,
			Releases: releases,
		})
	}

	return WriteHTMLForLinks(path, bucketName, sections)
}

var htmlTemplate = `
<!doctype html>

  <title>{{ .Title }}</title>

	{{ range $index, $sec := .Sections }}

{{ $sec.Header }}

		{{ range $index2, $rel := $sec.Releases }}

{{ $rel.Name }} {{ $rel.Version }} {{ $rel.Date }} {{ $rel.Commit }}

		{{ end }}

	{{ end }}

`

func WriteHTMLForLinks(path string, title string, sections []Section) error {
	vars := map[string]interface{}{
		"Title":    title,
		"Sections": sections,
	}

	t, err := template.New("t").Parse(htmlTemplate)
	if err != nil {
		return err
	}

	if path != "" {
		var data bytes.Buffer
		err = t.Execute(&data, vars)
		if err != nil {
			return err
		}
		err := makeParentDirs(path)
		if err != nil {
			return err
		}
		return ioutil.WriteFile(path, data.Bytes(), 0644)
	}
	return nil
}

type Platform struct {
	Name          string
	Prefix        string
	PrefixSupport string
	Suffix        string
	LatestName    string
}

func CopyLatest(bucketName string) error {
	client, err := NewClient()
	if err != nil {
		return err
	}
	return client.CopyLatest(bucketName)
}

func Platforms() []Platform {
	return []Platform{
		Platform{Name: "darwin", Prefix: "darwin/", PrefixSupport: "darwin-support/", LatestName: "Keybase.dmg"},
		Platform{Name: "deb", Prefix: "linux_binaries/deb/", Suffix: "_amd64.deb", LatestName: "keybase_amd64.deb"},
		Platform{Name: "rpm", Prefix: "linux_binaries/rpm/", Suffix: ".x86_64.rpm", LatestName: "keybase_amd64.rpm"},
		Platform{Name: "windows", Prefix: "windows/", Suffix: ".386.exe", LatestName: "keybase_setup_386.exe"},
	}
}

func FindPlatform(name string) *Platform {
	platforms := Platforms()
	for _, p := range platforms {
		if p.Name == name {
			return &p
		}
	}
	return nil
}

func (p *Platform) FindRelease(bucket s3.Bucket, f func(r Release) bool) (*Release, error) {
	resp, err := bucket.List(p.Prefix, "", "", 0)
	if err != nil {
		return nil, err
	}
	releases := loadReleases(resp.Contents, bucket.Name, p.Prefix, p.Suffix, 0)
	for _, release := range releases {
		k := release.Key
		if !strings.HasSuffix(k.Key, p.Suffix) {
			continue
		}
		if f(release) {
			return &release, nil
		}
	}
	return nil, nil
}

func (c *Client) CopyLatest(bucketName string) error {
	bucket := c.s3.Bucket(bucketName)

	platforms := Platforms()

	for _, platform := range platforms {
		release, err := platform.FindRelease(*bucket, func(r Release) bool { return true })
		if err != nil {
			return err
		}
		if release == nil {
			continue
		}
		k := release.Key
		url, _ := urlStringForKey(k, bucketName, platform.Prefix)
		// Instead of linking, we're making copies. S3 linking has some issues.
		// headers := map[string][]string{
		// 	"x-amz-website-redirect-location": []string{url},
		// }
		//err = bucket.PutHeader(name, []byte{}, headers, s3.PublicRead)
		log.Printf("Copying %s from %s (latest)\n", platform.LatestName, k.Key)
		_, err = bucket.PutCopy(platform.LatestName, s3.PublicRead, s3.CopyOptions{}, url)
		if err != nil {
			return err
		}
	}
	return nil
}

func (c *Client) CurrentUpdate(bucketName string, platform string, env string) (currentUpdate *keybase1.Update, err error) {
	bucket := c.s3.Bucket(bucketName)

	data, err := bucket.Get(fmt.Sprintf("update-%s-%s.json", platform, env))
	if err != nil {
		return
	}
	currentUpdate, err = update.DecodeJSON(data)
	return
}

func PromoteRelease(bucketName string, delay time.Duration, hourEastern int, channel string, platform string, env string) (*Release, error) {
	client, err := NewClient()
	if err != nil {
		return nil, err
	}
	return client.PromoteRelease(bucketName, delay, hourEastern, channel, platform, env)
}

func updateJSONName(channel string, platformName string, env string) string {
	if channel == "" {
		return fmt.Sprintf("update-%s-%s.json", platformName, env)
	}
	return fmt.Sprintf("update-%s-%s-%s.json", platformName, env, channel)
}

func (c *Client) PromoteRelease(bucketName string, delay time.Duration, hourEastern int, channel string, platformName string, env string) (*Release, error) {
	if channel == "" {
		log.Printf("Finding release to promote (%s delay)", delay)
	} else {
		log.Printf("Finding release to promote for %s channel (%s delay)", channel, delay)
	}
	bucket := c.s3.Bucket(bucketName)

	platform := FindPlatform(platformName)
	if platform == nil {
		return nil, fmt.Errorf("Unsupported platform")
	}
	release, err := platform.FindRelease(*bucket, func(r Release) bool {
		if delay != 0 && time.Since(r.Date) < delay {
			return false
		}
		hour, min, _ := r.Date.Clock()
		if hourEastern != 0 && hour < hourEastern && min < 15 {
			return false
		}
		return true
	})
	if err != nil {
		return nil, err
	}

	if release == nil {
		return nil, nil
	}
	log.Printf("Found release %s (%s), %s", release.Name, time.Since(release.Date), release.Version)

	// Get current update (ok to ignore error since it might have been pulled)
	currentUpdate, _ := c.CurrentUpdate(bucketName, platformName, env)
	if currentUpdate != nil {
		log.Printf("Found update: %s", currentUpdate.Version)
		currentVer, err := semver.Make(currentUpdate.Version)
		if err != nil {
			return nil, err
		}
		releaseVer, err := semver.Make(release.Version)
		if err != nil {
			return nil, err
		}

		if releaseVer.Equals(currentVer) {
			log.Printf("Release unchanged")
			return nil, nil
		} else if releaseVer.LT(currentVer) {
			log.Printf("Release older than current update")
			return nil, nil
		}
	}

	jsonName := updateJSONName(channel, platformName, env)
	jsonURL := urlString(bucketName, platform.PrefixSupport, fmt.Sprintf("update-%s-%s-%s.json", platformName, env, release.Version))
	log.Printf("Promoting %s", jsonURL)
	_, err = bucket.PutCopy(jsonName, s3.PublicRead, s3.CopyOptions{}, jsonURL)
	if err != nil {
		return nil, err
	}
	return release, nil
}
<commit_msg>Don't check current release, just promote<commit_after>// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.

package s3

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"sort"
	"strings"
	"time"

	"github.com/alecthomas/template"
	"github.com/goamz/goamz/aws"
	"github.com/goamz/goamz/s3"
	keybase1 "github.com/keybase/client/go/protocol"
	"github.com/keybase/release/update"
	"github.com/keybase/release/version"
)

type Section struct {
	Header   string
	Releases []Release
}

type Release struct {
	Name       string
	Key        s3.Key
	URL        string
	Version    string
	DateString string
	Date       time.Time
	Commit     string
}

type ByRelease []Release

func (s ByRelease) Len() int {
	return len(s)
}

func (s ByRelease) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

func (s ByRelease) Less(i, j int) bool {
	// Reverse date order
	return s[j].Date.Before(s[i].Date)
}

type Client struct {
	s3 *s3.S3
}

func NewClient() (*Client, error) {
	auth, err := aws.EnvAuth()
	if err != nil {
		return nil, err
	}
	s3 := s3.New(auth, aws.USEast)
	return &Client{s3: s3}, nil
}

func convertEastern(t time.Time) time.Time {
	locationNewYork, err := time.LoadLocation("America/New_York")
	if err != nil {
		log.Printf("Couldn't load location: %s", err)
	}
	return t.In(locationNewYork)
}

func loadReleases(keys []s3.Key, bucketName string, prefix string, suffix string, truncate int) []Release {
	var releases []Release
	for _, k := range keys {
		if strings.HasSuffix(k.Key, suffix) {
			urlString, name := urlStringForKey(k, bucketName, prefix)
			version, date, commit, err := version.Parse(name)
			if err != nil {
				log.Printf("Couldn't get version from name: %s\n", name)
			}
			date = convertEastern(date)
			releases = append(releases,
				Release{
					Name:       name,
					Key:        k,
					URL:        urlString,
					Version:    version,
					Date:       date,
					DateString: date.Format("Mon Jan _2 15:04:05 MST 2006"),
					Commit:     commit,
				})
		}
	}
	// TODO: Should also sanity check that version sort is same as time sort
	// otherwise something got messed up
	sort.Sort(ByRelease(releases))
	if truncate > 0 && len(releases) > truncate {
		releases = releases[0:truncate]
	}
	return releases
}

func WriteHTML(path string, bucketName string, prefixes string, suffix string) error {
	client, err := NewClient()
	if err != nil {
		return err
	}
	bucket := client.s3.Bucket(bucketName)
	if bucket == nil {
		return fmt.Errorf("Bucket %s not found", bucketName)
	}

	var sections []Section
	for _, prefix := range strings.Split(prefixes, ",") {
		resp, err := bucket.List(prefix, "", "", 0)
		if err != nil {
			return err
		}

		releases := loadReleases(resp.Contents, bucketName, prefix, suffix, 20)
		if len(releases) > 0 {
			log.Printf("Found %d release(s) at %s\n", len(releases), prefix)
			for _, release := range releases {
				log.Printf(" %s %s %s\n", release.Name, release.Version, release.DateString)
			}
		}
		sections = append(sections, Section{
			Header:   prefix,
			Releases: releases,
		})
	}

	return WriteHTMLForLinks(path, bucketName, sections)
}

var htmlTemplate = `
<!doctype html>

  <title>{{ .Title }}</title>

	{{ range $index, $sec := .Sections }}

{{ $sec.Header }}

		{{ range $index2, $rel := $sec.Releases }}

{{ $rel.Name }} {{ $rel.Version }} {{ $rel.Date }} {{ $rel.Commit }}

		{{ end }}

	{{ end }}

`

func WriteHTMLForLinks(path string, title string, sections []Section) error {
	vars := map[string]interface{}{
		"Title":    title,
		"Sections": sections,
	}

	t, err := template.New("t").Parse(htmlTemplate)
	if err != nil {
		return err
	}

	if path != "" {
		var data bytes.Buffer
		err = t.Execute(&data, vars)
		if err != nil {
			return err
		}
		err := makeParentDirs(path)
		if err != nil {
			return err
		}
		return ioutil.WriteFile(path, data.Bytes(), 0644)
	}
	return nil
}

type Platform struct {
	Name          string
	Prefix        string
	PrefixSupport string
	Suffix        string
	LatestName    string
}

func CopyLatest(bucketName string) error {
	client, err := NewClient()
	if err != nil {
		return err
	}
	return client.CopyLatest(bucketName)
}

func Platforms() []Platform {
	return []Platform{
		Platform{Name: "darwin", Prefix: "darwin/", PrefixSupport: "darwin-support/", LatestName: "Keybase.dmg"},
		Platform{Name: "deb", Prefix: "linux_binaries/deb/", Suffix: "_amd64.deb", LatestName: "keybase_amd64.deb"},
		Platform{Name: "rpm", Prefix: "linux_binaries/rpm/", Suffix: ".x86_64.rpm", LatestName: "keybase_amd64.rpm"},
		Platform{Name: "windows", Prefix: "windows/", Suffix: ".386.exe", LatestName: "keybase_setup_386.exe"},
	}
}

func FindPlatform(name string) *Platform {
	platforms := Platforms()
	for _, p := range platforms {
		if p.Name == name {
			return &p
		}
	}
	return nil
}

func (p *Platform) FindRelease(bucket s3.Bucket, f func(r Release) bool) (*Release, error) {
	resp, err := bucket.List(p.Prefix, "", "", 0)
	if err != nil {
		return nil, err
	}
	releases := loadReleases(resp.Contents, bucket.Name, p.Prefix, p.Suffix, 0)
	for _, release := range releases {
		k := release.Key
		if !strings.HasSuffix(k.Key, p.Suffix) {
			continue
		}
		if f(release) {
			return &release, nil
		}
	}
	return nil, nil
}

func (c *Client) CopyLatest(bucketName string) error {
	bucket := c.s3.Bucket(bucketName)

	platforms := Platforms()

	for _, platform := range platforms {
		release, err := platform.FindRelease(*bucket, func(r Release) bool { return true })
		if err != nil {
			return err
		}
		if release == nil {
			continue
		}
		k := release.Key
		url, _ := urlStringForKey(k, bucketName, platform.Prefix)
		// Instead of linking, we're making copies. S3 linking has some issues.
		// headers := map[string][]string{
		// 	"x-amz-website-redirect-location": []string{url},
		// }
		//err = bucket.PutHeader(name, []byte{}, headers, s3.PublicRead)
		log.Printf("Copying %s from %s (latest)\n", platform.LatestName, k.Key)
		_, err = bucket.PutCopy(platform.LatestName, s3.PublicRead, s3.CopyOptions{}, url)
		if err != nil {
			return err
		}
	}
	return nil
}

func (c *Client) CurrentUpdate(bucketName string, platform string, env string) (currentUpdate *keybase1.Update, err error) {
	bucket := c.s3.Bucket(bucketName)

	data, err := bucket.Get(fmt.Sprintf("update-%s-%s.json", platform, env))
	if err != nil {
		return
	}
	currentUpdate, err = update.DecodeJSON(data)
	return
}

func PromoteRelease(bucketName string, delay time.Duration, hourEastern int, channel string, platform string, env string) (*Release, error) {
	client, err := NewClient()
	if err != nil {
		return nil, err
	}
	return client.PromoteRelease(bucketName, delay, hourEastern, channel, platform, env)
}

func updateJSONName(channel string, platformName string, env string) string {
	if channel == "" {
		return fmt.Sprintf("update-%s-%s.json", platformName, env)
	}
	return fmt.Sprintf("update-%s-%s-%s.json", platformName, env, channel)
}

func (c *Client) PromoteRelease(bucketName string, delay time.Duration, hourEastern int, channel string, platformName string, env string) (*Release, error) {
	if channel == "" {
		log.Printf("Finding release to promote (%s delay)", delay)
	} else {
		log.Printf("Finding release to promote for %s channel (%s delay)", channel, delay)
	}
	bucket := c.s3.Bucket(bucketName)

	platform := FindPlatform(platformName)
	if platform == nil {
		return nil, fmt.Errorf("Unsupported platform")
	}
	release, err := platform.FindRelease(*bucket, func(r Release) bool {
		if delay != 0 && time.Since(r.Date) < delay {
			return false
		}
		hour, min, _ := r.Date.Clock()
		if hourEastern != 0 && hour < hourEastern && min < 15 {
			return false
		}
		return true
	})
	if err != nil {
		return nil, err
	}

	if release == nil {
		return nil, nil
	}
	log.Printf("Found release %s (%s), %s", release.Name, time.Since(release.Date), release.Version)

	jsonName := updateJSONName(channel, platformName, env)
	jsonURL := urlString(bucketName, platform.PrefixSupport, fmt.Sprintf("update-%s-%s-%s.json", platformName, env, release.Version))
	log.Printf("Promoting %s", jsonURL)
	_, err = bucket.PutCopy(jsonName, s3.PublicRead, s3.CopyOptions{}, jsonURL)
	if err != nil {
		return nil, err
	}
	return release, nil
}
<|endoftext|>


<commit_before><commit_msg>Make sure to propagate scanner errors.<commit_after><|endoftext|>