Skip to content

Commit

Permalink
feat(import): basic option on import collections from openai spec
Browse files Browse the repository at this point in the history
  • Loading branch information
jackMort committed Dec 9, 2024
1 parent 5ff0f21 commit 2aca0b2
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 6 deletions.
254 changes: 253 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"restman/utils"
"strings"

"github.com/getkin/kin-openapi/openapi3"
"github.com/google/uuid"

tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -102,6 +103,7 @@ type Call struct {
Auth *Auth `json:"auth"`
Data string `json:"data"`
DataType string `json:"data_type"`
hash string
}

func NewCall() *Call {
Expand All @@ -112,6 +114,11 @@ func NewCall() *Call {
}
}

// function to check if Call was updated
func (i Call) WasChanged() bool {
return i.hash != utils.ComputeHash(i)
}

func (i Call) Title() string {
url := i.Url
if i.Name != "" {
Expand Down Expand Up @@ -149,7 +156,6 @@ func (i Call) HeadersCount() int {
}

func (i Call) ParamsCount() int {

items := make(map[string][]string)
u, err := url.Parse(i.Url)
if err == nil && i.Url != "" {
Expand Down Expand Up @@ -229,11 +235,44 @@ func (a *App) ReadCollectionsFromJSON() tea.Cmd {
}
json.Unmarshal(file, &a.Collections)

// filePath := "/home/jackmort/programming/gotest/petstorev3.json" // Replace with your OpenAPI spec file path
// collection, err := ImportOpenAPISpec(filePath)
//
// // append to Collections
// a.Collections = append(a.Collections, *collection)

// set hash for each call
for i, collection := range a.Collections {
for j, call := range collection.Calls {
a.Collections[i].Calls[j].hash = utils.ComputeHash(call)
}
}

return func() tea.Msg {
return FetchCollectionsSuccessMsg{Collections: a.Collections}
}
}

func (a *App) ImportCollectionFromUrl(url string) tea.Cmd {
// create temporary file
file, err := utils.DownloadToTempFile(url)
if err != nil {
// TODO: handle error
println(err)
return nil
}

// add to collection and save
collection, err := ImportOpenAPISpec(file)
if err != nil {
// TODO: handle error
println(err)
return nil
}

return a.CreateCollection(*collection)
}

func (a *App) SetSelectedCollection(collection *Collection) tea.Cmd {
a.SelectedCollection = collection
return func() tea.Msg {
Expand Down Expand Up @@ -284,6 +323,8 @@ func (a *App) UpdateCall(call *Call) tea.Cmd {
for j, c := range collection.Calls {
if c.ID == call.ID {
a.Collections[i].Calls[j] = *call
// compute hash so we can compare later
a.Collections[i].Calls[j].hash = utils.ComputeHash(*call)
}
}
}
Expand Down Expand Up @@ -442,3 +483,214 @@ func (a *App) RemoveCollection(collection Collection) tea.Cmd {
a.SetSelectedCollection(a.SelectedCollection),
)
}

func ImportOpenAPISpec(filePath string) (*Collection, error) {
// Load the OpenAPI spec
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(filePath)
if err != nil {
return nil, err
}

// Create a new Collection
collection := &Collection{
ID: "example-id",
Name: doc.Info.Title,
BaseUrl: getBaseUrl(doc),
Calls: []Call{},
}

// Iterate over paths in matching order
for _, path := range doc.Paths.InMatchingOrder() {
item := doc.Paths.Find(path)

for method, operation := range item.Operations() {
data, dataType := extractRequestBodyData(doc, operation)
call := Call{
ID: operation.OperationID,
Name: operation.Summary,
Url: genereatePartialUrl(path),
Method: method,
Headers: extractHeaders(operation),
Auth: extractAuth(doc, operation),
Data: data,
DataType: dataType,
}
collection.Calls = append(collection.Calls, call)
}
}

return collection, nil
}

func getBaseUrl(doc *openapi3.T) string {
if doc.Servers != nil && len(doc.Servers) > 0 {
return doc.Servers[0].URL
}
return ""
}

func genereatePartialUrl(path string) string {
if strings.HasPrefix(path, "http") {
return path
}
if strings.HasPrefix(path, "/") {
return "{{BASE_URL}}" + path
}
return "{{BASE_URL}}/" + path
}

func extractRequestBodyData(doc *openapi3.T, operation *openapi3.Operation) (string, string) {
if operation.RequestBody != nil && operation.RequestBody.Value != nil {
for contentType, mediaType := range operation.RequestBody.Value.Content {
// Check for direct examples
if example := mediaType.Example; example != nil {
if exampleData, err := json.Marshal(example); err == nil {
return string(exampleData), contentType
}
}
// Check for named examples
for _, example := range mediaType.Examples {
if example.Value != nil {
if exampleData, err := json.Marshal(example.Value.Value); err == nil {
return string(exampleData), contentType
}
}
}
// Check for schema examples
if schemaRef := mediaType.Schema; schemaRef != nil && schemaRef.Value != nil {
if exampleData, err := generateExampleFromSchema(doc, schemaRef.Value); err == nil {
return exampleData, contentType
}
}
}
}
return "", ""
}

func isType(schema *openapi3.Schema, t string) bool {
if schema.Type != nil {
for _, typ := range *schema.Type {
if typ == t {
return true
}
}
}
return false
}

func generateExampleFromSchema(doc *openapi3.T, schema *openapi3.Schema) (string, error) {
// If the schema has an example, use it
if schema.Example != nil {
exampleData, err := json.Marshal(schema.Example)
if err != nil {
return "", err
}
return string(exampleData), nil
}

// Handle object type schemas
if isType(schema, "object") {
example := make(map[string]interface{})
for propName, propSchemaRef := range schema.Properties {
propSchema := propSchemaRef.Value
if propSchema == nil {
continue
}
propExample, err := generateExampleFromSchema(doc, propSchema)
if err != nil {
return "", err
}
var propValue interface{}
if err := json.Unmarshal([]byte(propExample), &propValue); err != nil {
return "", err
}
example[propName] = propValue
}
exampleData, err := json.Marshal(example)
if err != nil {
return "", err
}
return string(exampleData), nil
}

// Handle array type schemas
if isType(schema, "array") && schema.Items != nil {
itemSchema := schema.Items.Value
if itemSchema == nil {
return "", nil
}
itemExample, err := generateExampleFromSchema(doc, itemSchema)
if err != nil {
return "", err
}
var itemValue interface{}
if err := json.Unmarshal([]byte(itemExample), &itemValue); err != nil {
return "", err
}
example := []interface{}{itemValue}
exampleData, err := json.Marshal(example)
if err != nil {
return "", err
}
return string(exampleData), nil
}

// Handle primitive types with default values
if schema.Default != nil {
exampleData, err := json.Marshal(schema.Default)
if err != nil {
return "", err
}
return string(exampleData), nil
}

return "", nil
}

func extractHeaders(operation *openapi3.Operation) []string {
headers := []string{}
for _, param := range operation.Parameters {
if param.Value.In == "header" {
headers = append(headers, param.Value.Name)
}
}
return headers
}

func extractAuth(doc *openapi3.T, operation *openapi3.Operation) *Auth {
if operation.Security != nil {
for _, security := range *operation.Security {
for name := range security {
// Ensure doc.Components and SecuritySchemes are not nil
if doc == nil || doc.Components == nil || doc.Components.SecuritySchemes == nil {
continue
}
scheme, ok := doc.Components.SecuritySchemes[name]
if ok && scheme != nil && scheme.Value != nil {
return mapSecurityScheme(scheme.Value)
}
}
}
}
return nil
}

func mapSecurityScheme(scheme *openapi3.SecurityScheme) *Auth {
switch scheme.Type {
case "http":
if scheme.Scheme == "basic" {
return &Auth{Type: "basic"}
}
if scheme.Scheme == "bearer" {
return &Auth{Type: "bearer", Token: ""}
}
case "apiKey":
return &Auth{
Type: "apiKey",
HeaderName: scheme.Name,
HeaderValue: "",
}
}
return nil
}
5 changes: 5 additions & 0 deletions components/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ var (
COLOR_LINK = lipgloss.AdaptiveColor{Light: "#6C9EF8", Dark: "#6C9EF8"}
COLOR_ERROR = lipgloss.AdaptiveColor{Light: "#F25C54", Dark: "#F25C54"}
COLOR_LIGHTER = lipgloss.AdaptiveColor{Light: "#9f9f9f", Dark: "#9f9f9f"}
COLOR_WARNING = lipgloss.AdaptiveColor{Light: "#FFB454", Dark: "#FFB454"}
)

const (
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
PATCH = "PATCH"
)

var methodColors = map[string]string{
GET: "#43BF6D",
POST: "#FFB454",
PUT: "#F2C94C",
DELETE: "#F25C54",
PATCH: "#6C9EF8",
}

var BoxHeader = lipgloss.NewStyle().
Expand Down Expand Up @@ -94,13 +97,15 @@ var Methods = map[string]string{
"POST": MethodStyle.Background(lipgloss.Color(methodColors["POST"])).Render("POST"),
"PUT": MethodStyle.Background(lipgloss.Color(methodColors["PUT"])).Render("PUT"),
"DELETE": MethodStyle.Background(lipgloss.Color(methodColors["DELETE"])).Render("DELETE"),
"PATCH": MethodStyle.Background(lipgloss.Color(methodColors["PATCH"])).Render("PATCH"),
}

var MethodsShort = map[string]string{
"GET": MethodStyleShort.Foreground(lipgloss.Color(methodColors["GET"])).Render("GET"),
"POST": MethodStyleShort.Foreground(lipgloss.Color(methodColors["POST"])).Render("POS"),
"PUT": MethodStyleShort.Foreground(lipgloss.Color(methodColors["PUT"])).Render("PUT"),
"DELETE": MethodStyleShort.Foreground(lipgloss.Color(methodColors["DELETE"])).Render("DEL"),
"PATCH": MethodStyleShort.Foreground(lipgloss.Color(methodColors["PATCH"])).Render("PAT"),
}

type WindowFocusedMsg struct {
Expand Down
Loading

0 comments on commit 2aca0b2

Please sign in to comment.