@@ -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
107109func 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+
115122func (i Call ) Title () string {
116123 url := i .Url
117124 if i .Name != "" {
@@ -149,7 +156,6 @@ func (i Call) HeadersCount() int {
149156}
150157
151158func (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+
237276func (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+ }
0 commit comments