Repository: 1Password/typeshare (Rust)

Issue:
Typeshare ignores type annotations in embedded modules
Hi,

I have code like this:

```rust
mod my_module {
    // Types with Typeshare annotations
}
```

...which results in nothing being generated.

If I remove the module around the annotations, everything works.


Discussion:
This is probably an issue with our AST visitor. I will look into this.
I ran into this today and wish to add some context.
In my case, I'm making an app for tracking scores for the board game 7 Wonders. This game comes in two editions. Now I'm not gonna claim this is the best way to handle this but I have a top level enum for the ScoreCard:

```rust
#[derive(Deserialize)]
#[serde(tag = "type", content = "content", rename_all = "camelCase")]
#[typeshare]
pub enum ScoreCard {
    Edition1(edition1::ScoreCard),
    Edition2(edition2::ScoreCard),
}
```

And the data within the scorecards in each edition is slightly different. As you can see, I've put the edition specific types in a module each. I would really like to annotate somehow that these should be generated into namespaces or similar in typescript because I can't have two definitions for Player without them being in different scopes. To solve this for now I will need to add suffixes for these and put it all in the same module.
So there's two different issues here... one is that type annotations are silently ignored if they're in an inline module, while the other is that Typeshare doesn't support namespacing.

@Zonnex I'm going to break your problem into a new issue since that's a more complicated problem that intersects with similar design choices we're trying to decide on.



Patch:
diff --git a/core/src/parser.rs b/core/src/parser.rs
index d3e6bf6a..971f0d21 100644
--- a/core/src/parser.rs
+++ b/core/src/parser.rs
@@ -88,7 +88,8 @@ pub fn parse(input: &str) -> Result<ParsedData, ParseError> {
     // Parse and process the input, ensuring we parse only items marked with
     // `#[typeshare]
     let source = syn::parse_file(input)?;
-    for item in &source.items {
+
+    for item in flatten_items(source.items.iter()) {
         match item {
             syn::Item::Struct(s) if has_typeshare_annotation(&s.attrs) => {
                 parsed_data.push_rust_thing(parse_struct(s)?);
@@ -106,6 +107,23 @@ pub fn parse(input: &str) -> Result<ParsedData, ParseError> {
     Ok(parsed_data)
 }
 
+/// Given an iterator over items, will return an iterator that flattens the contents of embedded
+/// module items into the iterator.
+fn flatten_items<'a>(
+    items: impl Iterator<Item = &'a syn::Item>,
+) -> impl Iterator<Item = &'a syn::Item> {
+    items.flat_map(|item| {
+        match item {
+            syn::Item::Mod(syn::ItemMod {
+                content: Some((_, items)),
+                ..
+            }) => flatten_items(items.iter()).collect(),
+            item => vec![item],
+        }
+        .into_iter()
+    })
+}
+
 /// Parses a struct into a definition that more succinctly represents what
 /// typeshare needs to generate code for other languages.
 ///


Test patch:
diff --git a/core/data/tests/can_recognize_types_inside_modules/input.rs b/core/data/tests/can_recognize_types_inside_modules/input.rs
new file mode 100644
index 00000000..222d5eea
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/input.rs
@@ -0,0 +1,23 @@
+mod a {
+    #[typeshare]
+    pub struct A {
+        field: u32
+    }
+    mod b {
+        mod c {
+            #[typeshare]
+            pub struct ABC {
+                field: u32
+            }
+        }
+        #[typeshare]
+        pub struct AB {
+            field: u32
+        }
+    }
+}
+
+#[typeshare]
+pub struct OutsideOfModules {
+    field: u32
+}
\ No newline at end of file
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.go b/core/data/tests/can_recognize_types_inside_modules/output.go
new file mode 100644
index 00000000..f08ec778
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.go
@@ -0,0 +1,16 @@
+package proto
+
+import "encoding/json"
+
+type A struct {
+	Field uint32 `json:"field"`
+}
+type ABC struct {
+	Field uint32 `json:"field"`
+}
+type AB struct {
+	Field uint32 `json:"field"`
+}
+type OutsideOfModules struct {
+	Field uint32 `json:"field"`
+}
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.kt b/core/data/tests/can_recognize_types_inside_modules/output.kt
new file mode 100644
index 00000000..2de6a52b
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.kt
@@ -0,0 +1,27 @@
+@file:NoLiveLiterals
+
+package com.agilebits.onepassword
+
+import androidx.compose.runtime.NoLiveLiterals
+import kotlinx.serialization.*
+
+@Serializable
+data class A (
+	val field: UInt
+)
+
+@Serializable
+data class ABC (
+	val field: UInt
+)
+
+@Serializable
+data class AB (
+	val field: UInt
+)
+
+@Serializable
+data class OutsideOfModules (
+	val field: UInt
+)
+
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.scala b/core/data/tests/can_recognize_types_inside_modules/output.scala
new file mode 100644
index 00000000..5a2e40b0
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.scala
@@ -0,0 +1,29 @@
+package com.agilebits
+
+package object onepassword {
+
+type UByte = Byte
+type UShort = Short
+type UInt = Int
+type ULong = Int
+
+}
+package onepassword {
+
+case class A (
+	field: UInt
+)
+
+case class ABC (
+	field: UInt
+)
+
+case class AB (
+	field: UInt
+)
+
+case class OutsideOfModules (
+	field: UInt
+)
+
+}
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.swift b/core/data/tests/can_recognize_types_inside_modules/output.swift
new file mode 100644
index 00000000..ffaebb38
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.swift
@@ -0,0 +1,33 @@
+import Foundation
+
+public struct A: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
+
+public struct ABC: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
+
+public struct AB: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
+
+public struct OutsideOfModules: Codable {
+	public let field: UInt32
+
+	public init(field: UInt32) {
+		self.field = field
+	}
+}
diff --git a/core/data/tests/can_recognize_types_inside_modules/output.ts b/core/data/tests/can_recognize_types_inside_modules/output.ts
new file mode 100644
index 00000000..00c101e9
--- /dev/null
+++ b/core/data/tests/can_recognize_types_inside_modules/output.ts
@@ -0,0 +1,16 @@
+export interface A {
+	field: number;
+}
+
+export interface ABC {
+	field: number;
+}
+
+export interface AB {
+	field: number;
+}
+
+export interface OutsideOfModules {
+	field: number;
+}
+
diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs
index 117868c2..9a1826a9 100644
--- a/core/tests/snapshot_tests.rs
+++ b/core/tests/snapshot_tests.rs
@@ -406,6 +406,9 @@ tests! {
     can_generate_double_option_pattern: [
         typescript
     ];
+    can_recognize_types_inside_modules: [
+        swift, kotlin, scala, typescript, go
+    ];
     test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go ];
     test_algebraic_enum_case_name_support: [
         swift {