@@ -3,6 +3,7 @@ package openapi_test
33import (
44 "bytes"
55 "os"
6+ "strings"
67 "testing"
78
89 "github.com/speakeasy-api/openapi/openapi"
@@ -120,3 +121,123 @@ func TestInline_SiblingDirectories_Success(t *testing.T) {
120121 // Compare the actual output with expected output
121122 assert .Equal (t , string (expectedBytes ), string (actualYAML ), "Inlined document should match expected output" )
122123}
124+
125+ func TestInline_AdditionalOperations_Success (t * testing.T ) {
126+ t .Parallel ()
127+
128+ ctx := t .Context ()
129+
130+ // Load the input document with additionalOperations
131+ inputFile , err := os .Open ("testdata/inline/additionaloperations_input.yaml" )
132+ require .NoError (t , err )
133+ defer inputFile .Close ()
134+
135+ inputDoc , validationErrs , err := openapi .Unmarshal (ctx , inputFile )
136+ require .NoError (t , err )
137+ require .Empty (t , validationErrs , "Input document should be valid" )
138+
139+ // Configure inlining options
140+ opts := openapi.InlineOptions {
141+ ResolveOptions : openapi.ResolveOptions {
142+ RootDocument : inputDoc ,
143+ TargetLocation : "testdata/inline/additionaloperations_input.yaml" ,
144+ },
145+ RemoveUnusedComponents : true ,
146+ }
147+
148+ // Inline all references
149+ err = openapi .Inline (ctx , inputDoc , opts )
150+ require .NoError (t , err )
151+
152+ // Marshal the inlined document to YAML
153+ var buf bytes.Buffer
154+ err = openapi .Marshal (ctx , inputDoc , & buf )
155+ require .NoError (t , err )
156+ actualYAML := buf .String ()
157+
158+ // Verify that additionalOperations are preserved
159+ assert .Contains (t , actualYAML , "additionalOperations:" , "additionalOperations should be preserved" )
160+
161+ // Verify that external references in additionalOperations were inlined
162+ assert .NotContains (t , actualYAML , "$ref:" , "No references should remain after inlining" )
163+ assert .NotContains (t , actualYAML , "external_custom_operations.yaml" , "No external file references should remain" )
164+
165+ // Verify that the COPY operation has inlined content
166+ assert .Contains (t , actualYAML , "COPY:" , "COPY operation should be present" )
167+ assert .Contains (t , actualYAML , "operationId: copyResource" , "COPY operation content should be inlined" )
168+
169+ // Verify that external parameter was inlined in COPY operation
170+ copyOperationSection := extractAdditionalOperationSection (actualYAML , "COPY" )
171+ assert .Contains (t , copyOperationSection , "name: destination" , "DestinationParam should be inlined" )
172+ assert .Contains (t , copyOperationSection , "in: header" , "DestinationParam should be inlined" )
173+
174+ // Verify that external request body was inlined in COPY operation
175+ assert .Contains (t , copyOperationSection , "source_path:" , "CopyRequest schema should be inlined" )
176+ assert .Contains (t , copyOperationSection , "destination_path:" , "CopyRequest schema should be inlined" )
177+
178+ // Verify that the PURGE operation has inlined content
179+ assert .Contains (t , actualYAML , "PURGE:" , "PURGE operation should be present" )
180+ assert .Contains (t , actualYAML , "operationId: purgeResource" , "PURGE operation content should be inlined" )
181+
182+ // Verify that external parameter was inlined in PURGE operation
183+ purgeOperationSection := extractAdditionalOperationSection (actualYAML , "PURGE" )
184+ assert .Contains (t , purgeOperationSection , "name: X-Confirm-Purge" , "ConfirmationParam should be inlined" )
185+ assert .Contains (t , purgeOperationSection , "pattern: ^CONFIRM-[A-Z0-9]{8}$" , "ConfirmationParam schema should be inlined" )
186+
187+ // Verify that the SYNC operation has inlined content
188+ assert .Contains (t , actualYAML , "SYNC:" , "SYNC operation should be present" )
189+ assert .Contains (t , actualYAML , "operationId: syncResource" , "SYNC operation content should be inlined" )
190+
191+ // Verify that external schemas were inlined in SYNC operation
192+ syncOperationSection := extractAdditionalOperationSection (actualYAML , "SYNC" )
193+ assert .Contains (t , syncOperationSection , "source:" , "SyncConfig schema should be inlined" )
194+ assert .Contains (t , syncOperationSection , "destination:" , "SyncConfig schema should be inlined" )
195+ assert .Contains (t , syncOperationSection , "sync_id:" , "SyncResult schema should be inlined" )
196+ assert .Contains (t , syncOperationSection , "files_synced:" , "SyncResult schema should be inlined" )
197+
198+ // Verify that the BATCH operation has inlined content
199+ assert .Contains (t , actualYAML , "BATCH:" , "BATCH operation should be present" )
200+ assert .Contains (t , actualYAML , "operationId: batchProcess" , "BATCH operation content should be inlined" )
201+
202+ // Verify that nested external schemas were properly inlined
203+ batchOperationSection := extractAdditionalOperationSection (actualYAML , "BATCH" )
204+ assert .Contains (t , batchOperationSection , "parallel_execution:" , "BatchConfig schema should be inlined" )
205+ assert .Contains (t , batchOperationSection , "batch_id:" , "BatchResult schema should be inlined" )
206+ assert .Contains (t , batchOperationSection , "max_attempts:" , "RetryPolicy schema should be inlined" )
207+
208+ // Verify components section was removed (since RemoveUnusedComponents is true)
209+ // Note: Some components might remain if they're still referenced from the main document
210+ if ! assert .NotContains (t , actualYAML , "components:" , "Components section should be removed after inlining" ) {
211+ // If components section exists, ensure it doesn't contain the external schemas
212+ assert .NotContains (t , actualYAML , "ResourceMetadata:" , "External ResourceMetadata should not be in components after inlining" )
213+ assert .NotContains (t , actualYAML , "SyncConfig:" , "External SyncConfig should not be in components after inlining" )
214+ }
215+ }
216+
217+ // Helper function to extract a specific additionalOperation section from YAML
218+ func extractAdditionalOperationSection (yamlContent , operationName string ) string {
219+ lines := strings .Split (yamlContent , "\n " )
220+ var sectionLines []string
221+ inTargetOperation := false
222+ indentLevel := - 1
223+
224+ for _ , line := range lines {
225+ if strings .Contains (line , operationName + ":" ) && strings .Contains (line , "additionalOperations" ) == false {
226+ inTargetOperation = true
227+ indentLevel = len (line ) - len (strings .TrimLeft (line , " " ))
228+ sectionLines = append (sectionLines , line )
229+ continue
230+ }
231+
232+ if inTargetOperation {
233+ currentIndent := len (line ) - len (strings .TrimLeft (line , " " ))
234+ // If we hit a line at the same or lower indent level, we've left the operation
235+ if strings .TrimSpace (line ) != "" && currentIndent <= indentLevel {
236+ break
237+ }
238+ sectionLines = append (sectionLines , line )
239+ }
240+ }
241+
242+ return strings .Join (sectionLines , "\n " )
243+ }
0 commit comments