Skip to content

Commit 2aca0b2

Browse files
committed
feat(import): basic option on import collections from openai spec
1 parent 5ff0f21 commit 2aca0b2

File tree

9 files changed

+499
-6
lines changed

9 files changed

+499
-6
lines changed

app/app.go

Lines changed: 253 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"restman/utils"
1212
"strings"
1313

14+
"github.com/getkin/kin-openapi/openapi3"
1415
"github.com/google/uuid"
1516

1617
tea "github.com/charmbracelet/bubbletea"
@@ -102,6 +103,7 @@ type Call struct {
102103
Auth *Auth `json:"auth"`
103104
Data string `json:"data"`
104105
DataType string `json:"data_type"`
106+
hash string
105107
}
106108

107109
func NewCall() *Call {
@@ -112,6 +114,11 @@ func NewCall() *Call {
112114
}
113115
}
114116

117+
// function to check if Call was updated
118+
func (i Call) WasChanged() bool {
119+
return i.hash != utils.ComputeHash(i)
120+
}
121+
115122
func (i Call) Title() string {
116123
url := i.Url
117124
if i.Name != "" {
@@ -149,7 +156,6 @@ func (i Call) HeadersCount() int {
149156
}
150157

151158
func (i Call) ParamsCount() int {
152-
153159
items := make(map[string][]string)
154160
u, err := url.Parse(i.Url)
155161
if err == nil && i.Url != "" {
@@ -229,11 +235,44 @@ func (a *App) ReadCollectionsFromJSON() tea.Cmd {
229235
}
230236
json.Unmarshal(file, &a.Collections)
231237

238+
// filePath := "/home/jackmort/programming/gotest/petstorev3.json" // Replace with your OpenAPI spec file path
239+
// collection, err := ImportOpenAPISpec(filePath)
240+
//
241+
// // append to Collections
242+
// a.Collections = append(a.Collections, *collection)
243+
244+
// set hash for each call
245+
for i, collection := range a.Collections {
246+
for j, call := range collection.Calls {
247+
a.Collections[i].Calls[j].hash = utils.ComputeHash(call)
248+
}
249+
}
250+
232251
return func() tea.Msg {
233252
return FetchCollectionsSuccessMsg{Collections: a.Collections}
234253
}
235254
}
236255

256+
func (a *App) ImportCollectionFromUrl(url string) tea.Cmd {
257+
// create temporary file
258+
file, err := utils.DownloadToTempFile(url)
259+
if err != nil {
260+
// TODO: handle error
261+
println(err)
262+
return nil
263+
}
264+
265+
// add to collection and save
266+
collection, err := ImportOpenAPISpec(file)
267+
if err != nil {
268+
// TODO: handle error
269+
println(err)
270+
return nil
271+
}
272+
273+
return a.CreateCollection(*collection)
274+
}
275+
237276
func (a *App) SetSelectedCollection(collection *Collection) tea.Cmd {
238277
a.SelectedCollection = collection
239278
return func() tea.Msg {
@@ -284,6 +323,8 @@ func (a *App) UpdateCall(call *Call) tea.Cmd {
284323
for j, c := range collection.Calls {
285324
if c.ID == call.ID {
286325
a.Collections[i].Calls[j] = *call
326+
// compute hash so we can compare later
327+
a.Collections[i].Calls[j].hash = utils.ComputeHash(*call)
287328
}
288329
}
289330
}
@@ -442,3 +483,214 @@ func (a *App) RemoveCollection(collection Collection) tea.Cmd {
442483
a.SetSelectedCollection(a.SelectedCollection),
443484
)
444485
}
486+
487+
func ImportOpenAPISpec(filePath string) (*Collection, error) {
488+
// Load the OpenAPI spec
489+
loader := openapi3.NewLoader()
490+
doc, err := loader.LoadFromFile(filePath)
491+
if err != nil {
492+
return nil, err
493+
}
494+
495+
// Create a new Collection
496+
collection := &Collection{
497+
ID: "example-id",
498+
Name: doc.Info.Title,
499+
BaseUrl: getBaseUrl(doc),
500+
Calls: []Call{},
501+
}
502+
503+
// Iterate over paths in matching order
504+
for _, path := range doc.Paths.InMatchingOrder() {
505+
item := doc.Paths.Find(path)
506+
507+
for method, operation := range item.Operations() {
508+
data, dataType := extractRequestBodyData(doc, operation)
509+
call := Call{
510+
ID: operation.OperationID,
511+
Name: operation.Summary,
512+
Url: genereatePartialUrl(path),
513+
Method: method,
514+
Headers: extractHeaders(operation),
515+
Auth: extractAuth(doc, operation),
516+
Data: data,
517+
DataType: dataType,
518+
}
519+
collection.Calls = append(collection.Calls, call)
520+
}
521+
}
522+
523+
return collection, nil
524+
}
525+
526+
func getBaseUrl(doc *openapi3.T) string {
527+
if doc.Servers != nil && len(doc.Servers) > 0 {
528+
return doc.Servers[0].URL
529+
}
530+
return ""
531+
}
532+
533+
func genereatePartialUrl(path string) string {
534+
if strings.HasPrefix(path, "http") {
535+
return path
536+
}
537+
if strings.HasPrefix(path, "/") {
538+
return "{{BASE_URL}}" + path
539+
}
540+
return "{{BASE_URL}}/" + path
541+
}
542+
543+
func extractRequestBodyData(doc *openapi3.T, operation *openapi3.Operation) (string, string) {
544+
if operation.RequestBody != nil && operation.RequestBody.Value != nil {
545+
for contentType, mediaType := range operation.RequestBody.Value.Content {
546+
// Check for direct examples
547+
if example := mediaType.Example; example != nil {
548+
if exampleData, err := json.Marshal(example); err == nil {
549+
return string(exampleData), contentType
550+
}
551+
}
552+
// Check for named examples
553+
for _, example := range mediaType.Examples {
554+
if example.Value != nil {
555+
if exampleData, err := json.Marshal(example.Value.Value); err == nil {
556+
return string(exampleData), contentType
557+
}
558+
}
559+
}
560+
// Check for schema examples
561+
if schemaRef := mediaType.Schema; schemaRef != nil && schemaRef.Value != nil {
562+
if exampleData, err := generateExampleFromSchema(doc, schemaRef.Value); err == nil {
563+
return exampleData, contentType
564+
}
565+
}
566+
}
567+
}
568+
return "", ""
569+
}
570+
571+
func isType(schema *openapi3.Schema, t string) bool {
572+
if schema.Type != nil {
573+
for _, typ := range *schema.Type {
574+
if typ == t {
575+
return true
576+
}
577+
}
578+
}
579+
return false
580+
}
581+
582+
func generateExampleFromSchema(doc *openapi3.T, schema *openapi3.Schema) (string, error) {
583+
// If the schema has an example, use it
584+
if schema.Example != nil {
585+
exampleData, err := json.Marshal(schema.Example)
586+
if err != nil {
587+
return "", err
588+
}
589+
return string(exampleData), nil
590+
}
591+
592+
// Handle object type schemas
593+
if isType(schema, "object") {
594+
example := make(map[string]interface{})
595+
for propName, propSchemaRef := range schema.Properties {
596+
propSchema := propSchemaRef.Value
597+
if propSchema == nil {
598+
continue
599+
}
600+
propExample, err := generateExampleFromSchema(doc, propSchema)
601+
if err != nil {
602+
return "", err
603+
}
604+
var propValue interface{}
605+
if err := json.Unmarshal([]byte(propExample), &propValue); err != nil {
606+
return "", err
607+
}
608+
example[propName] = propValue
609+
}
610+
exampleData, err := json.Marshal(example)
611+
if err != nil {
612+
return "", err
613+
}
614+
return string(exampleData), nil
615+
}
616+
617+
// Handle array type schemas
618+
if isType(schema, "array") && schema.Items != nil {
619+
itemSchema := schema.Items.Value
620+
if itemSchema == nil {
621+
return "", nil
622+
}
623+
itemExample, err := generateExampleFromSchema(doc, itemSchema)
624+
if err != nil {
625+
return "", err
626+
}
627+
var itemValue interface{}
628+
if err := json.Unmarshal([]byte(itemExample), &itemValue); err != nil {
629+
return "", err
630+
}
631+
example := []interface{}{itemValue}
632+
exampleData, err := json.Marshal(example)
633+
if err != nil {
634+
return "", err
635+
}
636+
return string(exampleData), nil
637+
}
638+
639+
// Handle primitive types with default values
640+
if schema.Default != nil {
641+
exampleData, err := json.Marshal(schema.Default)
642+
if err != nil {
643+
return "", err
644+
}
645+
return string(exampleData), nil
646+
}
647+
648+
return "", nil
649+
}
650+
651+
func extractHeaders(operation *openapi3.Operation) []string {
652+
headers := []string{}
653+
for _, param := range operation.Parameters {
654+
if param.Value.In == "header" {
655+
headers = append(headers, param.Value.Name)
656+
}
657+
}
658+
return headers
659+
}
660+
661+
func extractAuth(doc *openapi3.T, operation *openapi3.Operation) *Auth {
662+
if operation.Security != nil {
663+
for _, security := range *operation.Security {
664+
for name := range security {
665+
// Ensure doc.Components and SecuritySchemes are not nil
666+
if doc == nil || doc.Components == nil || doc.Components.SecuritySchemes == nil {
667+
continue
668+
}
669+
scheme, ok := doc.Components.SecuritySchemes[name]
670+
if ok && scheme != nil && scheme.Value != nil {
671+
return mapSecurityScheme(scheme.Value)
672+
}
673+
}
674+
}
675+
}
676+
return nil
677+
}
678+
679+
func mapSecurityScheme(scheme *openapi3.SecurityScheme) *Auth {
680+
switch scheme.Type {
681+
case "http":
682+
if scheme.Scheme == "basic" {
683+
return &Auth{Type: "basic"}
684+
}
685+
if scheme.Scheme == "bearer" {
686+
return &Auth{Type: "bearer", Token: ""}
687+
}
688+
case "apiKey":
689+
return &Auth{
690+
Type: "apiKey",
691+
HeaderName: scheme.Name,
692+
HeaderValue: "",
693+
}
694+
}
695+
return nil
696+
}

components/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,23 @@ var (
1818
COLOR_LINK = lipgloss.AdaptiveColor{Light: "#6C9EF8", Dark: "#6C9EF8"}
1919
COLOR_ERROR = lipgloss.AdaptiveColor{Light: "#F25C54", Dark: "#F25C54"}
2020
COLOR_LIGHTER = lipgloss.AdaptiveColor{Light: "#9f9f9f", Dark: "#9f9f9f"}
21+
COLOR_WARNING = lipgloss.AdaptiveColor{Light: "#FFB454", Dark: "#FFB454"}
2122
)
2223

2324
const (
2425
GET = "GET"
2526
POST = "POST"
2627
PUT = "PUT"
2728
DELETE = "DELETE"
29+
PATCH = "PATCH"
2830
)
2931

3032
var methodColors = map[string]string{
3133
GET: "#43BF6D",
3234
POST: "#FFB454",
3335
PUT: "#F2C94C",
3436
DELETE: "#F25C54",
37+
PATCH: "#6C9EF8",
3538
}
3639

3740
var BoxHeader = lipgloss.NewStyle().
@@ -94,13 +97,15 @@ var Methods = map[string]string{
9497
"POST": MethodStyle.Background(lipgloss.Color(methodColors["POST"])).Render("POST"),
9598
"PUT": MethodStyle.Background(lipgloss.Color(methodColors["PUT"])).Render("PUT"),
9699
"DELETE": MethodStyle.Background(lipgloss.Color(methodColors["DELETE"])).Render("DELETE"),
100+
"PATCH": MethodStyle.Background(lipgloss.Color(methodColors["PATCH"])).Render("PATCH"),
97101
}
98102

99103
var MethodsShort = map[string]string{
100104
"GET": MethodStyleShort.Foreground(lipgloss.Color(methodColors["GET"])).Render("GET"),
101105
"POST": MethodStyleShort.Foreground(lipgloss.Color(methodColors["POST"])).Render("POS"),
102106
"PUT": MethodStyleShort.Foreground(lipgloss.Color(methodColors["PUT"])).Render("PUT"),
103107
"DELETE": MethodStyleShort.Foreground(lipgloss.Color(methodColors["DELETE"])).Render("DEL"),
108+
"PATCH": MethodStyleShort.Foreground(lipgloss.Color(methodColors["PATCH"])).Render("PAT"),
104109
}
105110

106111
type WindowFocusedMsg struct {

0 commit comments

Comments
 (0)