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 = `
Name | -Type | -Mandatory | -Description | -
---|---|---|---|
{{.Name}} | -{{.Type}} | -{{.Mandatory}} | -{{.Description}} | -
{{.RequestBody}}