1mod docs;
9mod errors;
10#[cfg(test)]
11mod model_compat_tests;
12mod models;
13mod multipart;
14mod naming;
15mod native_names;
16mod operation_samples;
17mod operation_tests;
18mod operations;
19mod pagination;
20mod presence;
21mod resources;
22mod runtime;
23mod surface;
24use docs::*;
25use errors::*;
26#[cfg(test)]
27use models::render_model;
28use models::render_model_parts;
29use naming::*;
30pub use operation_tests::{OperationTests, operation_tests};
31#[cfg(test)]
32use operations::render_operation;
33use operations::{render_client_facade, render_operation_chunk};
34pub use package::NativeSdk;
35use pagination::{java_pagination, render_pagination_operation};
36use resources::{
37 render_resource_chunk, render_resource_facade, resource_accessor_name, resource_operations,
38};
39use runtime::*;
40use surface::*;
41#[cfg(test)]
42mod call_options_tests;
43mod oauth;
44pub use oauth::{OAuth, oauth};
45mod webhooks;
46use std::collections::{BTreeMap, BTreeSet};
47use std::fmt::Write;
48pub use webhooks::{Webhooks, webhooks};
49
50use anyhow::{Result, bail};
51use poolster_core::{
52 AdditionalProperties, Api, Field, GeneratedFile, GeneratedTree, Operation, OperationParameter,
53 OperationResponse, Schema, SchemaKind, SchemaValue, SdkClientStyle,
54};
55use serde_json::Value;
56
57const NOTICE: &str = "// Generated by Poolster. Do not edit.\n";
58const OPERATIONS_PER_FILE: usize = 64;
61
62#[cfg(test)]
69fn render_test_sdk(
70 api: &Api,
71 output_dir: &str,
72 package_name: Option<&str>,
73) -> Result<GeneratedTree> {
74 render_sdk(api, output_dir, package_name, SdkClientStyle::Flat)
75}
76
77#[cfg(test)]
81fn render_sdk(
82 api: &Api,
83 output_dir: &str,
84 package_name: Option<&str>,
85 style: SdkClientStyle,
86) -> Result<GeneratedTree> {
87 render_sdk_with_policy(api, output_dir, package_name, style, false)
88}
89
90fn render_sdk_with_policy(
91 api: &Api,
92 output_dir: &str,
93 package_name: Option<&str>,
94 style: SdkClientStyle,
95 open_enums: bool,
96) -> Result<GeneratedTree> {
97 let prepared = prepare_api(api);
98 let api = &prepared;
99 multipart::validate(api)?;
100
101 for operation in &api.operations {
102 let extension = poolster_core::poolster_extension(&operation.annotations, "pagination")
103 .or_else(|| operation.annotations.get("x-speakeasy-pagination"));
104 if extension
105 .and_then(|extension| extension.get("type"))
106 .and_then(Value::as_str)
107 == Some("page")
108 {
109 poolster_core::pagination::normalize_pagination(api, operation, None)?;
110 }
111 }
112 let root = normalized_output_dir(output_dir)?;
113 let package = package_name
114 .filter(|name| !name.trim().is_empty())
115 .map(java_package_name)
116 .unwrap_or_else(|| format!("io.poolster.{}", package_segment(&api.name)));
117 if package.is_empty() {
118 bail!("a Java package name could not be derived from the API name");
119 }
120 let package_path = package.replace('.', "/");
121 let artifact = format!("{}-sdk", package_segment(&api.name));
122 let version = package_version(&api.version);
123 let mut tree = GeneratedTree::default();
124
125 insert(
126 &mut tree,
127 &root,
128 "settings.gradle",
129 settings_gradle(&artifact),
130 )?;
131 insert(
132 &mut tree,
133 &root,
134 "build.gradle",
135 build_gradle(&package, &artifact, &version),
136 )?;
137 insert(
138 &mut tree,
139 &root,
140 "pom.xml",
141 pom_xml(&package, &artifact, &version),
142 )?;
143 insert(
144 &mut tree,
145 &root,
146 "README.md",
147 readme(api, &package, &artifact, style),
148 )?;
149 insert(
150 &mut tree,
151 &root,
152 "STYLE_GUIDE.md",
153 style_guide(api, &package, style),
154 )?;
155 insert(
156 &mut tree,
157 &root,
158 &format!("src/main/java/{package_path}/ApiException.java"),
159 api_exception(&package),
160 )?;
161 insert(
162 &mut tree,
163 &root,
164 &format!("src/main/java/{package_path}/ClientConfig.java"),
165 client_config(&package),
166 )?;
167 insert(
168 &mut tree,
169 &root,
170 &format!("src/main/java/{package_path}/RetryConfig.java"),
171 retry_config(&package),
172 )?;
173 insert(
174 &mut tree,
175 &root,
176 &format!("src/main/java/{package_path}/ClientHooks.java"),
177 client_hooks(&package),
178 )?;
179 insert(
180 &mut tree,
181 &root,
182 &format!("src/main/java/{package_path}/ClientCallOptions.java"),
183 include_str!("../templates/call_options.java.tmpl").replace("__PACKAGE__", &package),
184 )?;
185 multipart::emit(api, &root, &package, &mut tree)?;
186 for (index, schema) in api.schemas.iter().enumerate() {
187 for (filename, source) in render_model_parts(schema, &package, open_enums, index)? {
188 insert(
189 &mut tree,
190 &root,
191 &format!("src/main/java/{package_path}/model/{filename}"),
192 source,
193 )?;
194 }
195 }
196 insert(
197 &mut tree,
198 &root,
199 &format!("src/main/java/{package_path}/ClientBase.java"),
200 render_client_base(api, &package),
201 )?;
202 let operation_overhead = render_operation_chunk(api, &[], &package, 0).len() + 64;
203 let units = api
204 .operations
205 .iter()
206 .map(|operation| poolster_core::source_layout::SourceUnit {
207 bytes: render_operation_chunk(api, std::slice::from_ref(operation), &package, 0)
208 .len()
209 .saturating_sub(operation_overhead - 64),
210 resource: None,
211 })
212 .collect::<Vec<_>>();
213 let groups = poolster_core::source_layout::SourceLayout::Chunked {
214 max_file_bytes: 128 * 1024,
215 max_declarations: Some(OPERATIONS_PER_FILE),
216 }
217 .groups(&units, operation_overhead)?;
218 let operation_chunks = groups
219 .iter()
220 .map(|indices| {
221 indices
222 .iter()
223 .map(|index| api.operations[*index].clone())
224 .collect::<Vec<_>>()
225 })
226 .collect::<Vec<_>>();
227 for (index, operations) in operation_chunks.iter().enumerate() {
228 insert(
229 &mut tree,
230 &root,
231 &format!("src/main/java/{package_path}/internal/Operations{index:03}.java"),
232 render_operation_chunk(api, operations, &package, index),
233 )?;
234 }
235 insert(
236 &mut tree,
237 &root,
238 &format!("src/main/java/{package_path}/Client.java"),
239 render_client_facade(api, &package, style, operation_chunks.len()),
240 )?;
241 if style == SdkClientStyle::Namespaced {
242 for (resource, operations) in resource_operations(api) {
243 let overhead = render_resource_chunk(&resource, &[], &package, 0).len() + 64;
244 let units = operations
245 .iter()
246 .map(|operation| poolster_core::source_layout::SourceUnit {
247 bytes: render_resource_chunk(
248 &resource,
249 std::slice::from_ref(operation),
250 &package,
251 0,
252 )
253 .len()
254 .saturating_sub(overhead - 64),
255 resource: None,
256 })
257 .collect::<Vec<_>>();
258 let groups = poolster_core::source_layout::SourceLayout::Chunked {
259 max_file_bytes: 128 * 1024,
260 max_declarations: Some(100),
261 }
262 .groups(&units, overhead)?;
263 for (index, indices) in groups.iter().enumerate() {
264 let operations = indices
265 .iter()
266 .map(|index| operations[*index].clone())
267 .collect::<Vec<_>>();
268 insert(
269 &mut tree,
270 &root,
271 &format!(
272 "src/main/java/{package_path}/internal/resources/{resource}ResourcePart{index:03}.java"
273 ),
274 render_resource_chunk(&resource, &operations, &package, index),
275 )?;
276 }
277 insert(
278 &mut tree,
279 &root,
280 &format!("src/main/java/{package_path}/{resource}Resource.java"),
281 render_resource_facade(&resource, &package, groups.len()),
282 )?;
283 }
284 }
285 if api.schemas.is_empty() {
286 let imports = tree
287 .iter()
288 .filter(|(path, _)| path.extension().is_some_and(|ext| ext == "java"))
289 .map(|(path, source)| {
290 (
291 path.to_path_buf(),
292 source.replace(&format!("import {package}.model.*;\n"), ""),
293 )
294 })
295 .collect::<Vec<_>>();
296 for (path, source) in imports {
297 tree.replace(GeneratedFile::new(path, source)?)?;
298 }
299 }
300 let oversized = tree.iter().filter_map(|(path, source)| {
301 let native = path.extension().and_then(|value| value.to_str()) == Some("java");
302 (native && source.len() > 128 * 1024).then(|| serde_json::json!({"path":path,"bytes":source.len(),"max_file_bytes":128*1024,"reason":"Atomic native declaration or public facade exceeds the grouping budget; source was retained intact."}))
303 }).collect::<Vec<_>>();
304 if !oversized.is_empty() {
305 insert(
306 &mut tree,
307 &root,
308 ".poolster/source-layout-diagnostics.json",
309 serde_json::to_string_pretty(&oversized)?,
310 )?;
311 }
312 Ok(tree)
313}
314
315fn normalized_output_dir(output_dir: &str) -> Result<String> {
316 let root = output_dir.trim_matches('/');
317 if root.split('/').any(|part| part == "..") {
318 bail!("Java SDK output directory cannot contain parent-directory components");
319 }
320 Ok(root.to_owned())
321}
322
323fn insert(tree: &mut GeneratedTree, root: &str, path: &str, contents: String) -> Result<()> {
324 let path = match root {
325 "" | "." => path.to_owned(),
326 _ => format!("{root}/{path}"),
327 };
328 tree.insert(GeneratedFile::new(path, contents)?)
329}
330
331#[cfg(test)]
332mod tests;
333
334mod package;
335pub use package::{Java, PackageExt, Sdk, Settings, package, sdk};
336
337mod bundled_middleware;
338
339#[cfg(test)]
340mod source_layout_tests;