Skip to content

Commit c8b75c2

Browse files
authored
Merge pull request #29 from speakeasy-api/ad/init-cmd-build-from-config
Ad/init cmd build from config
2 parents 05655f9 + f67874f commit c8b75c2

8 files changed

Lines changed: 419 additions & 184 deletions

File tree

cmd/speakeasy/build.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"regexp"
8+
9+
"github.com/speakeasy-api/parser/apipackage"
10+
"github.com/speakeasy-api/parser/services/parser"
11+
"github.com/urfave/cli/v2"
12+
"gopkg.in/yaml.v2"
13+
)
14+
15+
type SpeakeasyConfig struct {
16+
Name string
17+
Spec struct {
18+
Version string
19+
Schemas struct {
20+
Type string
21+
Version string
22+
Output string
23+
}
24+
}
25+
Root string
26+
}
27+
28+
const configOutputRegexp = "(yaml|json)"
29+
30+
var buildFlags = []cli.Flag{
31+
&cli.StringFlag{
32+
Name: searchDirFlag,
33+
Aliases: []string{"d"},
34+
Value: "./",
35+
Usage: "Directories you want to parse,comma separated and general-info file must be in the first one",
36+
},
37+
&cli.StringFlag{
38+
Name: excludeFlag,
39+
Usage: "Exclude directories and files when searching, comma separated",
40+
},
41+
&cli.StringFlag{
42+
Name: propertyStrategyFlag,
43+
Aliases: []string{"p"},
44+
Value: parser.CamelCase,
45+
Usage: "Property Naming Strategy like " + parser.SnakeCase + "," + parser.CamelCase + "," + parser.PascalCase,
46+
},
47+
&cli.StringFlag{
48+
Name: outputFlag,
49+
Aliases: []string{"o"},
50+
Value: "./docs",
51+
Usage: "Output directory for all the generated files(opeanapi.json, opeanapi.yaml)",
52+
},
53+
&cli.StringFlag{
54+
Name: configFileFlag,
55+
Aliases: []string{"c"},
56+
Value: speakeasyConfigFileName,
57+
Usage: "Yaml file to load speakeasy configuration from",
58+
},
59+
&cli.BoolFlag{
60+
Name: parseVendorFlag,
61+
Usage: "Parse go files in 'vendor' folder, disabled by default",
62+
},
63+
&cli.BoolFlag{
64+
Name: parseDependencyFlag,
65+
Aliases: []string{"pd"},
66+
Usage: "Parse go files inside dependency folder, disabled by default",
67+
},
68+
&cli.StringFlag{
69+
Name: markdownFilesFlag,
70+
Aliases: []string{"md"},
71+
Value: "",
72+
Usage: "Parse folder containing markdown files to use as description, disabled by default",
73+
},
74+
&cli.StringFlag{
75+
Name: codeExampleFilesFlag,
76+
Aliases: []string{"cef"},
77+
Value: "",
78+
Usage: "Parse folder containing code example files to use for the x-codeSamples extension, disabled by default",
79+
},
80+
&cli.BoolFlag{
81+
Name: parseInternalFlag,
82+
Usage: "Parse go files in internal packages, disabled by default",
83+
},
84+
&cli.BoolFlag{
85+
Name: generatedTimeFlag,
86+
Usage: "Generate timestamp at the top of docs.go, disabled by default",
87+
},
88+
&cli.IntFlag{
89+
Name: parseDepthFlag,
90+
Value: 100,
91+
Usage: "Dependency parse depth",
92+
},
93+
&cli.StringFlag{
94+
Name: instanceNameFlag,
95+
Value: "",
96+
Usage: "This parameter can be used to name different schema(openapi) document instances. It is optional.",
97+
},
98+
&cli.StringFlag{
99+
Name: overridesFileFlag,
100+
Value: apipackage.DefaultOverridesFile,
101+
Usage: "File to read global type overrides from.",
102+
},
103+
}
104+
105+
func readConfig(fileName string) ([]SpeakeasyConfig, error) {
106+
content, err := os.ReadFile(fileName)
107+
if err != nil {
108+
return []SpeakeasyConfig{}, err
109+
}
110+
111+
reader := bytes.NewReader(content)
112+
decoder := yaml.NewDecoder(reader)
113+
114+
var configs []SpeakeasyConfig
115+
config := SpeakeasyConfig{}
116+
for decoder.Decode(&config) == nil {
117+
configs = append(configs, config)
118+
}
119+
return configs, nil
120+
}
121+
122+
func buildAction(c *cli.Context) error {
123+
strategy := c.String(propertyStrategyFlag)
124+
125+
switch strategy {
126+
case parser.CamelCase, parser.SnakeCase, parser.PascalCase:
127+
default:
128+
return fmt.Errorf("not supported %s propertyStrategy", strategy)
129+
}
130+
131+
var configs, err = readConfig(c.String(configFileFlag))
132+
if err != nil {
133+
return err
134+
}
135+
136+
if len(configs) == 0 {
137+
return fmt.Errorf("no valid configurations found")
138+
}
139+
140+
for _, config := range configs {
141+
r, err := regexp.Compile(configOutputRegexp)
142+
if err != nil {
143+
return err
144+
}
145+
146+
outputTypes := r.FindAllString(config.Spec.Schemas.Output, -1)
147+
if len(outputTypes) == 0 {
148+
return fmt.Errorf("no valid output types specified")
149+
}
150+
151+
err = apipackage.New().Build(&apipackage.Config{
152+
SearchDir: c.String(searchDirFlag),
153+
Excludes: c.String(excludeFlag),
154+
MainAPIFile: config.Root,
155+
PropNamingStrategy: strategy,
156+
// TODO: use the -o flag for the output dir and add property "OutputFile" to
157+
// parser config to determine the output-file name.
158+
OutputDir: fmt.Sprintf("schemas/%s", config.Name),
159+
OutputTypes: outputTypes,
160+
ParseVendor: c.Bool(parseVendorFlag),
161+
ParseDependency: c.Bool(parseDependencyFlag),
162+
MarkdownFilesDir: c.String(markdownFilesFlag),
163+
ParseInternal: c.Bool(parseInternalFlag),
164+
GeneratedTime: c.Bool(generatedTimeFlag),
165+
CodeExampleFilesDir: c.String(codeExampleFilesFlag),
166+
ParseDepth: c.Int(parseDepthFlag),
167+
InstanceName: c.String(instanceNameFlag),
168+
OverridesFile: c.String(overridesFileFlag),
169+
})
170+
if err != nil {
171+
return err
172+
}
173+
}
174+
return nil
175+
}

cmd/speakeasy/build_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"testing"
8+
9+
"github.com/speakeasy-api/parser/services/parser"
10+
"github.com/urfave/cli/v2"
11+
)
12+
13+
const testOutputDirectory = ".buildTestOutput"
14+
15+
func TestBuild(t *testing.T) {
16+
defer func() {
17+
// Clean up temporary output directories.
18+
err := os.RemoveAll("schemas")
19+
if err != nil {
20+
panic(err)
21+
}
22+
}()
23+
24+
validConfigFile := "test_fixtures/speakeasy.yaml"
25+
invalidConfigFile := "test_fixtures/invalidConfigFile.yaml"
26+
27+
tests := []struct {
28+
configFile, strategy string
29+
expected string
30+
}{
31+
{validConfigFile, parser.CamelCase, ""},
32+
{validConfigFile, parser.SnakeCase, ""},
33+
{validConfigFile, parser.PascalCase, ""},
34+
{validConfigFile, parser.PascalCase, ""},
35+
{validConfigFile, "invalidStrategy", "not supported invalidStrategy propertyStrategy"},
36+
{invalidConfigFile, parser.PascalCase, fmt.Sprintf("open %s: no such file or directory", invalidConfigFile)},
37+
{"test_fixtures/speakeasy_with_invalid_output.yaml", parser.PascalCase, "no valid output types specified"},
38+
}
39+
40+
for _, test := range tests {
41+
42+
// Create temporary output directory.
43+
if _, err := os.Stat(testOutputDirectory); os.IsNotExist(err) {
44+
err := os.Mkdir(testOutputDirectory, os.ModePerm)
45+
if err != nil {
46+
panic(err)
47+
}
48+
}
49+
50+
t.Run(fmt.Sprintf("%s, %s", test.configFile, test.strategy), func(t *testing.T) {
51+
set := flag.NewFlagSet("test", 0)
52+
set.String(configFileFlag, test.configFile, "yaml")
53+
set.String(propertyStrategyFlag, test.strategy, "strategy")
54+
set.String(searchDirFlag, "test_fixtures", "search")
55+
set.String(generalInfoFlag, "fixture.go", "generalInfo")
56+
57+
actual := buildAction(cli.NewContext(nil, set, nil))
58+
59+
if actual == nil {
60+
if test.expected != "" {
61+
t.Errorf("Got nil, expected error with message '%s'", test.expected)
62+
}
63+
} else if actual.Error() != test.expected {
64+
t.Errorf("Got error with message %s, expected message %s", actual.Error(), test.expected)
65+
}
66+
})
67+
}
68+
}

cmd/speakeasy/init.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/urfave/cli/v2"
8+
)
9+
10+
var initFlags = []cli.Flag{
11+
&cli.StringFlag{
12+
Name: generalInfoFlag,
13+
Aliases: []string{"g"},
14+
Value: "main.go",
15+
Usage: "Go file path in which 'OpenAPI general API Info' is written",
16+
},
17+
&cli.StringFlag{
18+
Name: apiNameFlag,
19+
Aliases: []string{"n"},
20+
Value: "main_api",
21+
Usage: "Name of the api",
22+
},
23+
}
24+
25+
const (
26+
actionFileName = ".github/workflows/speakeasy.yaml"
27+
apiNameVariable = "SPEAKEASY_API_NAME"
28+
apiRootVariable = "SPEAKEASY_API_ROOT"
29+
)
30+
31+
func writeSliceToFile(stringsToWrite []string, fileName string) error {
32+
file, err := os.Create(fileName)
33+
if err != nil {
34+
return err
35+
}
36+
defer file.Close()
37+
38+
for _, s := range stringsToWrite {
39+
_, err = file.WriteString(fmt.Sprintf("%s\n", s))
40+
if err != nil {
41+
return err
42+
}
43+
}
44+
return nil
45+
}
46+
47+
func buildConfigStrings(c *cli.Context) []string {
48+
nameString := fmt.Sprintf("name: %s", c.String(apiNameFlag))
49+
versionString := "\tversion: v1"
50+
openApiString := "\t\tOpenAPI3.0\n\t\tversion: 1.0.0"
51+
rootString := fmt.Sprintf("\troot: %s", c.String(generalInfoFlag))
52+
return []string{nameString, versionString, openApiString, rootString}
53+
}
54+
55+
func buildActionStrings() []string {
56+
// \n adds line of white-space
57+
nameString := "name: Run Speakeasy CLI\n"
58+
jobsString := "jobs:\n\tsetup_and_run_speakeasy:\n"
59+
containerString := "\t\truns-on: ubuntu-latest\n\n\t\tpermissions:\n\t\t\tcontents: 'read'\n\t\t\tid-token: 'write'\n"
60+
stepsString := "\t\tsteps:"
61+
checkoutString := "\t\t\t- name: Checkout\n\t\t\t\tuses: actions/checkout@v3\n\t\t\t\twith:\n\t\t\t\t\tref: ${{ github.head_ref }}\n"
62+
downloadString := "\t\t\t- name: Download Speakeasy\n\t\t\t\tuses: speakeasy-api/speakeasy-github-action\n"
63+
64+
// This builds and executes the speakeasy build command
65+
runString := "\t\t- name: Setup and Update API state\n\t\t\trun: speakeasy build"
66+
67+
// The changes should be committed
68+
commitString := "\t\t- name: Commit API state\n\t\t\trun: git add schemas; git commit -m \"[no ci]Add schema files\"; git push\n"
69+
70+
return []string{nameString, jobsString, containerString, stepsString, checkoutString, downloadString, runString, commitString}
71+
}
72+
73+
func initAction(c *cli.Context) error {
74+
configStrings := buildConfigStrings(c)
75+
err := writeSliceToFile(configStrings, speakeasyConfigFileName)
76+
if err != nil {
77+
return err
78+
}
79+
80+
buildActionStrings := buildActionStrings()
81+
err = writeSliceToFile(buildActionStrings, actionFileName)
82+
if err != nil {
83+
return err
84+
}
85+
return nil
86+
}

cmd/speakeasy/init_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/urfave/cli/v2"
9+
)
10+
11+
func TestBuildConfigStrings(t *testing.T) {
12+
var tests = []struct {
13+
name, generalInfo string
14+
expected []string
15+
}{
16+
{"api_name", "main.go", []string{"name: api_name", "\troot: main.go"}},
17+
{"other_api", "controller.go", []string{"name: other_api", "\troot: controller.go"}},
18+
}
19+
expectedLength := 4
20+
21+
for _, test := range tests {
22+
23+
t.Run(fmt.Sprintf("%s, %s", test.name, test.generalInfo), func(t *testing.T) {
24+
set := flag.NewFlagSet("test", 0)
25+
set.String(apiNameFlag, test.name, "name")
26+
set.String(generalInfoFlag, test.generalInfo, "generalInfo")
27+
28+
actual := buildConfigStrings(cli.NewContext(nil, set, nil))
29+
30+
if len(actual) != expectedLength {
31+
t.Errorf("Receieved %d strings, expected %d", len(actual), expectedLength)
32+
}
33+
if actual[0] != test.expected[0] {
34+
t.Errorf("Received %s, expected %s", actual[0], test.expected[0])
35+
}
36+
if actual[3] != test.expected[1] {
37+
t.Errorf("Received %s, expected %s", actual[3], test.expected[1])
38+
}
39+
})
40+
}
41+
}
42+
43+
func TestBuildActionStrings(t *testing.T) {
44+
expectedCount := 8
45+
t.Run("action string count", func(t *testing.T) {
46+
actual := buildActionStrings()
47+
48+
if len(actual) != expectedCount {
49+
t.Errorf("Received %d strings, expected %d", len(actual), expectedCount)
50+
}
51+
})
52+
}

0 commit comments

Comments
 (0)