44package cmd
55
66import (
7+ "context"
78 "encoding/json"
89 "errors"
910 "flag"
1011 "fmt"
11- "log"
12+ "log/slog "
1213 "os"
1314 "strings"
1415
@@ -19,6 +20,7 @@ import (
1920
2021 "github.com/mitchellh/cli"
2122 "github.com/pb33f/libopenapi"
23+ "github.com/pb33f/libopenapi/resolver"
2224)
2325
2426type GenerateCommand struct {
@@ -30,8 +32,8 @@ type GenerateCommand struct {
3032
3133func (cmd * GenerateCommand ) Flags () * flag.FlagSet {
3234 fs := flag .NewFlagSet ("generate" , flag .ExitOnError )
33- fs .StringVar (& cmd .flagConfigPath , "config" , "./tfopenapigen_config .yml" , "path to config file (YAML)" )
34- fs .StringVar (& cmd .flagOutputPath , "output" , "" , "path to output generated Framework IR file (JSON)" )
35+ fs .StringVar (& cmd .flagConfigPath , "config" , "./generator_config .yml" , "path to generator config file (YAML)" )
36+ fs .StringVar (& cmd .flagOutputPath , "output" , "./provider_code_spec.json " , "destination file path for generated provider code spec (JSON)" )
3537 return fs
3638}
3739
@@ -74,137 +76,153 @@ func (cmd *GenerateCommand) Help() string {
7476}
7577
7678func (cmd * GenerateCommand ) Synopsis () string {
77- return "Generates Framework Intermediate Representation (IR) JSON for an OpenAPI spec (JSON or YAML format) "
79+ return "Generates Provider Code Specification from an OpenAPI 3.x Specification "
7880}
7981
8082func (cmd * GenerateCommand ) Run (args []string ) int {
83+ logger := slog .New (slog .NewTextHandler (os .Stdout , & slog.HandlerOptions {
84+ Level : slog .LevelWarn ,
85+ }))
86+
8187 fs := cmd .Flags ()
8288 err := fs .Parse (args )
8389 if err != nil {
84- cmd . UI . Error (fmt . Sprintf ( "unable to parse flags: %s" , err ) )
90+ logger . Error ("error parsing flags" , "err" , err )
8591 return 1
8692 }
8793
8894 cmd .oasInputPath = fs .Arg (0 )
8995 if cmd .oasInputPath == "" {
90- cmd . UI . Error ("Error executing command: OpenAPI specification file is required as last argument" )
96+ logger . Error ("error executing command" , "err" , " OpenAPI specification file is required as last argument" )
9197 return 1
9298 }
9399
94- err = cmd .runInternal ()
100+ err = cmd .runInternal (logger )
95101 if err != nil {
96- cmd . UI . Error (fmt . Sprintf ( "Error executing command: %s \n " , err ) )
102+ logger . Error ("error executing command" , "err" , err )
97103 return 1
98104 }
99105
100106 return 0
101107}
102108
103- func (cmd * GenerateCommand ) runInternal () error {
109+ func (cmd * GenerateCommand ) runInternal (logger * slog. Logger ) error {
104110 // 1. Read and parse generator config file
105111 configBytes , err := os .ReadFile (cmd .flagConfigPath )
106112 if err != nil {
107- return fmt .Errorf ("failed to read generator config file: %w" , err )
113+ return fmt .Errorf ("error reading generator config file: %w" , err )
108114 }
109115 config , err := config .ParseConfig (configBytes )
110116 if err != nil {
111- return fmt .Errorf ("failed to parse generator config file: %w" , err )
117+ return fmt .Errorf ("error parsing generator config file: %w" , err )
112118 }
113119
114120 // 2. Read and parse OpenAPI spec file
115121 oasBytes , err := os .ReadFile (cmd .oasInputPath )
116122 if err != nil {
117- return fmt .Errorf ("failed to read OpenAPI spec file: %w" , err )
123+ return fmt .Errorf ("error reading OpenAPI spec file: %w" , err )
118124 }
119125 doc , err := libopenapi .NewDocument (oasBytes )
120126 if err != nil {
121- return fmt .Errorf ("failed to parse OpenAPI spec file: %w" , err )
127+ return fmt .Errorf ("error parsing OpenAPI spec file: %w" , err )
122128 }
123129
124130 // 3. Build out the OpenAPI model, this will recursively load all local + remote references into one cohesive model
125131 model , errs := doc .BuildV3Model ()
126- // TODO: Determine how to handle circular ref errors - https://pb33f.io/libopenapi/circular-references/
127- if len (errs ) > 0 {
128- var errResult error
129- for _ , err := range errs {
130- errResult = errors .Join (errResult , err )
132+
133+ // 4. Log circular references as warnings and fail on any other model building errors
134+ var errResult error
135+ for _ , err := range errs {
136+ if rslvErr , ok := err .(* resolver.ResolvingError ); ok {
137+ logger .Warn (
138+ "circular reference found in OpenAPI spec" ,
139+ "circular_ref" , rslvErr .CircularReference .GenerateJourneyPath ())
140+ continue
131141 }
132- log .Printf ("[WARN] Potential issues in model spec: %s" , errResult )
142+
143+ errResult = errors .Join (errResult , err )
144+ }
145+ if errResult != nil {
146+ return fmt .Errorf ("error building OpenAPI 3.x model: %w" , errResult )
133147 }
134148
135- // 4 . Generate framework IR w/ config
149+ // 5 . Generate provider code spec w/ config
136150 oasExplorer := explorer .NewConfigExplorer (model .Model , * config )
137- frameworkIr , err := generateFrameworkIr ( oasExplorer , * config )
151+ providerCodeSpec , err := generateProviderCodeSpec ( logger , oasExplorer , * config )
138152 if err != nil {
139153 return err
140154 }
141155
142- // 5 . Use framework IR to create JSON
143- bytes , err := json .MarshalIndent (frameworkIr , "" , "\t " )
156+ // 6 . Use provider code spec to create JSON
157+ bytes , err := json .MarshalIndent (providerCodeSpec , "" , "\t " )
144158 if err != nil {
145- return fmt .Errorf ("error marshalling Framework IR to JSON: %w" , err )
159+ return fmt .Errorf ("error marshalling provider code spec to JSON: %w" , err )
146160 }
147161
148- // 6. Output to STDOUT or file
149- if cmd .flagOutputPath == "" {
150- cmd .UI .Output (string (bytes ))
151- return nil
162+ // 7. Log a warning if the provider code spec is not valid based on the JSON schema
163+ err = spec .Validate (context .Background (), bytes )
164+ if err != nil {
165+ logger .Warn (
166+ "generated provider code spec failed validation" ,
167+ "validation_msg" , err )
152168 }
153169
170+ // 8. Output to file
154171 output , err := os .Create (cmd .flagOutputPath )
155172 if err != nil {
156- return fmt .Errorf ("error creating output file for Framework IR : %w" , err )
173+ return fmt .Errorf ("error creating output file for provider code spec : %w" , err )
157174 }
158175
159176 _ , err = output .Write (bytes )
160177 if err != nil {
161- return fmt .Errorf ("error writing framework IR to output: %w" , err )
178+ return fmt .Errorf ("error writing provider code spec to output: %w" , err )
162179 }
163180
164181 return nil
165182}
166183
167- func generateFrameworkIr ( dora explorer.Explorer , cfg config.Config ) (* spec.Specification , error ) {
168- // 1. Find TF resources
184+ func generateProviderCodeSpec ( logger * slog. Logger , dora explorer.Explorer , cfg config.Config ) (* spec.Specification , error ) {
185+ // 1. Find TF resources in OAS
169186 explorerResources , err := dora .FindResources ()
170187 if err != nil {
171- return nil , fmt .Errorf ("error finding resources : %w" , err )
188+ return nil , fmt .Errorf ("error finding resource(s) : %w" , err )
172189 }
173190
174- // 2. Find TF data sources
191+ // 2. Find TF data sources in OAS
175192 explorerDataSources , err := dora .FindDataSources ()
176193 if err != nil {
177- return nil , fmt .Errorf ("error finding data sources : %w" , err )
194+ return nil , fmt .Errorf ("error finding data source(s) : %w" , err )
178195 }
179196
180- // 3. Find TF provider
197+ // 3. Find TF provider in OAS
181198 explorerProvider , err := dora .FindProvider ()
182199 if err != nil {
183200 return nil , fmt .Errorf ("error finding provider: %w" , err )
184201 }
185202
186- // 4. Use TF info to generate framework IR for resources
203+ // 4. Use TF info to generate provider code spec for resources
187204 resourceMapper := mapper .NewResourceMapper (explorerResources , cfg )
188- resourcesIR , err := resourceMapper .MapToIR ()
205+ resourcesIR , err := resourceMapper .MapToIR (logger )
189206 if err != nil {
190- return nil , fmt .Errorf ("error generating Framework IR for resources: %w" , err )
207+ return nil , fmt .Errorf ("error generating provider code spec for resources: %w" , err )
191208 }
192209
193- // 5. Use TF info to generate framework IR for data sources
210+ // 5. Use TF info to generate provider code spec for data sources
194211 dataSourceMapper := mapper .NewDataSourceMapper (explorerDataSources , cfg )
195- dataSourcesIR , err := dataSourceMapper .MapToIR ()
212+ dataSourcesIR , err := dataSourceMapper .MapToIR (logger )
196213 if err != nil {
197- return nil , fmt .Errorf ("error generating Framework IR for data sources: %w" , err )
214+ return nil , fmt .Errorf ("error generating provider code spec for data sources: %w" , err )
198215 }
199216
200- // 6. Use TF info to generate framework IR for provider
217+ // 6. Use TF info to generate provider code spec for provider
201218 providerMapper := mapper .NewProviderMapper (explorerProvider , cfg )
202- providerIR , err := providerMapper .MapToIR ()
219+ providerIR , err := providerMapper .MapToIR (logger )
203220 if err != nil {
204- return nil , fmt .Errorf ("error generating Framework IR for provider: %w" , err )
221+ return nil , fmt .Errorf ("error generating provider code spec for provider: %w" , err )
205222 }
206223
207224 return & spec.Specification {
225+ Version : spec .Version0_1 ,
208226 Provider : providerIR ,
209227 Resources : resourcesIR ,
210228 DataSources : dataSourcesIR ,
0 commit comments