Skip to content

Commit f84174b

Browse files
committed
feat: add sdk check command and support union types
Implement `am sdk check` to verify Amplitude SDK configuration. Update build.rs to generate proper serde-tagged enums for FlatBuffers union types instead of skipping them. Adapt collection builder and tests for newly generated fields (sounds, scheduler config).
1 parent fb202f2 commit f84174b

File tree

13 files changed

+164
-39
lines changed

13 files changed

+164
-39
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.rs

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct EnumDef {
5353
struct EnumVariant {
5454
name: String,
5555
value: i64,
56+
rust_type: Option<String>,
5657
}
5758

5859
#[derive(Debug, Clone)]
@@ -196,6 +197,9 @@ fn process_schema(
196197
let schema = reflection::root_as_schema(schema_bytes)
197198
.map_err(|e| format!("Failed to parse schema: {}", e))?;
198199

200+
// Collect objects (tables and structs) first so we can resolve union variants
201+
let schema_objects = schema.objects();
202+
199203
// Collect enums
200204
let schema_enums = schema.enums();
201205
for i in 0..schema_enums.len() {
@@ -213,9 +217,29 @@ fn process_schema(
213217
let vals = e.values();
214218
for j in 0..vals.len() {
215219
let v = vals.get(j);
220+
let rust_type = if is_union {
221+
if let Some(union_type) = v.union_type() {
222+
let base = union_type.base_type();
223+
let idx = union_type.index();
224+
if base == BaseType::Obj {
225+
let obj_fqn = schema_objects.get(idx as usize).name().to_string();
226+
Some(leaf_name(&obj_fqn).to_string())
227+
} else if base == BaseType::String {
228+
Some("String".to_string())
229+
} else {
230+
None
231+
}
232+
} else {
233+
None
234+
}
235+
} else {
236+
None
237+
};
238+
216239
variants.push(EnumVariant {
217240
name: v.name().to_string(),
218241
value: v.value(),
242+
rust_type,
219243
});
220244
}
221245

@@ -229,8 +253,7 @@ fn process_schema(
229253
);
230254
}
231255

232-
// Collect objects (tables and structs)
233-
let schema_objects = schema.objects();
256+
// Process fields for objects
234257
for i in 0..schema_objects.len() {
235258
let obj = schema_objects.get(i);
236259
let fqn = obj.name().to_string();
@@ -253,17 +276,17 @@ fn process_schema(
253276
continue;
254277
}
255278

256-
// Skip union fields — unions require special handling (TODO: Story 2c.2)
257-
if base == BaseType::Union {
258-
continue;
259-
}
279+
// Union fields are now processed
280+
// if base == BaseType::Union {
281+
// continue;
282+
// }
260283

261-
// Skip union vector fields (vector of unions or their discriminators)
262-
if base == BaseType::Vector
263-
&& (ty.element() == BaseType::Union || ty.element() == BaseType::UType)
264-
{
265-
continue;
266-
}
284+
// Union vector fields are now processed
285+
// if base == BaseType::Vector
286+
// && (ty.element() == BaseType::Union || ty.element() == BaseType::UType)
287+
// {
288+
// continue;
289+
// }
267290

268291
// Determine the Rust type for this field
269292
let Some((rust_type, is_optional, default_value, serde_rename)) =
@@ -357,6 +380,14 @@ fn resolve_field_type(
357380
}
358381

359382
match base {
383+
BaseType::Union => {
384+
let enum_fqn = schema_enums.get(idx as usize).name().to_string();
385+
let enum_name = all_enum_names
386+
.get(&enum_fqn)
387+
.cloned()
388+
.unwrap_or_else(|| leaf_name(&enum_fqn).to_string());
389+
Some((enum_name, is_opt, None, None))
390+
}
360391
BaseType::Obj => {
361392
let obj_fqn = objects.get(idx as usize).name().to_string();
362393
let obj_name = leaf_name(&obj_fqn).to_string();
@@ -365,6 +396,10 @@ fn resolve_field_type(
365396
}
366397
BaseType::Vector => {
367398
let inner = match ty.element() {
399+
BaseType::Union => {
400+
let enum_fqn = schema_enums.get(idx as usize).name().to_string();
401+
leaf_name(&enum_fqn).to_string()
402+
}
368403
BaseType::Obj => {
369404
let obj_fqn = objects.get(idx as usize).name().to_string();
370405
leaf_name(&obj_fqn).to_string()
@@ -453,10 +488,21 @@ fn generate_code(
453488
if def.is_union {
454489
writeln!(
455490
out,
456-
"// TODO: Union type {} skipped — requires special serde handling (Story 2c.2)",
457-
def.name
491+
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
458492
)
459493
.unwrap();
494+
writeln!(out, "#[serde(untagged)]").unwrap();
495+
writeln!(out, "pub enum {} {{", def.name).unwrap();
496+
for variant in &def.variants {
497+
let rust_name = &variant.name;
498+
if rust_name == "NONE" {
499+
writeln!(out, " None,").unwrap();
500+
} else {
501+
let ty = variant.rust_type.as_deref().unwrap_or(rust_name);
502+
writeln!(out, " {}({}),", rust_name, ty).unwrap();
503+
}
504+
}
505+
writeln!(out, "}}").unwrap();
460506
writeln!(out).unwrap();
461507
continue;
462508
}
@@ -738,7 +784,7 @@ See the README for detailed setup instructions.
738784
let union_count = enums.values().filter(|e| e.is_union).count();
739785
let struct_count = structs.len();
740786
println!(
741-
"cargo:warning=Generated asset types from {} schema files: {} enums, {} structs ({} unions skipped)",
787+
"cargo:warning=Generated asset types from {} schema files: {} enums, {} structs ({} unions processed)",
742788
bfbs_files.len(),
743789
enum_count,
744790
struct_count,

src/app.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ pub enum Commands {
8282
command: SudoCommands,
8383
},
8484

85+
/// SDK-related tasks
86+
Sdk {
87+
#[command(subcommand)]
88+
command: crate::commands::sdk::SdkCommands,
89+
},
90+
8591
/// Manage project templates
8692
Template {
8793
#[command(subcommand)]

src/assets/collection.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ impl CollectionBuilder {
8888
scope: Scope::World,
8989
play_mode: CollectionPlayMode::PlayOne,
9090
scheduler: Some(SoundSchedulerSettings::default()),
91+
sounds: Some(Vec::new()),
9192
},
9293
}
9394
}
@@ -166,7 +167,7 @@ impl CollectionBuilder {
166167

167168
/// Sets the scheduler mode.
168169
pub fn scheduler_mode(mut self, mode: SoundSchedulerMode) -> Self {
169-
self.collection.scheduler = Some(SoundSchedulerSettings { mode });
170+
self.collection.scheduler = Some(SoundSchedulerSettings { mode, config: None });
170171
self
171172
}
172173

@@ -293,7 +294,8 @@ mod tests {
293294
assert_eq!(
294295
c.scheduler,
295296
Some(SoundSchedulerSettings {
296-
mode: SoundSchedulerMode::Random
297+
mode: SoundSchedulerMode::Random,
298+
config: None,
297299
})
298300
);
299301
}
@@ -329,7 +331,8 @@ mod tests {
329331
assert_eq!(
330332
c.scheduler,
331333
Some(SoundSchedulerSettings {
332-
mode: SoundSchedulerMode::Sequence
334+
mode: SoundSchedulerMode::Sequence,
335+
config: None,
333336
})
334337
);
335338
}
@@ -430,7 +433,8 @@ mod tests {
430433
assert_eq!(
431434
c.scheduler,
432435
Some(SoundSchedulerSettings {
433-
mode: SoundSchedulerMode::Sequence
436+
mode: SoundSchedulerMode::Sequence,
437+
config: None,
434438
})
435439
);
436440
assert_eq!(c.spatialization, Spatialization::Position);

src/assets/extensions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,15 @@ impl SoundSchedulerSettings {
255255
pub fn random() -> Self {
256256
Self {
257257
mode: SoundSchedulerMode::Random,
258+
config: None,
258259
}
259260
}
260261

261262
/// Creates a sequential scheduler configuration.
262263
pub fn sequential() -> Self {
263264
Self {
264265
mode: SoundSchedulerMode::Sequence,
266+
config: None,
265267
}
266268
}
267269
}

src/commands/asset/collection.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,10 @@ fn apply_flag_updates(
719719
"Invalid scheduler mode value",
720720
)
721721
})?;
722-
collection.scheduler = Some(crate::assets::SoundSchedulerSettings { mode });
722+
collection.scheduler = Some(crate::assets::SoundSchedulerSettings {
723+
mode,
724+
config: None,
725+
});
723726
updated_fields.push("scheduler_mode".to_string());
724727
}
725728

@@ -767,7 +770,10 @@ fn prompt_collection_updates(
767770
.map(|s| s.mode)
768771
.unwrap_or(SoundSchedulerMode::Random);
769772
if let Some(new_sm) = prompt_update_scheduler_mode(input, &current_scheduler)? {
770-
collection.scheduler = Some(crate::assets::SoundSchedulerSettings { mode: new_sm });
773+
collection.scheduler = Some(crate::assets::SoundSchedulerSettings {
774+
mode: new_sm,
775+
config: None,
776+
});
771777
updated_fields.push("scheduler_mode".to_string());
772778
}
773779

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414

1515
pub mod asset;
1616
pub mod project;
17+
pub mod sdk;
1718
pub mod sudo;
1819
pub mod template;

src/commands/sdk.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2026-present Sparky Studios. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use crate::config::sdk::discover_sdk;
16+
use crate::database::Database;
17+
use crate::input::Input;
18+
use crate::presentation::Output;
19+
use clap::Subcommand;
20+
use serde_json::json;
21+
use std::sync::Arc;
22+
23+
#[derive(Subcommand, Debug)]
24+
pub enum SdkCommands {
25+
/// Check if the Amplitude SDK is properly configured
26+
Check,
27+
}
28+
29+
pub async fn handler(
30+
command: &SdkCommands,
31+
_database: Option<Arc<Database>>,
32+
_input: &dyn Input,
33+
output: &dyn Output,
34+
) -> anyhow::Result<()> {
35+
match command {
36+
SdkCommands::Check => {
37+
let location = discover_sdk()?;
38+
39+
output.success(
40+
json!({
41+
"message": "SDK is properly configured",
42+
"path": location.root().to_string_lossy(),
43+
"schemas_dir": location.schemas_dir().to_string_lossy(),
44+
}),
45+
None,
46+
);
47+
Ok(())
48+
}
49+
}
50+
}

src/common/errors.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -376,15 +376,6 @@ pub fn asset_already_exists(asset_type: &str, name: &str) -> CliError {
376376
)
377377
}
378378

379-
/// Create an error for the SDK not being found.
380-
pub fn sdk_not_found() -> CliError {
381-
CliError::new(
382-
codes::ERR_SDK_NOT_FOUND,
383-
"Amplitude SDK not found",
384-
"The AM_SDK_PATH environment variable is not set or points to an invalid location",
385-
)
386-
}
387-
388379
// =============================================================================
389380
// Macro for quick error construction
390381
// =============================================================================

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub mod schema;
2929
pub mod commands {
3030
pub mod asset;
3131
pub mod project;
32+
pub mod sdk;
3233
pub mod sudo;
3334
pub mod template;
3435
}

0 commit comments

Comments
 (0)