66import com .retailsvc .http .spec .security .SecurityScheme ;
77import com .retailsvc .http .spec .security .SecuritySchemeParser ;
88import java .io .IOException ;
9+ import java .io .InputStream ;
910import java .io .UncheckedIOException ;
1011import java .lang .reflect .Method ;
1112import java .net .URI ;
13+ import java .nio .charset .StandardCharsets ;
1214import java .nio .file .Files ;
1315import java .nio .file .Path ;
1416import java .util .ArrayList ;
1517import java .util .LinkedHashMap ;
1618import java .util .List ;
1719import java .util .Locale ;
1820import java .util .Map ;
21+ import java .util .Objects ;
1922import java .util .Optional ;
23+ import java .util .function .Function ;
2024
2125public record Spec (
2226 String openapi ,
@@ -67,18 +71,18 @@ static Map<String, Object> extractExtensions(Map<String, Object> raw) {
6771 * has an unrecognised extension
6872 */
6973 public static Spec fromPath (Path path ) {
70- String text ;
74+ byte [] bytes ;
7175 try {
72- text = Files .readString (path );
76+ bytes = Files .readAllBytes (path );
7377 } catch (IOException e ) {
7478 throw new UncheckedIOException ("Failed to read OpenAPI spec from " + path , e );
7579 }
7680 String name = path .getFileName ().toString ().toLowerCase (Locale .ROOT );
7781 Map <String , Object > raw ;
7882 if (name .endsWith (".json" )) {
79- raw = parseJson ( text );
83+ raw = parseJsonWithGson ( bytes );
8084 } else if (name .endsWith (".yaml" ) || name .endsWith (".yml" )) {
81- raw = parseYaml ( text );
85+ raw = parseYamlWithSnakeYaml ( bytes );
8286 } else {
8387 throw new IllegalStateException (
8488 "Unrecognised OpenAPI spec extension for "
@@ -89,8 +93,89 @@ public static Spec fromPath(Path path) {
8993 return from (raw );
9094 }
9195
92- private static Map <String , Object > parseJson (String text ) {
93- Class <?> gsonClass = loadOptional (GSON_CLASS , "JSON" , "Gson" );
96+ /**
97+ * Reads a JSON OpenAPI specification from {@code in} using Gson. Gson must be on the classpath;
98+ * otherwise throws {@link IllegalStateException}. The stream is fully consumed and closed before
99+ * this method returns.
100+ *
101+ * <p>Useful for loading specs from the classpath:
102+ *
103+ * <pre>{@code
104+ * try (InputStream in = getClass().getResourceAsStream("/openapi.json")) {
105+ * Spec spec = Spec.fromJson(in);
106+ * }
107+ * }</pre>
108+ *
109+ * <p>To avoid the Gson dependency (e.g. when using Jackson), use {@link #fromJson(InputStream,
110+ * Function)} instead.
111+ *
112+ * @throws NullPointerException if {@code in} is {@code null}
113+ * @throws UncheckedIOException if the stream cannot be read
114+ * @throws IllegalStateException if Gson is not on the classpath
115+ */
116+ public static Spec fromJson (InputStream in ) {
117+ return fromJson (in , Spec ::parseJsonWithGson );
118+ }
119+
120+ /**
121+ * Reads a JSON OpenAPI specification from {@code in} using the supplied {@code parser}. The
122+ * parser receives the full body as bytes and returns the decoded map. The stream is fully
123+ * consumed and closed before this method returns.
124+ *
125+ * <p>Example with Jackson:
126+ *
127+ * <pre>{@code
128+ * ObjectMapper mapper = new ObjectMapper();
129+ * Spec spec = Spec.fromJson(in, bytes -> mapper.readValue(bytes, Map.class));
130+ * }</pre>
131+ *
132+ * @throws NullPointerException if {@code in} or {@code parser} is {@code null}
133+ * @throws UncheckedIOException if the stream cannot be read
134+ */
135+ public static Spec fromJson (InputStream in , Function <byte [], Map <String , Object >> parser ) {
136+ Objects .requireNonNull (parser , "parser" );
137+ return from (parser .apply (readAll (in )));
138+ }
139+
140+ /**
141+ * Reads a YAML OpenAPI specification from {@code in} using SnakeYAML. SnakeYAML must be on the
142+ * classpath; otherwise throws {@link IllegalStateException}. The stream is fully consumed and
143+ * closed before this method returns.
144+ *
145+ * <p>To avoid the SnakeYAML dependency, use {@link #fromYaml(InputStream, Function)} instead.
146+ *
147+ * @throws NullPointerException if {@code in} is {@code null}
148+ * @throws UncheckedIOException if the stream cannot be read
149+ * @throws IllegalStateException if SnakeYAML is not on the classpath
150+ */
151+ public static Spec fromYaml (InputStream in ) {
152+ return fromYaml (in , Spec ::parseYamlWithSnakeYaml );
153+ }
154+
155+ /**
156+ * Reads a YAML OpenAPI specification from {@code in} using the supplied {@code parser}. The
157+ * stream is fully consumed and closed before this method returns.
158+ *
159+ * @throws NullPointerException if {@code in} or {@code parser} is {@code null}
160+ * @throws UncheckedIOException if the stream cannot be read
161+ */
162+ public static Spec fromYaml (InputStream in , Function <byte [], Map <String , Object >> parser ) {
163+ Objects .requireNonNull (parser , "parser" );
164+ return from (parser .apply (readAll (in )));
165+ }
166+
167+ private static byte [] readAll (InputStream in ) {
168+ Objects .requireNonNull (in , "in" );
169+ try (in ) {
170+ return in .readAllBytes ();
171+ } catch (IOException e ) {
172+ throw new UncheckedIOException ("Failed to read OpenAPI spec from stream" , e );
173+ }
174+ }
175+
176+ private static Map <String , Object > parseJsonWithGson (byte [] bytes ) {
177+ String text = new String (bytes , StandardCharsets .UTF_8 );
178+ Class <?> gsonClass = loadOptional (GSON_CLASS , "Json" , "Gson" );
94179 try {
95180 Object gson = gsonClass .getDeclaredConstructor ().newInstance ();
96181 Method fromJson = gsonClass .getMethod ("fromJson" , String .class , Class .class );
@@ -102,8 +187,9 @@ private static Map<String, Object> parseJson(String text) {
102187 }
103188 }
104189
105- private static Map <String , Object > parseYaml (String text ) {
106- Class <?> yamlClass = loadOptional (SNAKEYAML_CLASS , "YAML" , "SnakeYAML" );
190+ private static Map <String , Object > parseYamlWithSnakeYaml (byte [] bytes ) {
191+ String text = new String (bytes , StandardCharsets .UTF_8 );
192+ Class <?> yamlClass = loadOptional (SNAKEYAML_CLASS , "Yaml" , "SnakeYAML" );
107193 try {
108194 Object yaml = yamlClass .getDeclaredConstructor ().newInstance ();
109195 Method load = yamlClass .getMethod ("load" , String .class );
@@ -120,14 +206,15 @@ private static Class<?> loadOptional(String className, String format, String lib
120206 return Class .forName (className , false , Spec .class .getClassLoader ());
121207 } catch (ClassNotFoundException e ) {
122208 throw new IllegalStateException (
123- "Spec.fromPath requires "
124- + libName
125- + " on the classpath for "
209+ "Loading "
126210 + format
127- + " specs. Add a "
211+ + " OpenAPI specs requires "
128212 + libName
129- + " dependency, or parse the file yourself and call"
130- + " Spec.from(Map<String, Object>) instead." ,
213+ + " on the classpath. Add a "
214+ + libName
215+ + " dependency, or supply your own parser via Spec.from"
216+ + format
217+ + "(InputStream, Function) / Spec.from(Map<String, Object>) instead." ,
131218 e );
132219 }
133220 }
0 commit comments