diff --git a/assert.go b/assert/assert.go similarity index 58% rename from assert.go rename to assert/assert.go index d4ce1e0..9e13d14 100755 --- a/assert.go +++ b/assert/assert.go @@ -1,66 +1,65 @@ -package oxyde +package assert import ( "fmt" + "github.com/wisbery/oxyde/common" "reflect" ) -// AssertNil function asserts that actual value is nil. -// When actual value is not nil, an error is reported. -func AssertNil(actual interface{}) { - if !isNil(actual) { +func Nil(actual interface{}) { + if !common.NilValue(actual) { displayAssertionError(nil, actual) } } -func AssertNotNil(actual interface{}) { - if isNil(actual) { +func NotNil(actual interface{}) { + if reflect.ValueOf(actual).IsNil() { displayAssertionError("not nil", actual) } } -func AssertNilError(e error) { +func NilError(e error) { if e != nil { displayAssertionError(nil, e) } } -func AssertNilString(actual *string) { +func NilString(actual *string) { if actual != nil { displayAssertionError(nil, actual) } } -func AssertNotNilString(actual *string) { +func NotNilString(actual *string) { if actual == nil { displayAssertionError("not nil", actual) } } -func AssertTrue(actual bool) { +func True(actual bool) { if !actual { displayAssertionError(true, actual) } } -func AssertFalse(actual bool) { +func False(actual bool) { if actual { displayAssertionError(false, actual) } } -func AssertNotNilId(actual *string) { - AssertNotNilString(actual) - AssertEqualInt(36, len(*actual)) +func NotNilId(actual *string) { + NotNilString(actual) + EqualInt(36, len(*actual)) } -func AssertEqualString(expected string, actual string) { +func EqualString(expected string, actual string) { if !equalString(expected, actual) { displayAssertionError(expected, actual) } } -func AssertEqualStringNullable(expected *string, actual *string) { +func EqualStringNullable(expected *string, actual *string) { if !equalStringNullable(expected, actual) { if expected != nil && actual != nil { displayAssertionError(*expected, *actual) @@ -69,19 +68,19 @@ func AssertEqualStringNullable(expected *string, actual *string) { } } -func AssertNilInt(actual *int) { +func NilInt(actual *int) { if actual != nil { displayAssertionError(nil, actual) } } -func AssertEqualInt(expected int, actual int) { +func EqualInt(expected int, actual int) { if !equalInt(expected, actual) { displayAssertionError(expected, actual) } } -func AssertEqualIntNullable(expected *int, actual *int) { +func EqualIntNullable(expected *int, actual *int) { if !equalIntNullable(expected, actual) { if expected != nil && actual != nil { displayAssertionError(*expected, *actual) @@ -90,32 +89,18 @@ func AssertEqualIntNullable(expected *int, actual *int) { } } -func AssertEqualInt64Nullable(expected *int64, actual *int64) { - if !equalInt64Nullable(expected, actual) { - if expected != nil && actual != nil { - displayAssertionError(*expected, *actual) - } - displayAssertionError(expected, actual) - } -} - -func AssertEqualFloat64(expected float64, actual float64) { +func EqualFloat64(expected float64, actual float64) { if !equalFloat64(expected, actual) { displayAssertionError(expected, actual) } } -func AssertEqualBool(expected bool, actual bool) { +func EqualBool(expected bool, actual bool) { if !equalBool(expected, actual) { displayAssertionError(expected, actual) } } -// Function isNil checks if the value specified as parameter is nil. -func isNil(value interface{}) bool { - return value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) -} - // Function equalString checks if two string values are equal. func equalString(expected string, actual string) bool { return expected == actual @@ -148,22 +133,6 @@ func equalIntNullable(expected *int, actual *int) bool { return true } -// Function equalInt64 checks if two int64 values are equal. -func equalInt64(expected int64, actual int64) bool { - return expected == actual -} - -// Function equalInt64Nullable checks if two pointers to int64 values are equal. -func equalInt64Nullable(expected *int64, actual *int64) bool { - if expected != nil && actual != nil { - return equalInt64(*expected, *actual) - } - if expected != nil || actual != nil { - return false - } - return true -} - // Function equalFloat64 checks if two float64 values are equal. func equalFloat64(expected float64, actual float64) bool { return expected == actual @@ -176,11 +145,11 @@ func equalBool(expected bool, actual bool) bool { // Function displayAssertionError displays assertion error details. func displayAssertionError(expected interface{}, actual interface{}) { - separator := makeText("-", 120) + separator := common.MakeString('-', 120) fmt.Printf("\n\n%s\n> ERROR: assertion error\n> Expected: %+v\n> Actual: %+v\n%s\n\n", separator, expected, actual, separator) - brexit() + common.BrExit() } diff --git a/assert_test.go b/assert/assert_test.go similarity index 96% rename from assert_test.go rename to assert/assert_test.go index 812c23f..99867a3 100755 --- a/assert_test.go +++ b/assert/assert_test.go @@ -1,8 +1,6 @@ -package oxyde +package assert -import ( - "testing" -) +import "testing" func TestEqualStrings(t *testing.T) { if !equalString("string", "string") { diff --git a/common/utils.go b/common/utils.go new file mode 100755 index 0000000..531dfb1 --- /dev/null +++ b/common/utils.go @@ -0,0 +1,103 @@ +package common + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "github.com/google/uuid" + "os" + "reflect" + "regexp" + "runtime" +) + +const ( + ApiTagName = "api" // Name of the tag in which documentation details are stored. + JsonTagName = "json" // Name of the tag in which JSON details are stored. + OptionalPrefix = "?" // Prefix used to mark th field as optional. +) + +// Function MakeString creates a string of length 'len' containing the same character 'ch'. +func MakeString(ch byte, len int) string { + b := make([]byte, len) + for i := 0; i < len; i++ { + b[i] = ch + } + return string(b) +} + +// Function NilValue checks if the value specified as parameter is nil. +func NilValue(value interface{}) bool { + return value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) +} + +// Function PrettyPrint takes JSON string as an argument +// and returns the same JSON but pretty-printed. +func PrettyPrint(in []byte) string { + var out bytes.Buffer + err := json.Indent(&out, in, "", " ") + if err != nil { + return string(in) + } + return out.String() +} + +// Function PanicOnError panics when the error passed as argument is not nil. +func PanicOnError(err error) { + if err != nil { + panic(err) + } +} + +// Function GenerateId generated unique identifier in form of UUID string. +func GenerateId() string { + id, err := uuid.NewRandom() + PanicOnError(err) + return id.String() +} + +// Function TypeOfValue returns the type of value specified as parameter. +// If specified value is a pointer, then is first dereferenced. +func TypeOfValue(value interface{}) reflect.Type { + t := reflect.TypeOf(value) + if t.Kind().String() == "ptr" { + v := reflect.ValueOf(value) + t = reflect.Indirect(v).Type() + } + return t +} + +// Function ValueOfValue returns the value of specified parameter. +// If specified value is a pointer, then is first dereferenced. +func ValueOfValue(value interface{}) reflect.Value { + t := reflect.TypeOf(value) + if t.Kind().String() == "ptr" { + v := reflect.ValueOf(value) + return reflect.Indirect(v) + } + return reflect.ValueOf(value) +} + +// Function BrExit breaks the execution of test and displays stack trace. +// After breaking the execution flow, application returns exit code -1 +// that can be utilized by test automation tools. +func BrExit() { + fmt.Printf("Stack trace:\n------------\n") + reDeepCalls := regexp.MustCompile(`(^goroutine[^:]*:$)|(^.*/oxyde/.*$)`) + reFuncParams := regexp.MustCompile(`([a-zA-Z_0-9]+)\([^\)]+\)`) + reFuncOffset := regexp.MustCompile(`\s+\+.*$`) + b := make([]byte, 100000) + runtime.Stack(b, false) + scanner := bufio.NewScanner(bytes.NewBuffer(b)) + for scanner.Scan() { + line := scanner.Text() + if reDeepCalls.MatchString(line) { + continue + } + line = reFuncParams.ReplaceAllString(line, "$1()") + line = reFuncOffset.ReplaceAllString(line, "") + fmt.Println(line) + } + os.Exit(-1) +} diff --git a/doc/doc.go b/doc/doc.go new file mode 100755 index 0000000..ca498b2 --- /dev/null +++ b/doc/doc.go @@ -0,0 +1,311 @@ +package doc + +import ( + "errors" + "fmt" + "github.com/wisbery/oxyde/common" + "reflect" + "strings" +) + +const ( + CollectNone = iota + CollectDescription + CollectExamples + CollectAll +) + +const ( + AccessGranted = iota + AccessDenied + AccessUnknown + AccessError +) + +type RoleKey struct { + method string + path string + roleName string +} + +type Context struct { + mode int // Documentation collecting data mode. + exampleSummary string // Summary for the next collected example. + exampleDescription string // Detailed description for the next collected example. + roleName string // Role name of the principal for the next endpoint example. + endpoint *Endpoint // Currently documented endpoint data. + endpoints []Endpoint // List of documented endpoints. + roleNames []string // Role names in order they should be displayed. + roles map[RoleKey]int // Map of access roles tested for endpoints. +} + +func CreateDocContext() *Context { + return &Context{ + mode: CollectNone, + endpoint: nil, + endpoints: make([]Endpoint, 0), + roles: make(map[RoleKey]int)} +} + +func (dc *Context) ClearDocumentation() { + dc.mode = CollectNone + dc.endpoint = nil + dc.endpoints = make([]Endpoint, 0) + dc.roles = make(map[RoleKey]int) +} + +func (dc *Context) PublishDocumentation() { + for _, endpoint := range dc.endpoints { + PrintEndpoint(endpoint) + } +} + +func (dc *Context) NewEndpointDocumentation(id string, tag string, summary string) { + if id == "" { + id = common.GenerateId() + } + dc.endpoint = &Endpoint{ + Id: id, + Summary: summary} + dc.endpoint.AddTag(tag) +} + +func (dc *Context) GetEndpoint() *Endpoint { + return dc.endpoint +} + +func (dc *Context) CollectDescription() { + dc.mode = CollectDescription +} + +func (dc *Context) CollectDescriptionMode() bool { + return dc.mode == CollectDescription || dc.mode == CollectAll +} + +func (dc *Context) CollectExamples(exampleSummary string, exampleDescription string) { + exampleSummary = strings.TrimSpace(exampleSummary) + exampleDescription = strings.TrimSpace(exampleDescription) + if exampleSummary != "" || exampleDescription != "" { + dc.mode = CollectExamples + dc.exampleSummary = exampleSummary + dc.exampleDescription = exampleDescription + } +} + +func (dc *Context) CollectRole(roleName string) { + dc.roleName = roleName +} + +func (dc *Context) CollectExamplesMode() bool { + return dc.mode == CollectExamples || dc.mode == CollectAll +} + +func (dc *Context) CollectAll(exampleSummary string) { + dc.mode = CollectAll + dc.exampleSummary = exampleSummary +} + +func (dc *Context) StopCollecting() { + dc.mode = CollectNone + dc.exampleSummary = "" + dc.roleName = "" +} + +func (dc *Context) SetRolesOrder(roleOrder []string) { + dc.roleNames = roleOrder +} + +func (dc *Context) SaveRole(method string, path string, status int) { + if dc.roleName != "" { + key := RoleKey{method: method, path: path, roleName: dc.roleName} + switch status { + case 200: + dc.roles[key] = AccessGranted + case 401: + dc.roles[key] = AccessDenied + default: + dc.roles[key] = AccessError + } + } +} + +func (dc *Context) GetRoleNames() []string { + return dc.roleNames +} + +func (dc *Context) GetAccess(method string, path string, roleName string) int { + key := RoleKey{ + method: method, + path: path, + roleName: roleName} + if access, ok := dc.roles[key]; ok { + return access + } else { + return AccessUnknown + } +} + +func (dc *Context) SaveEndpointDocumentation() { + if dc.endpoint != nil { + dc.endpoints = append(dc.endpoints, *dc.endpoint) + } +} + +func (dc *Context) GetEndpoints() []Endpoint { + return dc.endpoints +} + +func (dc *Context) GetExampleSummary() string { + return dc.exampleSummary +} + +func (dc *Context) GetExampleDescription() string { + return dc.exampleDescription +} + +type Endpoint struct { + Id string // Unique endpoint identifier. + Tags []string // List of tags of endpoint. + Method string // HTTP method name, like GET, POST, PUT or DELETE. + UrlRoot string // Request URL root. + UrlPath string // Request URL path after root. + Summary string // Summary text describing endpoint. + Parameters []Field // Description of request parameters. + RequestBody []Field // Description of request body. + ResponseBody []Field // Description of results. + Examples []Example // Description of usage examples. +} + +func (e *Endpoint) AddTag(tag string) { + if e.Tags == nil { + e.Tags = make([]string, 0) + } + e.Tags = append(e.Tags, tag) +} + +type Field struct { + JsonName string // Name of the field in JSON. + JsonType string // Type of the field in JSON. + Mandatory bool // Flag indicating if field is mandatory in JSON. + Description string // Description of the field. + Children []Field // List of child fields (may be empty). +} + +func CreateField(typ reflect.Type, structField reflect.StructField) Field { + jsonType := jsonType(typ) + jsonName := structField.Tag.Get(common.JsonTagName) + apiTagContent := structField.Tag.Get(common.ApiTagName) + mandatory := !strings.HasPrefix(apiTagContent, common.OptionalPrefix) + apiTagContent = strings.TrimPrefix(apiTagContent, common.OptionalPrefix) + return Field{ + JsonName: jsonName, + JsonType: jsonType, + Mandatory: mandatory, + Description: apiTagContent, + Children: make([]Field, 0)} +} + +type Example struct { + Summary string // Example summary. + Description string // Detailed example description. + Method string // HTTP method name. + Uri string // Request URI. + StatusCode int // HTTP status code. + RequestBody string // Request body as JSON string. + ResponseBody string // Response body as JSON string. +} + +func ParseObject(o interface{}) []Field { + typ := reflect.TypeOf(o) + return ParseFields(typ) +} + +func ParseFields(typ reflect.Type) []Field { + switch typ.Kind() { + case reflect.Ptr: + return ParseFields(typ.Elem()) + case reflect.Struct: + fields := make([]Field, 0) + for i := 0; i < typ.NumField(); i++ { + childField := typ.Field(i) + childType := childField.Type + field := CreateField(childType, childField) + switch field.JsonType { + case "object": + field.Children = append(field.Children, ParseFields(childType)...) + case "array": + field.Children = append(field.Children, ParseFields(childType.Elem())...) + } + fields = append(fields, field) + } + return fields + } + return []Field{} +} + +func jsonType(typ reflect.Type) string { + switch typ.Kind() { + case reflect.Ptr: + return jsonType(typ.Elem()) + case reflect.Struct: + return "object" + case reflect.Slice: + return "array" + case reflect.String: + return "string" + case reflect.Bool: + return "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return "number" + default: + panic(errors.New("unsupported type: " + typ.Kind().String())) + } +} + +func PrintEndpoint(endpoint Endpoint) { + fmt.Printf("\n\n%s\n%s\n\n", endpoint.Method, endpoint.UrlRoot) + fmt.Println("Parameters:") + fmt.Println(common.MakeString('-', 120)) + PrintFields(endpoint.RequestBody, " ", 0) + fmt.Println() + fmt.Println(common.MakeString('-', 120)) + fmt.Printf("\n") + fmt.Println("ResponseBody:") + fmt.Println(common.MakeString('-', 120)) + PrintFields(endpoint.ResponseBody, " ", 0) + fmt.Println() + fmt.Println(common.MakeString('-', 120)) + fmt.Printf("\n") + for _, usage := range endpoint.Examples { + PrintExample(usage) + } +} + +func PrintFields(fields []Field, indent string, level int) { + for i, field := range fields { + nameText := indent + field.JsonName + nameText = nameText + strings.Repeat(" ", 30-len(nameText)) + typeText := field.JsonType + typeText = typeText + strings.Repeat(" ", 20-len(typeText)) + mandatoryText := "N" + if field.Mandatory { + mandatoryText = "Y" + } + if i > 0 || level > 0 { + fmt.Println() + } + fmt.Printf("|%s | %s | %s | %s", nameText, typeText, mandatoryText, field.Description) + if field.JsonType == "object" || field.JsonType == "array" { + PrintFields(field.Children, indent+indent, level+1) + } + } +} + +func PrintExample(usage Example) { + fmt.Printf("\nExample:\n") + fmt.Printf("%d %s %s\n", usage.StatusCode, usage.Method, usage.Uri) + fmt.Printf("Parameters:\n%s\n", usage.RequestBody) + fmt.Printf("ResponseBody:\n%s\n", usage.ResponseBody) +} diff --git a/doc_context_test.go b/doc/doc_test.go similarity index 91% rename from doc_context_test.go rename to doc/doc_test.go index ec6b947..1513224 100755 --- a/doc_context_test.go +++ b/doc/doc_test.go @@ -1,4 +1,4 @@ -package oxyde +package doc import ( "errors" @@ -14,7 +14,7 @@ type TestLoginParams struct { func TestSimpleJSONObject(t *testing.T) { - fields := ParseType(TestLoginParams{}) + fields := ParseObject(TestLoginParams{}) if len(fields) != 2 { t.Error("expected two fields in object") } @@ -93,7 +93,7 @@ func TestA(t *testing.T) { SubDetails: nil, Tags: nil} fmt.Println("---d") - ParseType(s) + ParseObject(s) // Traverse(&s) fmt.Println() } @@ -105,10 +105,11 @@ func TestSimpleTypes(t *testing.T) { Salary int64 `json:"salary" api:"Salary."` Married bool `json:"married" api:"Is married?"` Height float64 `json:"height" api:"Height."` - Tags []string `json:"tags" api:"Group."` + Tags []string `json:"tags" api:"Tags."` } d := Data{} - ParseType(d) + fields := ParseObject(d) + PrintFields(fields, " ", 0) fmt.Println() } @@ -127,11 +128,12 @@ func TestStructures(t *testing.T) { Salary int64 `json:"salary" api:"Salary."` Married *bool `json:"married" api:"Is married?"` Height float64 `json:"height" api:"Height."` - Tags []string `json:"tags" api:"Group."` + Tags []string `json:"tags" api:"Tags."` Children []Child `json:"children" api:"Children."` Address *Address `json:"address" api:"Address details."` } d := &Data{} - ParseType(d) + fields := ParseObject(d) + PrintFields(fields, " ", 0) fmt.Println() } diff --git a/doc_context.go b/doc_context.go deleted file mode 100755 index 67e49ea..0000000 --- a/doc_context.go +++ /dev/null @@ -1,149 +0,0 @@ -package oxyde - -import ( - "strings" -) - -const ( - CollectNothing = iota // No documentation data will be collected. - CollectEndpointDescription // Endpoint description will be collected. - CollectEndpointUsageExample // An example of endpoint usage will be collected. - CollectEndpointDescriptionAndUsage // Endpoint description and usage example will be collected. -) - -type DocContext struct { - mode int // Documentation collecting data mode. - usageSummary string // Summary for the next collected usage example. - usageDescription string // Detailed description for the next collected usage example. - roleName string // Principal's role name for the next endpoint usage example. - endpoint *DocEndpoint // Pointer to currently documented endpoint. - endpoints []DocEndpoint // List of all documented endpoints with usage examples. - roleNames []string // Role names in order they should be displayed in preview. - roles roles // Map of verified access roles for all endpoints. -} - -func CreateDocContext() *DocContext { - return &DocContext{ - mode: CollectNothing, - usageSummary: "", - usageDescription: "", - roleName: "", - endpoint: nil, - endpoints: make([]DocEndpoint, 0), - roles: make(roles)} -} - -func (dc *DocContext) Clear() { - dc.mode = CollectNothing - dc.usageSummary = "" - dc.usageDescription = "" - dc.roleName = "" - dc.endpoint = nil - dc.endpoints = make([]DocEndpoint, 0) - dc.roles = make(map[roleKey]int) -} - -func (dc *DocContext) NewEndpoint(version string, group string, summary string, description string) { - if version == "" { - version = "v1" - } - summary = strings.TrimSpace(summary) - description = strings.TrimSpace(description) - dc.endpoint = createEndpoint(group, version, summary, description) -} - -func (dc *DocContext) GetEndpoint() *DocEndpoint { - return dc.endpoint -} - -func (dc *DocContext) CollectDescription() { - dc.mode = CollectEndpointDescription -} - -func (dc *DocContext) CollectDescriptionMode() bool { - return dc.mode == CollectEndpointDescription || dc.mode == CollectEndpointDescriptionAndUsage -} - -func (dc *DocContext) CollectUsage(summary string, description string) { - summary = strings.TrimSpace(summary) - description = strings.TrimSpace(description) - if summary != "" || description != "" { - dc.mode = CollectEndpointUsageExample - dc.usageSummary = summary - dc.usageDescription = description - } -} - -func (dc *DocContext) CollectRole(roleName string) { - dc.roleName = roleName -} - -func (dc *DocContext) CollectExamplesMode() bool { - return dc.mode == CollectEndpointUsageExample || dc.mode == CollectEndpointDescriptionAndUsage -} - -func (dc *DocContext) CollectAll(summary string, description string) { - dc.mode = CollectEndpointDescriptionAndUsage - dc.usageSummary = summary - dc.usageDescription = description -} - -func (dc *DocContext) StopCollecting() { - dc.mode = CollectNothing - dc.usageSummary = "" - dc.usageDescription = "" - dc.roleName = "" -} - -func (dc *DocContext) SetRolesOrder(roleOrder []string) { - dc.roleNames = roleOrder -} - -func (dc *DocContext) SaveRole(method string, path string, status int) { - if dc.roleName != "" { - key := roleKey{method: method, path: path, roleName: dc.roleName} - switch status { - case 200: - dc.roles[key] = AccessGranted - case 401: - dc.roles[key] = AccessDenied - default: - dc.roles[key] = AccessError - } - } -} - -func (dc *DocContext) GetRoleNames() []string { - return dc.roleNames -} - -func (dc *DocContext) GetAccess(method string, path string, roleName string) int { - key := roleKey{ - method: method, - path: path, - roleName: roleName} - if access, ok := dc.roles[key]; ok { - return access - } else { - return AccessUnknown - } -} - -func (dc *DocContext) SaveEndpoint() { - if dc.endpoint != nil { - dc.endpoints = append(dc.endpoints, *dc.endpoint) - dc.endpoint = nil - } -} - -func (dc *DocContext) GetEndpoints() []DocEndpoint { - return dc.endpoints -} - -func (dc *DocContext) GetExampleSummary() string { - return dc.usageSummary -} - -func (dc *DocContext) GetExampleDescription() string { - return dc.usageDescription -} diff --git a/doc_model.go b/doc_model.go deleted file mode 100755 index d805115..0000000 --- a/doc_model.go +++ /dev/null @@ -1,196 +0,0 @@ -package oxyde - -import ( - "errors" - "fmt" - "reflect" - "strings" -) - -const ( - AccessGranted = iota // Access to endpoint for selected role is granted. - AccessDenied // Access to endpoint for selected role is denied. - AccessUnknown // No info about endpoint access rights for selected role. - AccessError // Error occurred during collecting access rights for selected role. -) - -// Properties of documented API endpoint. -type DocEndpoint struct { - Id string // Unique endpoint identifier. - Group string // Name of the group this endpoint belongs to. - Version string // API version number for endpoint. - Method string // HTTP method name, like GET, POST, PUT or DELETE. - RootPath string // Request root path. - RequestPath string // Request path after root path. - Summary string // Endpoint summary. - Description string // Endpoint detailed description. - Headers []Field // Description of request headers. - Parameters []Field // Description of request parameters. - RequestBody []Field // Description of request body. - ResponseBody []Field // Description of response body. - Usages []Usage // Description of usage examples. -} - -// Function createEndpoint creates new API endpoint. -func createEndpoint(group string, version string, summary string, description string) *DocEndpoint { - return &DocEndpoint{ - Id: generateId(), - Group: group, - Version: version, - Summary: summary, - Description: description} -} - -type Field struct { - FieldName string // Field name in struct or array. - FieldType string // Type of field in struct or array. - JsonName string // Name of the field in JSON object. - JsonType string // Type of the field in JSON object. - Mandatory bool // Flag indicating if field is mandatory in JSON object. - Recursive bool // Flag indicating if this field is used recursively. - Description string // Description of the field. - Children []Field // List of child fields (may be empty). -} - -func ParseType(i interface{}) []Field { - typ := reflect.TypeOf(i) - return ParseFields(typ, "") -} - -func ParseFields(typ reflect.Type, parentType string) []Field { - typeName := typ.Name() - switch typ.Kind() { - case reflect.Ptr: - return ParseFields(typ.Elem(), "") - case reflect.Struct: - fields := make([]Field, 0) - for i := 0; i < typ.NumField(); i++ { - childField := typ.Field(i) - childType := childField.Type - field := createField(childType, childField) - switch field.JsonType { - case "object": - if parentType != typeName || typeName != field.FieldType { - field.Children = append(field.Children, ParseFields(childType, typeName)...) - } else { - field.Recursive = true - } - case "array": - if parentType != typeName || typeName != field.FieldType { - field.Children = append(field.Children, ParseFields(childType.Elem(), typeName)...) - } else { - field.Recursive = true - } - } - fields = append(fields, field) - } - return fields - } - return []Field{} -} - -func jsonType(typ reflect.Type) (string, string) { - switch typ.Kind() { - case reflect.Ptr: - return jsonType(typ.Elem()) - case reflect.Struct: - return "object", typ.Name() - case reflect.Slice: - return "array", typ.Elem().Name() - case reflect.String: - return "string", typ.Name() - case reflect.Bool: - return "boolean", typ.Name() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64: - return "number", typ.Name() - default: - panic(errors.New("unsupported type: " + typ.Kind().String())) - } -} - -func createField(typ reflect.Type, structField reflect.StructField) Field { - fieldName := structField.Name - jsonType, fieldType := jsonType(typ) - jsonName := structField.Tag.Get(JsonTagName) - apiTagContent := structField.Tag.Get(ApiTagName) - mandatory := !strings.HasPrefix(apiTagContent, OptionalPrefix) - apiTagContent = strings.TrimPrefix(apiTagContent, OptionalPrefix) - return Field{ - FieldName: fieldName, - FieldType: fieldType, - JsonName: jsonName, - JsonType: jsonType, - Mandatory: mandatory, - Recursive: false, - Description: apiTagContent, - Children: make([]Field, 0)} -} - -type Usage struct { - Summary string // Usage summary. - Description string // Usage detailed description. - Method string // HTTP method name. - Headers headers // Request headers. - Url string // Request URL. - RequestBody string // Request body as JSON string. - ResponseBody string // Response body as JSON string. - StatusCode int // HTTP status code. -} - -// Type headers is a map that defines names and values of HTTP request headers. -// Keys are header names and values are header values. This is a convenient way -// to pass any number of headers to functions that call REST endpoints. -type headers map[string]string - -// Function parseHeaders traverses the interface given in parameter and retrieves -// names and values of request headers. All request headers required in endpoint call -// should be defined as a struct having string fields (or pointers to strings). -// Each field in such a struct should have a tag named 'json' with the name of the header. -// This way allows to define and document headers and pass header values in one -// single (and simple) structure. -func parseHeaders(any interface{}) headers { - headersMap := make(headers) - if any == nil { - return headersMap - } - typ := reflect.TypeOf(any) - value := reflect.ValueOf(any) - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - if value.IsNil() || !value.IsValid() { - return headersMap - } - value = reflect.Indirect(value) - } - if typ.Kind() != reflect.Struct { - return headersMap - } - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - fieldType := field.Type - fieldValue := value.Field(i) - if fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - if fieldValue.IsNil() { - continue - } - fieldValue = reflect.Indirect(fieldValue) - } - if fieldType.Kind() != reflect.String { - continue - } - fieldName := field.Tag.Get(JsonTagName) - headersMap[fieldName] = fmt.Sprintf("%s", fieldValue) - } - return headersMap -} - -type roleKey struct { - method string // HTTP method name. - path string // Request path. - roleName string // Name of the role. -} - -type roles map[roleKey]int diff --git a/doc_model_test.go b/doc_model_test.go deleted file mode 100755 index fcd452c..0000000 --- a/doc_model_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package oxyde - -import ( - "fmt" - "testing" -) - -func TestHeadersNil(t *testing.T) { - out := parseHeaders(nil) - assertTestResultEmptyHeadersMap(t, out) -} - -func TestHeadersNilPointerToStruct(t *testing.T) { - type Headers struct { - Authorization string - } - type PHeaders = *Headers - var in PHeaders - out := parseHeaders(in) - assertTestResultEmptyHeadersMap(t, out) -} - -func TestHeadersString(t *testing.T) { - in := "Authorization" - out := parseHeaders(in) - assertTestResultEmptyHeadersMap(t, out) -} - -func TestHeadersStruct(t *testing.T) { - type Headers struct { - H1 string `json:"h1"` - H2 string `json:"h2"` - H3 string `json:"h3"` - } - in := Headers{ - H1: "v1", - H2: "v2", - H3: "v3"} - out := parseHeaders(in) - assertTestResultHeaders3(t, out, in.H1, in.H2, in.H3) -} - -func TestHeadersPointerToStruct(t *testing.T) { - type Headers struct { - H1 string `json:"h1"` - H2 string `json:"h2"` - H3 string `json:"h3"` - } - in := Headers{ - H1: "v1", - H2: "v2", - H3: "v3"} - out := parseHeaders(&in) - assertTestResultHeaders3(t, out, in.H1, in.H2, in.H3) -} - -func TestHeadersStructWithPointers(t *testing.T) { - type Headers struct { - H1 *string `json:"h1"` - H2 *string `json:"h2"` - H3 *string `json:"h3"` - } - v1 := "v1" - v2 := "v2" - v3 := "v3" - in := Headers{ - H1: &v1, - H2: &v2, - H3: &v3} - out := parseHeaders(in) - assertTestResultHeaders3(t, out, *in.H1, *in.H2, *in.H3) -} - -func TestHeadersStructWithPointersAndNilValues(t *testing.T) { - type Headers struct { - H1 *string `json:"h1"` - H2 *string `json:"h2"` - H3 *string `json:"h3"` - } - v1 := "v1" - v3 := "v3" - in := Headers{ - H1: &v1, - H2: nil, - H3: &v3} - out := parseHeaders(in) - if len(out) != 2 { - t.Error(fmt.Sprintf("expected exactly 2 headers, but %d found", len(out))) - } - key := "h1" - if value, ok := out[key]; !ok || value != *in.H1 { - t.Error(fmt.Sprintf("expected key '%s' with value '%s' not found", key, *in.H1)) - } - key = "h2" - if value, ok := out[key]; ok { - t.Error(fmt.Sprintf("expected no key '%s', but value '%s' found", key, value)) - } - key = "h3" - if value, ok := out[key]; !ok || value != *in.H3 { - t.Error(fmt.Sprintf("expected key '%s' with value '%s' not found", key, *in.H3)) - } -} - -func assertTestResultEmptyHeadersMap(t *testing.T, actual headers) { - if len(actual) != 0 { - t.Error(fmt.Sprintf("expected empty map, found: %+v", actual)) - } -} - -func assertTestResultHeaders3(t *testing.T, actual headers, h1, h2, h3 string) { - if len(actual) != 3 { - t.Error(fmt.Sprintf("expected 3 headers, found %d", len(actual))) - } - key := "h1" - if value, ok := actual[key]; !ok || value != h1 { - t.Error(fmt.Sprintf("expected key '%s' with value '%s', not found", key, h1)) - } - key = "h2" - if value, ok := actual[key]; !ok || value != h2 { - t.Error(fmt.Sprintf("expected key '%s' with value '%s' not found", key, h2)) - } - key = "h3" - if value, ok := actual[key]; !ok || value != h3 { - t.Error(fmt.Sprintf("expected key '%s' with value '%s' not found", key, h3)) - } -} diff --git a/errors.go b/errors.go deleted file mode 100644 index 6e9f505..0000000 --- a/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package oxyde - -// JsonApiError is a JSON API implementation of an error. -type JsonApiError struct { - Status *string `json:"status" api:"The HTTP status code applicable to reported problem."` - Code *string `json:"code" api:"An application-specific error code."` - Title *string `json:"title" api:"A short, human-readable summary of the problem that never changed from occurrence to occurrence of the problem."` - Detail *string `json:"detail" api:"A human-readable explanation specific to the occurrence of the problem."` -} - -// JsonApiErrors is an array of JSON API errors. -type JsonApiErrors = []JsonApiError diff --git a/go.sum b/go.sum index baeff4a..a43f94d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/html_template_endpoint.go b/html/EndpointTemplate.go similarity index 73% rename from html_template_endpoint.go rename to html/EndpointTemplate.go index e842d23..e568b5d 100755 --- a/html_template_endpoint.go +++ b/html/EndpointTemplate.go @@ -1,39 +1,9 @@ -package oxyde +package html const EndpointTemplate = `
{{.Summary}}
{{.MethodUp}}
{{.UrlPath}}
-
{{.Description}}
- -
Headers
-
- {{if .Headers}} - - - - - - - - - - - {{range .Headers}} - - - - - - - {{end}} - -
NameTypeMandatoryDescription
{{.Name}}{{.Type}}{{.Mandatory}}{{.Description}}
- {{else}} -
(none)
- {{end}} -
-
Parameters
{{if .Parameters}} @@ -119,21 +89,15 @@ const EndpointTemplate = `
Examples
-{{range .Usages}} +{{range .Examples}}
{{.Summary}}
{{.Description}}
-
{{.MethodUp}}
-
{{.Url}}
+
{{.Method}}
+
{{.Uri}}
- {{range .Headers}} -
-
{{.Name}}
-
{{.Value}}
-
- {{end}} {{if .RequestBody}}
{{.RequestBody}}
{{end}} diff --git a/html_template_error.go b/html/ErrorTemplate.go similarity index 73% rename from html_template_error.go rename to html/ErrorTemplate.go index 4c14177..5a21dfd 100755 --- a/html_template_error.go +++ b/html/ErrorTemplate.go @@ -1,3 +1,3 @@ -package oxyde +package html const ErrorTemplate = `ERROR: {{.}}` diff --git a/html_template_index.go b/html/IndexTemplate.go similarity index 99% rename from html_template_index.go rename to html/IndexTemplate.go index 2671068..2b3c956 100755 --- a/html_template_index.go +++ b/html/IndexTemplate.go @@ -1,4 +1,4 @@ -package oxyde +package html const IndexTemplate = ` {{range .Groups}} diff --git a/html_template_page.go b/html/PageTemplate.go similarity index 98% rename from html_template_page.go rename to html/PageTemplate.go index 52718f9..2191301 100755 --- a/html_template_page.go +++ b/html/PageTemplate.go @@ -1,4 +1,4 @@ -package oxyde +package html const PageTemplate = ` diff --git a/html_template_style.go b/html/StyleCss.go similarity index 90% rename from html_template_style.go rename to html/StyleCss.go index 8f70285..a8dcf29 100755 --- a/html_template_style.go +++ b/html/StyleCss.go @@ -1,4 +1,4 @@ -package oxyde +package html const StyleCss = ` body { @@ -111,15 +111,6 @@ pre { margin: 10px 0 10px 0; } -.endpoint-details-description { - font-weight: normal; - font-size: 1.2em; - margin: 10px 0 10px 0; - padding: 10px 0 10px 0; - border-bottom: solid 1px gray; - width: 70%; -} - .fields-container-title { font-size: 1.3em; font-weight: bold; @@ -260,23 +251,6 @@ pre { max-width: 1000px; } -.example-header-container { - display: flex; - flex-direction: row; - margin-bottom: 8px; -} - -.example-header-name { - margin-right: 10px; - padding: 2px 8px 3px 8px; - color: white; - background-color: gray; - border-radius: 10px; -} - -.example-header-value { -} - .example-request { margin: 4px 0 8px 0; } diff --git a/model/model.go b/model/model.go new file mode 100755 index 0000000..0ce027f --- /dev/null +++ b/model/model.go @@ -0,0 +1,234 @@ +package model + +import ( + d "github.com/wisbery/oxyde/doc" + "sort" + "strings" +) + +var ( + HttpMethodOrder = map[string]int{"POST": 1, "PUT": 2, "GET": 3, "DELETE": 4} +) + +type Model struct { + Groups []Group // Groups of endpoints. + Endpoints []Endpoint // List of all endpoints in model. + EndpointsById map[string]*Endpoint // Pointers to endpoints by endpoint identifier. + RoleNames []string // List of tested role names for endpoints. +} + +func CreateModel(dc *d.Context) *Model { + // create model structure + model := Model{ + Groups: make([]Group, 0), + Endpoints: make([]Endpoint, 0), + EndpointsById: make(map[string]*Endpoint), + RoleNames: dc.GetRoleNames()} + // create all preview endpoints + for _, docEndpoint := range dc.GetEndpoints() { + endpoint := Endpoint{ + Id: docEndpoint.Id, + MethodUp: strings.ToUpper(docEndpoint.Method), + MethodLo: strings.ToLower(docEndpoint.Method), + UrlRoot: docEndpoint.UrlRoot, + UrlPath: docEndpoint.UrlPath, + Tags: append(make([]string, 0), docEndpoint.Tags...), + Summary: docEndpoint.Summary, + Parameters: prepareFields(docEndpoint.Parameters), + RequestBody: prepareFields(docEndpoint.RequestBody), + ResponseBody: prepareFields(docEndpoint.ResponseBody), + Examples: prepareExamples(docEndpoint.Examples), + Access: model.GetAccess(dc, docEndpoint.Method, docEndpoint.UrlPath)} + model.Endpoints = append(model.Endpoints, endpoint) + } + // prepare endpoint mapping by identifiers + for i, endpoint := range model.Endpoints { + model.EndpointsById[endpoint.Id] = &model.Endpoints[i] + } + // create groups of endpoints + model.createGroups() + return &model +} + +func (m *Model) FindEndpointById(id string) *Endpoint { + if endpoint, ok := m.EndpointsById[id]; ok { + return endpoint + } + return nil +} + +func (m *Model) createGroups() { + tags := make(map[string][]string) + for _, docEndpoint := range m.Endpoints { + if docEndpoint.Tags != nil { + for _, tag := range docEndpoint.Tags { + if ids, ok := tags[tag]; ok { + tags[tag] = append(ids, docEndpoint.Id) + } else { + tags[tag] = append(make([]string, 0), docEndpoint.Id) + } + } + } + } + groupNames := make([]string, 0) + for tag := range tags { + groupNames = append(groupNames, tag) + } + sort.Strings(groupNames) + m.Groups = make([]Group, 0) + for _, groupName := range groupNames { + group := CreateGroup(m, groupName, tags[groupName]) + m.Groups = append(m.Groups, group) + } +} + +func (m *Model) GetAccess(dc *d.Context, method string, path string) []string { + access := make([]string, len(m.RoleNames)) + for i, roleName := range m.RoleNames { + switch dc.GetAccess(method, path, roleName) { + case d.AccessGranted: + access[i] = "YES" + case d.AccessDenied: + access[i] = "NO" + case d.AccessError: + access[i] = "ERR" + case d.AccessUnknown: + access[i] = "?" + default: + access[i] = "-" + } + } + return access +} + +type Group struct { + Model *Model // Mode the group belong to. + Name string // Group name. + Endpoints []*Endpoint // List of endpoint identifiers in group. +} + +func CreateGroup(model *Model, name string, ids []string) Group { + group := Group{ + Model: model, + Name: strings.ToUpper(name), + Endpoints: make([]*Endpoint, 0)} + sort.SliceStable(ids, func(i1, i2 int) bool { + e1 := model.EndpointsById[ids[i1]] + e2 := model.EndpointsById[ids[i2]] + return compareEndpoints(e1, e2) + }) + for _, id := range ids { + group.Endpoints = append(group.Endpoints, model.EndpointsById[id]) + } + return group +} + +type Endpoint struct { + Id string // Unique endpoint identifier. + MethodUp string // HTTP method name in uppercase, like GET, POST, PUT or DELETE. + MethodLo string // HTTP method name in lowercase, like get, post, put or delete. + UrlRoot string // Root part of request URL. + UrlPath string // Request path after root part. + Tags []string // List of tags for endpoint. + Summary string // Summary describing endpoint. + Parameters []Field // List of parameter fields. + RequestBody []Field // List of request body fields. + ResponseBody []Field // List of response body fields. + Examples []Example // List of examples. + Access []string // List of access rights for roles. +} + +type Field struct { + Name string // Name of the field. + Type string // Type of the field. + Mandatory string // Flag indicating if field is mandatory. + MandatoryLo string // Flag indicating if field is mandatory in lowercase. + Description string // Description of the field. +} + +type Example struct { + Summary string // Example summary. + Description string // Example detailed description. + Method string // HTTP method name. + MethodLo string // HTTP method name in lowercase. + Uri string // Request URI. + StatusCode int // HTTP status code. + RequestBody string // Request body as JSON string. + ResponseBody string // Response body as JSON string. +} + +func compareEndpoints(e1, e2 *Endpoint) bool { + if i1, ok1 := HttpMethodOrder[e1.MethodUp]; ok1 { + if i2, ok2 := HttpMethodOrder[e2.MethodUp]; ok2 { + if i1 < i2 { + return true + } else if i1 == i2 { + return len(e1.UrlPath) < len(e2.UrlPath) + } + } + } + return false +} + +func prepareFields(docFields []d.Field) []Field { + if docFields == nil { + return nil + } + return traverseFields(docFields, 0) +} + +func traverseFields(docFields []d.Field, level int) []Field { + previewFields := make([]Field, 0) + for _, docField := range docFields { + mandatory := prepareMandatoryString(docField.Mandatory) + previewField := Field{ + Name: prepareFieldNameString(docField.JsonName, level), + Type: docField.JsonType, + Mandatory: mandatory, + MandatoryLo: strings.ToLower(mandatory), + Description: docField.Description} + previewFields = append(previewFields, previewField) + if docField.Children != nil { + previewFields = append(previewFields, traverseFields(docField.Children, level+1)...) + } + } + return previewFields +} + +func prepareFieldNameString(name string, level int) string { + indentString := "    " + indent := "" + for i := 0; i < level; i++ { + indent = indent + indentString + } + return indent + name +} + +func prepareMandatoryString(mandatory bool) string { + if mandatory { + return "Yes" + } else { + return "No" + } +} + +func prepareExamples(docExamples []d.Example) []Example { + examples := make([]Example, 0) + for _, docExample := range docExamples { + example := Example{ + Summary: docExample.Summary, + Description: docExample.Description, + Method: docExample.Method, + MethodLo: strings.ToLower(docExample.Method), + Uri: docExample.Uri, + StatusCode: docExample.StatusCode, + RequestBody: docExample.RequestBody, + ResponseBody: docExample.ResponseBody} + examples = append(examples, example) + } + // sort examples by status code in ascending order + sort.Slice(examples, func(i, j int) bool { + return examples[i].StatusCode < examples[j].StatusCode + }) + return examples +} diff --git a/model/model_test.go b/model/model_test.go new file mode 100755 index 0000000..8b53790 --- /dev/null +++ b/model/model_test.go @@ -0,0 +1 @@ +package model diff --git a/oxyde.go b/oxyde.go new file mode 100644 index 0000000..4f2fcbd --- /dev/null +++ b/oxyde.go @@ -0,0 +1 @@ +package oxyde diff --git a/preview_model.go b/preview_model.go deleted file mode 100755 index 8615364..0000000 --- a/preview_model.go +++ /dev/null @@ -1,255 +0,0 @@ -package oxyde - -import ( - "sort" - "strconv" - "strings" -) - -var ( - HttpMethodOrder = map[string]int{"POST": 1, "PUT": 2, "GET": 3, "DELETE": 4} -) - -type PreviewModel struct { - Groups []PreviewGroup // Groups of endpoints. - Endpoints []PreviewEndpoint // All endpoints in preview model. - EndpointsById map[string]*PreviewEndpoint // Endpoints indexed by identifier. - RoleNames []string // Names of access roles for endpoints. -} - -type PreviewGroup struct { - Model *PreviewModel // Model the group belongs to. - Name string // Group name. - Endpoints []*PreviewEndpoint // Endpoints in group. -} - -type PreviewEndpoint struct { - Id string // Unique endpoint identifier. - MethodUp string // HTTP method name in upper-case, like GET, POST, PUT or DELETE. - MethodLo string // HTTP method name in lower-case, like get, post, put or delete. - UrlRoot string // Root part of request URL. - UrlPath string // Request path after root part. - Group string // Name of the group the endpoint belongs to. - Summary string // Summary describing endpoint. - Description string // Detailed endpoint description. - Headers []PreviewField // List of header fields. - Parameters []PreviewField // List of parameter fields. - RequestBody []PreviewField // List of request body fields. - ResponseBody []PreviewField // List of response body fields. - Usages []PreviewUsage // List of examples. - Access []string // List of access rights for roles. -} - -type PreviewField struct { - Name string // Name of the field. - Type string // Type of the field. - Mandatory string // Flag indicating if field is mandatory. - MandatoryLo string // Flag indicating if field is mandatory in lower-case. - Description string // Description of the field. -} - -type PreviewHeader struct { - Name string // HTTP header name. - Value string // HTTP header value. -} - -type PreviewUsage struct { - Summary string // Usage example summary. - Description string // Usage example description. - MethodUp string // HTTP method name in upper-case. - MethodLo string // HTTP method name in lower-case. - Url string // Full request URL. - Headers []PreviewHeader // Usage headers. - RequestBody string // Request body as JSON string. - ResponseBody string // Response body as JSON string. - StatusCode int // HTTP status code. -} - -func CreatePreviewModel(dc *DocContext) *PreviewModel { - // create preview previewModel structure - previewModel := PreviewModel{ - Groups: make([]PreviewGroup, 0), - Endpoints: make([]PreviewEndpoint, 0), - EndpointsById: make(map[string]*PreviewEndpoint), - RoleNames: dc.GetRoleNames()} - // create all preview endpoints - for _, endpoint := range dc.GetEndpoints() { - previewEndpoint := PreviewEndpoint{ - Id: endpoint.Id, - MethodUp: strings.ToUpper(endpoint.Method), - MethodLo: strings.ToLower(endpoint.Method), - UrlRoot: endpoint.RootPath, - UrlPath: endpoint.RequestPath, - Group: endpoint.Group, - Summary: endpoint.Summary, - Description: endpoint.Description, - Headers: prepareFields(endpoint.Headers), - Parameters: prepareFields(endpoint.Parameters), - RequestBody: prepareFields(endpoint.RequestBody), - ResponseBody: prepareFields(endpoint.ResponseBody), - Usages: preparePreviewUsages(endpoint.Usages), - Access: previewModel.GetAccess(dc, endpoint.Method, endpoint.RequestPath)} - previewModel.Endpoints = append(previewModel.Endpoints, previewEndpoint) - } - // prepare previewEndpoint mapping by identifiers - for i, previewEndpoint := range previewModel.Endpoints { - previewModel.EndpointsById[previewEndpoint.Id] = &previewModel.Endpoints[i] - } - // create groups of endpoints - previewModel.createGroups() - return &previewModel -} - -func CreatePreviewGroup(previewModel *PreviewModel, name string, ids []string) PreviewGroup { - previewGroup := PreviewGroup{ - Model: previewModel, - Name: strings.ToUpper(name), - Endpoints: make([]*PreviewEndpoint, 0)} - sort.SliceStable(ids, func(i1, i2 int) bool { - e1 := previewModel.EndpointsById[ids[i1]] - e2 := previewModel.EndpointsById[ids[i2]] - return compareEndpoints(e1, e2) - }) - for _, id := range ids { - previewGroup.Endpoints = append(previewGroup.Endpoints, previewModel.EndpointsById[id]) - } - return previewGroup -} - -func compareEndpoints(e1, e2 *PreviewEndpoint) bool { - if i1, ok1 := HttpMethodOrder[e1.MethodUp]; ok1 { - if i2, ok2 := HttpMethodOrder[e2.MethodUp]; ok2 { - if i1 < i2 { - return true - } else if i1 == i2 { - return len(e1.UrlPath) < len(e2.UrlPath) - } - } - } - return false -} - -func (m *PreviewModel) FindEndpointById(id string) *PreviewEndpoint { - if endpoint, ok := m.EndpointsById[id]; ok { - return endpoint - } - return nil -} - -func (m *PreviewModel) createGroups() { - groups := make(map[string][]string) - for _, endpoint := range m.Endpoints { - groupName := endpoint.Group - if ids, ok := groups[groupName]; ok { - groups[groupName] = append(ids, endpoint.Id) - } else { - groups[groupName] = append(make([]string, 0), endpoint.Id) - } - } - groupNames := make([]string, 0) - for groupName := range groups { - groupNames = append(groupNames, groupName) - } - sort.Strings(groupNames) - m.Groups = make([]PreviewGroup, 0) - for _, groupName := range groupNames { - group := CreatePreviewGroup(m, groupName, groups[groupName]) - m.Groups = append(m.Groups, group) - } -} - -func (m *PreviewModel) GetAccess(dc *DocContext, method string, path string) []string { - access := make([]string, len(m.RoleNames)) - for i, roleName := range m.RoleNames { - switch dc.GetAccess(method, path, roleName) { - case AccessGranted: - access[i] = "YES" - case AccessDenied: - access[i] = "NO" - case AccessError: - access[i] = "ERR" - case AccessUnknown: - access[i] = "?" - default: - access[i] = "-" - } - } - return access -} - -func prepareFields(docFields []Field) []PreviewField { - if docFields == nil { - return nil - } - return traverseFields(docFields, 0) -} - -func traverseFields(fields []Field, level int) []PreviewField { - previewFields := make([]PreviewField, 0) - for _, field := range fields { - mandatory := prepareMandatoryString(field.Mandatory) - previewField := PreviewField{ - Name: prepareFieldNameString(field.JsonName, level), - Type: field.JsonType, - Mandatory: mandatory, - MandatoryLo: strings.ToLower(mandatory), - Description: field.Description} - previewFields = append(previewFields, previewField) - if field.Children != nil { - previewFields = append(previewFields, traverseFields(field.Children, level+1)...) - } - } - return previewFields -} - -func prepareFieldNameString(name string, level int) string { - indentString := "    " - indent := "" - for i := 0; i < level; i++ { - indent = indent + indentString - } - return indent + name -} - -func prepareMandatoryString(mandatory bool) string { - if mandatory { - return "Yes" - } else { - return "No" - } -} - -func preparePreviewUsages(usages []Usage) []PreviewUsage { - previewUsages := make([]PreviewUsage, 0) - for _, usage := range usages { - previewExample := PreviewUsage{ - Summary: usage.Summary, - Description: usage.Description, - MethodUp: strings.ToUpper(usage.Method), - MethodLo: strings.ToLower(usage.Method), - Headers: preparePreviewHeaders(usage.Headers), - Url: usage.Url, - StatusCode: usage.StatusCode, - RequestBody: usage.RequestBody, - ResponseBody: usage.ResponseBody} - previewUsages = append(previewUsages, previewExample) - } - // sort preview usages by status code in ascending order - sort.Slice(previewUsages, func(i, j int) bool { - return previewUsages[i].StatusCode < previewUsages[j].StatusCode - }) - return previewUsages -} - -func preparePreviewHeaders(headers headers) []PreviewHeader { - const maxLen = 50 - previewHeaders := make([]PreviewHeader, 0) - for name, value := range headers { - length := len(value) - if length > maxLen { - value = value[:maxLen] + "[...](" + strconv.Itoa(length) + ")" - } - previewHeaders = append(previewHeaders, PreviewHeader{Name: name, Value: value}) - } - return previewHeaders -} diff --git a/rest.go b/rest.go deleted file mode 100755 index 627daf3..0000000 --- a/rest.go +++ /dev/null @@ -1,371 +0,0 @@ -package oxyde - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "reflect" - "strings" -) - -const ( - httpGET = "GET" - httpPOST = "POST" - httpPUT = "PUT" - httpDELETE = "DELETE" -) - -// Request context. Provides additional data required to successfully execute HTTP requests. -type Context struct { - Url string // The URL of tested endpoint. - Token string // Authorization token. - UserName string // Name of the authorized user. - RoleName string // Name of the current role of the authorized user. - Verbose bool // Flag indicating if executing process should be more verbose. - Version string // API version to be used in endpoint URL. -} - -func CreateContext() *Context { - return &Context{ - Url: "", - Token: "", - UserName: "", - RoleName: "", - Verbose: false, - Version: "v1", - } -} - -// Function HttpGETString executes HTTP GET request and returns simple text result (not JSON string!) -func HttpGETString( - ctx *Context, - dtx *DocContext, - path string, - params interface{}, - result interface{}, - status int) { - requestPath, err := prepareRequestPath(path, ctx.Version, params) - uri := prepareUri(ctx, requestPath) - displayRequestDetails(ctx, httpGET, uri) - req, err := http.NewRequest(httpGET, uri, nil) - panicOnError(err) - setRequestHeaders(ctx, req, nil) - client := http.Client{} - res, err := client.Do(req) - panicOnError(err) - panicOnUnexpectedStatusCode(ctx, status, res) - responseBody := readResponseBody(ctx, res) - collectDocumentationData(ctx, dtx, res, httpGET, path, requestPath, nil, params, nil, result, nil, responseBody) - resultFields := ParseType(result) - if len(resultFields) == 1 && resultFields[0].JsonName == "-" && resultFields[0].JsonType == "string" { - reflect.ValueOf(result).Elem().Field(0).SetString(string(responseBody)) - } -} - -// Function HttpGET executes HTTP GET request and returns JSON result. -func HttpGET( - ctx *Context, /* Request context. */ - dtx *DocContext, /* Documentation context. */ - path string, /* Request path. */ - headers interface{}, /* Request headers. */ - params interface{}, /* Request parameters. */ - result interface{}, /* Response payload (response body). */ - status int /* Expected HTTP status code. */) { - var responseBody []byte - requestPath, err := prepareRequestPath(path, ctx.Version, params) - uri := prepareUri(ctx, requestPath) - displayRequestDetails(ctx, httpGET, uri) - req, err := http.NewRequest(httpGET, uri, nil) - panicOnError(err) - setRequestHeaders(ctx, req, headers) - client := http.Client{} - res, err := client.Do(req) - panicOnError(err) - panicOnUnexpectedStatusCode(ctx, status, res) - if nilValue(result) { - responseBody = nil - } else { - responseBody = readResponseBody(ctx, res) - err = json.Unmarshal(responseBody, result) - panicOnError(err) - } - collectDocumentationData(ctx, dtx, res, httpGET, path, requestPath, headers, params, nil, result, nil, responseBody) -} - -// Function HttpPOST executes HTTP POST request. -func HttpPOST( - ctx *Context, /* Request context. */ - dtx *DocContext, /* Documentation context. */ - path string, /* Request path. */ - headers interface{}, /* Request headers. */ - params interface{}, /* Request parameters. */ - body interface{}, /* Request payload (request body). */ - result interface{}, /* Response payload (response body). */ - status int /* Expected HTTP status code. */) { - httpCall(ctx, dtx, httpPOST, path, headers, params, body, result, status) -} - -// Function HttpPUT executes HTTP PUT request. -func HttpPUT( - ctx *Context, /* Request context. */ - dtx *DocContext, /* Documentation context. */ - path string, /* Request path. */ - headers interface{}, /* Request headers. */ - params interface{}, /* Request parameters. */ - body interface{}, /* Request payload (request body). */ - result interface{}, /* Response payload (response body). */ - status int /* Expected HTTP status code. */) { - httpCall(ctx, dtx, httpPUT, path, headers, params, body, result, status) -} - -// Function HttpDELETE executes HTTP DELETE request. -func HttpDELETE( - ctx *Context, /* Request context. */ - dtx *DocContext, /* Documentation context. */ - path string, /* Request path. */ - headers interface{}, /* Request headers. */ - params interface{}, /* Request parameters. */ - body interface{}, /* Request payload (request body). */ - result interface{}, /* Response payload (response body). */ - status int /* Expected HTTP status code. */) { - httpCall(ctx, dtx, httpDELETE, path, headers, params, body, result, status) -} - -// Function httpCall executes HTTP request with specified HTTP method and parameters. -func httpCall( - ctx *Context, - dtx *DocContext, - method string, - path string, - headers interface{}, - params interface{}, - body interface{}, - result interface{}, - status int) { - var req *http.Request - var requestBody []byte - var responseBody []byte - var err error - requestPath, err := prepareRequestPath(path, ctx.Version, params) - panicOnError(err) - uri := prepareUri(ctx, requestPath) - displayRequestDetails(ctx, method, uri) - if nilValue(body) { - requestBody = nil - displayRequestPayload(ctx, nil) - req, err = http.NewRequest(method, uri, nil) - panicOnError(err) - } else { - bodyFields := ParseType(body) - if len(bodyFields) == 1 && bodyFields[0].JsonName == "-" && bodyFields[0].JsonType == "string" { - field := reflect.ValueOf(body).Elem().Field(0) - if field.Kind() == reflect.Ptr { - field = reflect.Indirect(field) - } - requestBody = []byte(field.String()) - } else { - requestBody, err = json.Marshal(body) - panicOnError(err) - } - displayRequestPayload(ctx, requestBody) - req, err = http.NewRequest(method, uri, bytes.NewReader(requestBody)) - panicOnError(err) - req.Header.Add("Content-Type", "application/json") - } - setRequestHeaders(ctx, req, headers) - client := http.Client{} - res, err := client.Do(req) - panicOnError(err) - panicOnUnexpectedStatusCode(ctx, status, res) - if nilValue(result) { - responseBody = nil - } else { - responseBody = readResponseBody(ctx, res) - err = json.Unmarshal(responseBody, result) - panicOnError(err) - } - collectDocumentationData(ctx, dtx, res, method, path, requestPath, headers, params, body, result, requestBody, responseBody) -} - -func collectDocumentationData( - ctx *Context, - dc *DocContext, - res *http.Response, - method string, - path string, - requestPath string, - headers interface{}, - params interface{}, - payload interface{}, - result interface{}, - requestBody []byte, - responseBody []byte) { - if endpoint := dc.GetEndpoint(); endpoint != nil && dc.CollectDescriptionMode() { - endpoint.Method = method - endpoint.RootPath = ctx.Url - endpoint.RequestPath = preparePath(path, ctx.Version) - if nilValue(headers) { - endpoint.Headers = nil - } else { - endpoint.Headers = ParseType(headers) - } - if nilValue(params) { - endpoint.Parameters = nil - } else { - endpoint.Parameters = ParseType(params) - } - if nilValue(payload) { - endpoint.RequestBody = nil - } else { - endpoint.RequestBody = ParseType(payload) - } - if nilValue(result) { - endpoint.ResponseBody = nil - } else { - endpoint.ResponseBody = ParseType(result) - } - } - if endpoint := dc.GetEndpoint(); endpoint != nil && dc.CollectExamplesMode() { - usages := endpoint.Usages - if usages == nil { - endpoint.Usages = make([]Usage, 0) - } - usage := Usage{ - Summary: dc.GetExampleSummary(), - Description: dc.GetExampleDescription(), - Method: method, - Headers: parseHeaders(headers), - Url: ctx.Url + requestPath, - RequestBody: prettyPrint(requestBody), - ResponseBody: prettyPrint(responseBody), - StatusCode: res.StatusCode} - endpoint.Usages = append(endpoint.Usages, usage) - } - dc.SaveRole(method, preparePath(path, ctx.Version), res.StatusCode) - dc.StopCollecting() -} - -func preparePath(path string, version string) string { - if version != "" { - if strings.Contains(path, VersionPlaceholder) { - path = strings.ReplaceAll(path, VersionPlaceholder, version) - } - } - return path -} - -func prepareRequestPath(path string, version string, params interface{}) (string, error) { - if version != "" { - if strings.Contains(path, VersionPlaceholder) { - path = strings.ReplaceAll(path, VersionPlaceholder, version) - } - } - if nilValue(params) { - return path, nil - } - paramsType := TypeOfValue(params) - if paramsType.Kind().String() != "struct" { - return "", errors.New("only struct parameters are allowed") - } - firstParameter := true - for i := 0; i < paramsType.NumField(); i++ { - field := paramsType.Field(i) - fieldJsonName := field.Tag.Get(JsonTagName) - placeholder := "{" + fieldJsonName + "}" - value := ValueOfValue(params).Field(i).Interface() - if !nilValue(value) { - valueStr := url.PathEscape(fmt.Sprintf("%v", ValueOfValue(value))) - if strings.Contains(path, placeholder) { - path = strings.ReplaceAll(path, placeholder, valueStr) - } else { - if firstParameter { - path = path + "?" - } else { - path = path + "&" - } - path = path + fieldJsonName + "=" + valueStr - firstParameter = false - } - } - } - return path, nil -} - -// Function setRequestHeaders adds headers to the request. -func setRequestHeaders(ctx *Context, req *http.Request, headers interface{}) { - for name, value := range parseHeaders(headers) { - req.Header.Add(name, value) - } -} - -// Function readResponseBody reads and returns the body of HTTP response. -func readResponseBody(ctx *Context, res *http.Response) []byte { - body, err := ioutil.ReadAll(res.Body) - panicOnError(err) - err = res.Body.Close() - panicOnError(err) - displayResponseBody(ctx, body) - return body -} - -// Function prepareUri concatenates URL defined in context with -// request path and returns full URI of HTTP request. -func prepareUri(ctx *Context, path string) string { - return ctx.Url + path -} - -// Function displayRequestDetails writes to standard output -// request method and URI. -func displayRequestDetails(ctx *Context, method string, uri string) { - if ctx.Verbose { - fmt.Printf("\n\n===> %s:\n%s\n", method, uri) - } -} - -// Function displayRequestPayload writes to standard output -// pretty-printed request payload. -func displayRequestPayload(ctx *Context, payload []byte) { - if ctx.Verbose { - if payload == nil { - fmt.Printf("\n===> REQUEST PAYLOAD:\n(none)\n") - } else { - fmt.Printf("\n===> REQUEST PAYLOAD:\n%s\n", prettyPrint(payload)) - } - } -} - -// Function displayResponseBody writes to standard output -// pretty-printed response body when verbose mode is on. -func displayResponseBody(ctx *Context, body []byte) { - if ctx.Verbose { - if body == nil { - fmt.Printf("\n<=== RESPONSE BODY:\n(none)\n") - } else { - fmt.Printf("\n<=== RESPONSE BODY:\n%s\n", prettyPrint(body)) - } - } -} - -// Function panicOnUnexpectedStatusCode displays error message and panics when -// actual HTTP response status code differs from the expected one. -func panicOnUnexpectedStatusCode(ctx *Context, expectedCode int, res *http.Response) { - // display the returned status code if the same as expected - if ctx.Verbose { - fmt.Printf("\n<=== STATUS:\n%d\n", res.StatusCode) - } - // check if the expected status code is the same as returned by server - if res.StatusCode != expectedCode { - readResponseBody(ctx, res) - separator := makeText("-", 120) - fmt.Printf("\n\n%s\n> ERROR: unexpected status code\n> Expected: %d\n> Actual: %d\n%s\n\n", - separator, - expectedCode, - res.StatusCode, - separator) - brexit() - } -} diff --git a/rest/rest.go b/rest/rest.go new file mode 100755 index 0000000..5b32d37 --- /dev/null +++ b/rest/rest.go @@ -0,0 +1,279 @@ +package rest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/wisbery/oxyde/common" + "github.com/wisbery/oxyde/doc" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "strings" +) + +const ( + httpGET = "GET" + httpPOST = "POST" + httpPUT = "PUT" + httpDELETE = "DELETE" +) + +// Interface for request context. Instances of this interface +// provide data required to successfully execute HTTP requests. +type Context interface { + GetUrl() string // Returns URL of the endpoint to be called. + GetAuthorizationToken() string // Returns access token to be passed in 'Authorization' header. + GetHeaders() map[string]string // Returns map of HTTP headers to be passed to endpoint call. + GetVerbose() bool // Returns flag indicating if executing process should be more verbose. +} + +// Function HttpGETString executes HTTP GET request and returns simple text result (not JSON string!) +func HttpGETString(c Context, dc *doc.Context, path string, params interface{}, result interface{}, status int) { + requestPath, err := prepareRequestPath(path, params) + uri := prepareUri(c, requestPath) + displayRequestDetails(c, httpGET, uri) + req, err := http.NewRequest(httpGET, uri, nil) + common.PanicOnError(err) + setRequestHeaders(c, req) + client := http.Client{} + res, err := client.Do(req) + common.PanicOnError(err) + panicOnUnexpectedStatusCode(c, status, res) + responseBody := readResponseBody(c, res) + collectDocumentationData(c, dc, res, httpGET, path, requestPath, params, nil, result, nil, responseBody) + resultFields := doc.ParseObject(result) + if len(resultFields) == 1 && resultFields[0].JsonName == "-" && resultFields[0].JsonType == "string" { + reflect.ValueOf(result).Elem().Field(0).SetString(string(responseBody)) + } +} + +// Function HttpGET executes HTTP GET request and returns JSON result. +func HttpGET(c Context, dc *doc.Context, path string, params interface{}, result interface{}, status int) { + var responseBody []byte + requestPath, err := prepareRequestPath(path, params) + uri := prepareUri(c, requestPath) + displayRequestDetails(c, httpGET, uri) + req, err := http.NewRequest(httpGET, uri, nil) + common.PanicOnError(err) + setRequestHeaders(c, req) + client := http.Client{} + res, err := client.Do(req) + common.PanicOnError(err) + panicOnUnexpectedStatusCode(c, status, res) + if common.NilValue(result) { + responseBody = nil + } else { + responseBody = readResponseBody(c, res) + err = json.Unmarshal(responseBody, result) + common.PanicOnError(err) + } + collectDocumentationData(c, dc, res, httpGET, path, requestPath, params, nil, result, nil, responseBody) +} + +// Function HttpPOST executes HTTP POST request. +func HttpPOST(c Context, dc *doc.Context, path string, payload interface{}, result interface{}, status int) { + httpCall(c, dc, httpPOST, path, nil, payload, result, status) +} + +// Function HttpPUT executes HTTP PUT request. +func HttpPUT(c Context, dc *doc.Context, path string, payload interface{}, result interface{}, status int) { + httpCall(c, dc, httpPUT, path, nil, payload, result, status) +} + +// Function HttpDELETE executes HTTP DELETE request. +func HttpDELETE(c Context, dc *doc.Context, path string, params interface{}, payload interface{}, result interface{}, status int) { + httpCall(c, dc, httpDELETE, path, params, payload, result, status) +} + +// Function httpCall executes HTTP request with specified HTTP method and parameters. +func httpCall(c Context, dc *doc.Context, method string, path string, params interface{}, payload interface{}, result interface{}, status int) { + var req *http.Request + var requestBody []byte + var responseBody []byte + var err error + requestPath, err := prepareRequestPath(path, params) + common.PanicOnError(err) + uri := prepareUri(c, requestPath) + displayRequestDetails(c, method, uri) + if common.NilValue(payload) { + requestBody = nil + displayRequestPayload(c, nil) + req, err = http.NewRequest(method, uri, nil) + common.PanicOnError(err) + } else { + requestBody, err = json.Marshal(payload) + common.PanicOnError(err) + displayRequestPayload(c, requestBody) + req, err = http.NewRequest(method, uri, bytes.NewReader(requestBody)) + common.PanicOnError(err) + req.Header.Add("Content-Type", "application/json") + } + setRequestHeaders(c, req) + client := http.Client{} + res, err := client.Do(req) + common.PanicOnError(err) + panicOnUnexpectedStatusCode(c, status, res) + if common.NilValue(result) { + responseBody = nil + } else { + responseBody = readResponseBody(c, res) + err = json.Unmarshal(responseBody, result) + common.PanicOnError(err) + } + collectDocumentationData(c, dc, res, method, path, requestPath, params, payload, result, requestBody, responseBody) +} + +func collectDocumentationData(c Context, dc *doc.Context, res *http.Response, method string, path string, requestPath string, params interface{}, payload interface{}, result interface{}, requestBody []byte, responseBody []byte) { + if endpoint := dc.GetEndpoint(); endpoint != nil && dc.CollectDescriptionMode() { + endpoint.Method = method + endpoint.UrlRoot = c.GetUrl() + endpoint.UrlPath = path + if common.NilValue(params) { + endpoint.Parameters = nil + } else { + endpoint.Parameters = doc.ParseObject(params) + } + if common.NilValue(payload) { + endpoint.RequestBody = nil + } else { + endpoint.RequestBody = doc.ParseObject(payload) + } + if common.NilValue(result) { + endpoint.ResponseBody = nil + } else { + endpoint.ResponseBody = doc.ParseObject(result) + } + } + if endpoint := dc.GetEndpoint(); endpoint != nil && dc.CollectExamplesMode() { + examples := endpoint.Examples + if examples == nil { + endpoint.Examples = make([]doc.Example, 0) + } + example := doc.Example{ + Summary: dc.GetExampleSummary(), + Description: dc.GetExampleDescription(), + Method: method, + Uri: c.GetUrl() + requestPath, + StatusCode: res.StatusCode, + RequestBody: common.PrettyPrint(requestBody), + ResponseBody: common.PrettyPrint(responseBody)} + endpoint.Examples = append(endpoint.Examples, example) + } + dc.SaveRole(method, path, res.StatusCode) + dc.StopCollecting() +} + +func prepareRequestPath(path string, params interface{}) (string, error) { + if common.NilValue(params) { + return path, nil + } + paramsType := common.TypeOfValue(params) + if paramsType.Kind().String() != "struct" { + return "", errors.New("only struct parameters are allowed") + } + firstParameter := true + for i := 0; i < paramsType.NumField(); i++ { + field := paramsType.Field(i) + fieldJsonName := field.Tag.Get(common.JsonTagName) + placeholder := "{" + fieldJsonName + "}" + value := common.ValueOfValue(params).Field(i).Interface() + if !common.NilValue(value) { + valueStr := url.PathEscape(fmt.Sprintf("%v", common.ValueOfValue(value))) + if strings.Contains(path, placeholder) { + path = strings.ReplaceAll(path, placeholder, valueStr) + } else { + if firstParameter { + path = path + "?" + } else { + path = path + "&" + } + path = path + fieldJsonName + "=" + valueStr + firstParameter = false + } + } + } + return path, nil +} + +// Function setRequestHeaders adds to the request authorization header and user defined headers. +func setRequestHeaders(c Context, req *http.Request) { + if len(c.GetAuthorizationToken()) > 0 { + req.Header.Add("Authorization", c.GetAuthorizationToken()) + } + if c.GetHeaders() != nil { + for name, value := range c.GetHeaders() { + req.Header.Add(name, value) + } + } +} + +// Function readResponseBody reads and returns the body of HTTP response. +func readResponseBody(c Context, res *http.Response) []byte { + body, err := ioutil.ReadAll(res.Body) + common.PanicOnError(err) + err = res.Body.Close() + common.PanicOnError(err) + displayResponseBody(c, body) + return body +} + +// Function prepareUri concatenates URL defined in context with +// request path and returns full URI of HTTP request. +func prepareUri(c Context, path string) string { + return c.GetUrl() + path +} + +// Function displayRequestDetails writes to standard output +// request method and URI. +func displayRequestDetails(c Context, method string, uri string) { + if c.GetVerbose() { + fmt.Printf("\n\n===> %s:\n%s\n", method, uri) + } +} + +// Function displayRequestPayload writes to standard output +// pretty-printed request payload. +func displayRequestPayload(c Context, payload []byte) { + if c.GetVerbose() { + if payload == nil { + fmt.Printf("\n===> REQUEST PAYLOAD:\n(none)\n") + } else { + fmt.Printf("\n===> REQUEST PAYLOAD:\n%s\n", common.PrettyPrint(payload)) + } + } +} + +// Function displayResponseBody writes to standard output +// pretty-printed response body when verbose mode is on. +func displayResponseBody(c Context, body []byte) { + if c.GetVerbose() { + if body == nil { + fmt.Printf("\n<=== RESPONSE BODY:\n(none)\n") + } else { + fmt.Printf("\n<=== RESPONSE BODY:\n%s\n", common.PrettyPrint(body)) + } + } +} + +// Function panicOnUnexpectedStatusCode displays error message and panics when +// actual HTTP response status code differs from the expected one. +func panicOnUnexpectedStatusCode(c Context, expectedCode int, res *http.Response) { + // display the returned status code if the same as expected + if c.GetVerbose() { + fmt.Printf("\n<=== STATUS:\n%d\n", res.StatusCode) + } + // check if the expected status code is the same as returned by server + if res.StatusCode != expectedCode { + readResponseBody(c, res) + separator := common.MakeString('-', 120) + fmt.Printf("\n\n%s\n> ERROR: unexpected status code\n> Expected: %d\n> Actual: %d\n%s\n\n", + separator, + expectedCode, + res.StatusCode, + separator) + common.BrExit() + } +} diff --git a/rest_test.go b/rest/rest_test.go similarity index 76% rename from rest_test.go rename to rest/rest_test.go index 67612fd..a329855 100755 --- a/rest_test.go +++ b/rest/rest_test.go @@ -1,4 +1,4 @@ -package oxyde +package rest import ( "testing" @@ -9,9 +9,8 @@ func TestSingleParameterInjection(t *testing.T) { params := struct { UserId string `json:"userId"` }{ - UserId: "c4f63c4f-e66b-4cd4-b0a7-f7a5e2bc6edd", - } - requestPath, err := prepareRequestPath(path, "v1", params) + UserId: "c4f63c4f-e66b-4cd4-b0a7-f7a5e2bc6edd"} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users/c4f63c4f-e66b-4cd4-b0a7-f7a5e2bc6edd" || err != nil { t.Error("single parameter not injected") } @@ -24,9 +23,8 @@ func TestMultipleParameterInjection(t *testing.T) { UserName string `json:"userName"` }{ UserId: "b494fd53-10c8-43bf-b585-334a2cac0995", - UserName: "John", - } - requestPath, err := prepareRequestPath(path, "v1", params) + UserName: "John"} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users/b494fd53-10c8-43bf-b585-334a2cac0995/John" || err != nil { t.Error("multiple parameters not injected") } @@ -37,9 +35,8 @@ func TestSingleRepeatedParameterInjection(t *testing.T) { params := struct { UserId string `json:"userId"` }{ - UserId: "ee90021b-15ce-4d3e-bd2c-6ce023503fff", - } - requestPath, err := prepareRequestPath(path, "v1", params) + UserId: "ee90021b-15ce-4d3e-bd2c-6ce023503fff"} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users/ee90021b-15ce-4d3e-bd2c-6ce023503fff/ee90021b-15ce-4d3e-bd2c-6ce023503fff" || err != nil { t.Error("single repeated parameters not injected") } @@ -50,9 +47,8 @@ func TestSingleParameterAppend(t *testing.T) { params := struct { UserId string `json:"userId"` }{ - UserId: "2b4ca889-7ed0-41ca-b832-222a9ecaf183", - } - requestPath, err := prepareRequestPath(path, "v1", params) + UserId: "2b4ca889-7ed0-41ca-b832-222a9ecaf183"} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users?userId=2b4ca889-7ed0-41ca-b832-222a9ecaf183" || err != nil { t.Error("single parameters not appended") } @@ -67,9 +63,8 @@ func TestMultipleParameterAppend(t *testing.T) { }{ UserId: "2b4ca889-7ed0-41ca-b832-222a9ecaf183", UserName: "Matthew", - Age: 32, - } - requestPath, err := prepareRequestPath(path, "v1", params) + Age: 32} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users?userId=2b4ca889-7ed0-41ca-b832-222a9ecaf183&userName=Matthew&age=32" || err != nil { t.Error("multiple parameters not appended") } @@ -80,9 +75,8 @@ func TestEmptyParameterInjection(t *testing.T) { params := struct { UserId string `json:"userId"` }{ - UserId: "", - } - requestPath, err := prepareRequestPath(path, "v1", params) + UserId: ""} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users/empty" || err != nil { t.Error("empty parameter not injected") } @@ -93,9 +87,8 @@ func TestEmptyParameterAppend(t *testing.T) { params := struct { UserId string `json:"userId"` }{ - UserId: "", - } - requestPath, err := prepareRequestPath(path, "v1", params) + UserId: ""} + requestPath, err := prepareRequestPath(path, params) if requestPath != "/users?userId=" || err != nil { t.Error("empty parameter not appended") } diff --git a/preview_server.go b/server/server.go similarity index 59% rename from preview_server.go rename to server/server.go index 5ee4559..3dba074 100755 --- a/preview_server.go +++ b/server/server.go @@ -1,8 +1,12 @@ -package oxyde +package server import ( "bytes" "fmt" + "github.com/wisbery/oxyde/common" + d "github.com/wisbery/oxyde/doc" + h "github.com/wisbery/oxyde/html" + m "github.com/wisbery/oxyde/model" "io" "log" "net/http" @@ -16,12 +20,12 @@ var ( errorTemplate = prepareErrorTemplate() ) -func StartPreview(dc *DocContext) { - model := CreatePreviewModel(dc) +func StartPreview(dc *d.Context) { + model := m.CreateModel(dc) runPreviewServer(model, 16100) } -func runPreviewServer(model *PreviewModel, port int) { +func runPreviewServer(model *m.Model, port int) { index := func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -30,8 +34,8 @@ func runPreviewServer(model *PreviewModel, port int) { styleCss := func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/css") - _, err := io.WriteString(w, StyleCss) - panicOnError(err) + _, err := io.WriteString(w, h.StyleCss) + common.PanicOnError(err) } endpointDetails := func(w http.ResponseWriter, req *http.Request) { @@ -49,35 +53,34 @@ func runPreviewServer(model *PreviewModel, port int) { return } - mux := http.NewServeMux() - mux.HandleFunc("/", index) - mux.HandleFunc("/style.css", styleCss) - mux.HandleFunc("/endpoint-details", endpointDetails) - fmt.Printf("Documentation server started and listening on port: %d\n", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux)) + http.HandleFunc("/", index) + http.HandleFunc("/style.css", styleCss) + http.HandleFunc("/endpoint-details", endpointDetails) + fmt.Printf(">> API preview server started and listening on port: %d\n", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) } func preparePageTemplate() *template.Template { - t, err := template.New("pageTemplate").Parse(PageTemplate) - panicOnError(err) + t, err := template.New("pageTemplate").Parse(h.PageTemplate) + common.PanicOnError(err) return t } func prepareIndexTemplate() *template.Template { - t, err := template.New("indexTemplate").Parse(IndexTemplate) - panicOnError(err) + t, err := template.New("indexTemplate").Parse(h.IndexTemplate) + common.PanicOnError(err) return t } func prepareEndpointTemplate() *template.Template { - t, err := template.New("endpointTemplate").Parse(EndpointTemplate) - panicOnError(err) + t, err := template.New("endpointTemplate").Parse(h.EndpointTemplate) + common.PanicOnError(err) return t } func prepareErrorTemplate() *template.Template { - t, err := template.New("errorTemplate").Parse(ErrorTemplate) - panicOnError(err) + t, err := template.New("errorTemplate").Parse(h.ErrorTemplate) + common.PanicOnError(err) return t } @@ -85,7 +88,7 @@ func wrapInPage(w http.ResponseWriter, t *template.Template, data interface{}) { var out bytes.Buffer outWriter := io.Writer(&out) err := t.Execute(outWriter, data) - panicOnError(err) + common.PanicOnError(err) err = pageTemplate.Execute(w, out.String()) - panicOnError(err) + common.PanicOnError(err) } diff --git a/utils.go b/utils.go deleted file mode 100755 index 1da7c38..0000000 --- a/utils.go +++ /dev/null @@ -1,161 +0,0 @@ -package oxyde - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "github.com/google/uuid" - "os" - "reflect" - "regexp" - "runtime" - "strings" -) - -const ( - ApiTagName = "api" // Name of the tag in which documentation details are stored. - JsonTagName = "json" // Name of the tag in which JSON details are stored. - VersionPlaceholder = "{apiVersion}" // Placeholder for API version number in request path. - OptionalPrefix = "?" // Prefix used to mark the field as optional. - TestSuitePrefix = "ts_" // Prefix used to name the function that runs the test suite. - TestCasePrefix = "tc_" // Prefix used to name the function that runs the test case. - TestDocumentationPrefix = "td_" // Prefix used to name the function that documents the API. -) - -var ( - reCamelBoundary = regexp.MustCompile("([a-z])([A-Z])") - reFunctionName = regexp.MustCompile(`\.([a-zA-Z_0-9]+)\(`) -) - -// Function makeText creates a string that contains repeated text. -func makeText(text string, repeat int) string { - var builder strings.Builder - for i := 0; i < repeat; i++ { - builder.WriteString(text) - } - return builder.String() -} - -// Function nilValue checks if the value specified as parameter is nil. -func nilValue(value interface{}) bool { - return value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) -} - -// Function prettyPrint takes JSON string as a parameter -// and returns the same string as pretty-printed JSON. -func prettyPrint(in []byte) string { - var out bytes.Buffer - err := json.Indent(&out, in, "", " ") - if err != nil { - return string(in) - } - return out.String() -} - -// Function panicOnError panics when the error passed as argument is not nil. -func panicOnError(err error) { - if err != nil { - panic(err) - } -} - -// Function generateId returns universally unique identifier. -func generateId() string { - id, err := uuid.NewRandom() - panicOnError(err) - return id.String() -} - -// Function TypeOfValue returns the type of value specified as parameter. -// If specified value is a pointer, then is first dereferenced. -func TypeOfValue(value interface{}) reflect.Type { - t := reflect.TypeOf(value) - if t.Kind().String() == "ptr" { - v := reflect.ValueOf(value) - t = reflect.Indirect(v).Type() - } - return t -} - -// Function ValueOfValue returns the value of specified parameter. -// If specified value is a pointer, then is first dereferenced. -func ValueOfValue(value interface{}) reflect.Value { - t := reflect.TypeOf(value) - if t.Kind().String() == "ptr" { - v := reflect.ValueOf(value) - return reflect.Indirect(v) - } - return reflect.ValueOf(value) -} - -// Function brexit stops the execution of the test and displays the stack trace. -// After breaking the execution flow, application returns exit code -1 -// that can be utilized by test automation tools. -func brexit() { - fmt.Printf("Stack trace:\n------------\n") - reDeepCalls := regexp.MustCompile(`(^goroutine[^:]*:$)|(^.*oxyde.*$)`) - reFuncParams := regexp.MustCompile(`([a-zA-Z_0-9]+)\([^\)]+\)`) - reFuncOffset := regexp.MustCompile(`\s+\+.*$`) - b := make([]byte, 100000) - runtime.Stack(b, false) - scanner := bufio.NewScanner(bytes.NewBuffer(b)) - for scanner.Scan() { - line := scanner.Text() - if reDeepCalls.MatchString(line) { - continue - } - line = reFuncParams.ReplaceAllString(line, "$1()") - line = reFuncOffset.ReplaceAllString(line, "") - fmt.Println(line) - } - os.Exit(-1) -} - -func Display(ctx *Context) { - DisplayLevel(ctx, 3) -} - -func Display2(ctx *Context) { - DisplayLevel(ctx, 4) -} - -func Ok() { - fmt.Println("OK") -} - -func DisplayLevel(ctx *Context, level int) { - text := strings.TrimSpace(FunctionName(level)) - newline := "\n" - if strings.HasPrefix(text, TestSuitePrefix) { - text = " >> " + text - } else if strings.HasPrefix(text, TestDocumentationPrefix) { - text = " > " + text - } else if strings.HasPrefix(text, TestCasePrefix) { - text = " - " + text + " [" + ctx.UserName + "]" - newline = "" - } else { - text = ">>> " + text - } - fmt.Printf("%-120s%-5s%s", text, ctx.Version, newline) -} - -func FunctionName(level int) string { - b := make([]byte, 8192) - runtime.Stack(b, false) - scanner := bufio.NewScanner(bytes.NewBuffer(b)) - index := 0 - var line string - for scanner.Scan() { - line = scanner.Text() - if index == level*2+1 { - break - } - index++ - } - line = reFunctionName.FindString(line) - line = reFunctionName.ReplaceAllString(line, "$1") - line = reCamelBoundary.ReplaceAllString(line, `$1#$2`) - line = strings.ToLower(strings.ReplaceAll(line, "#", "_")) - return line -}