googleads-rs 23.2.0

A gRPC client library for Google Ads API, generated automatically from the API definition files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Google Ads gRPC library.
//!
//! A gRPC client library for Google Ads API, generated automatically from the API definition files.
//!
//! Provides `GoogleAdsRow.get(path: &str)` accessor method to easily retrieve fields selected in GAQL.
//!
//! # Example
//!
//! ```ignore
//! let field_mask = response.field_mask.unwrap();
//! for row in response.results {
//!     for path in &field_mask.paths {
//!         print!("{}: {}\t", path, row.get(&path));
//!     }
//!     print!("\n");
//! }
//! ```

#![doc(html_root_url = "https://docs.rs/googleads-rs/23.2.0")]

#[allow(clippy::all)]
#[allow(clippy::doc_lazy_continuation)]
#[allow(unused_must_use)]
mod protos {
    include!(concat!(env!("OUT_DIR"), "/protos.rs"));
}
pub use protos::*;

use once_cell::sync::Lazy;
use prost::Message;
use prost_reflect::{DescriptorPool, DynamicMessage, ReflectMessage, Value};
use std::io::Cursor;

static DESCRIPTOR_POOL: Lazy<DescriptorPool> = Lazy::new(|| {
    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin"));
    DescriptorPool::decode(bytes.as_ref()).expect("Failed to decode file descriptor set")
});

const GOOGLE_ADS_ROW_FQN: &str = "google.ads.googleads.v23.services.GoogleAdsRow";

impl google::ads::googleads::v23::services::GoogleAdsRow {
    /// Returns a field value from the GoogleAdsRow by its GAQL field path.
    ///
    /// This method uses `prost-reflect` to dynamically access any field in the row
    /// using the same dot-separated paths used in GAQL queries (e.g., `"campaign.id"`,
    /// `"ad_group.name"`). This eliminates the need to hardcode field accessors for
    /// each field in the API.
    ///
    /// # Arguments
    /// * `field_name` - A dot-separated field path (e.g., `"campaign.id"`, `"segments.device"`)
    ///
    /// # Returns
    /// The field value formatted as a string. Returns an empty string if the field
    /// is not set or doesn't exist in the response.
    ///
    /// # Performance
    /// This method encodes the row to protobuf bytes and decodes it as a `DynamicMessage`
    /// on each call. For retrieving multiple fields from the same row, prefer [`Self::get_many`].
    ///
    /// # Example
    ///
    /// ```ignore
    /// // After executing a GAQL query like:
    /// // SELECT campaign.id, campaign.name, campaign.status FROM campaign
    ///
    /// let field_mask = response.field_mask.unwrap();
    /// for row in response.results {
    ///     for path in &field_mask.paths {
    ///         // path might be "campaign.id", "campaign.name", etc.
    ///         print!("{}: {}\t", path, row.get(path));
    ///     }
    ///     print!("\n");
    /// }
    /// ```
    pub fn get(&self, field_name: &str) -> String {
        // Encode the GoogleAdsRow to bytes, then decode as DynamicMessage
        let encoded = self.encode_to_vec();

        let descriptor = DESCRIPTOR_POOL
            .get_message_by_name(GOOGLE_ADS_ROW_FQN)
            .expect("GoogleAdsRow descriptor not found");

        let dynamic_msg = DynamicMessage::decode(descriptor, Cursor::new(&encoded))
            .expect("Failed to decode GoogleAdsRow as DynamicMessage");

        self.get_field_from_dynamic(&dynamic_msg, field_name)
    }

    /// Returns multiple field values from the GoogleAdsRow efficiently.
    ///
    /// Like [`Self::get`], this uses `prost-reflect` for dynamic field access, but encodes
    /// the row only once and retrieves all requested fields. This is significantly more
    /// efficient than calling `get()` multiple times when you need several fields.
    ///
    /// # Arguments
    /// * `field_names` - A slice of dot-separated field paths (e.g., `["campaign.id", "campaign.name"]`)
    ///
    /// # Returns
    /// A `Vec<String>` containing the field values in the same order as `field_names`.
    /// Returns empty strings for fields that are not set or don't exist in the response.
    ///
    /// # Performance
    /// Encodes the row to protobuf bytes once and walks all paths in a single pass.
    /// Preferred over multiple `get()` calls when retrieving 2+ fields.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // After executing a GAQL query with multiple selected fields
    /// let fields = vec!["campaign.id", "campaign.name", "campaign.status", "segments.device"];
    ///
    /// for row in response.results {
    ///     let values = row.get_many(&fields);
    ///     // values[0] = campaign.id, values[1] = campaign.name, etc.
    ///     println!("Campaign {} (ID: {}) is {:?} on {:?}",
    ///         &values[1], &values[0], &values[2], &values[3]);
    /// }
    /// ```
    pub fn get_many(&self, field_names: &[&str]) -> Vec<String> {
        // Encode the GoogleAdsRow to bytes, then decode as DynamicMessage
        let encoded = self.encode_to_vec();

        let descriptor = DESCRIPTOR_POOL
            .get_message_by_name(GOOGLE_ADS_ROW_FQN)
            .expect("GoogleAdsRow descriptor not found");

        let dynamic_msg = DynamicMessage::decode(descriptor, Cursor::new(&encoded))
            .expect("Failed to decode GoogleAdsRow as DynamicMessage");

        field_names
            .iter()
            .map(|field_name| self.get_field_from_dynamic(&dynamic_msg, field_name))
            .collect()
    }

    /// Internal method to get a field value from a DynamicMessage
    fn get_field_from_dynamic(&self, dyn_msg: &DynamicMessage, field_name: &str) -> String {
        match field_name {
            // Special case for campaign.asset_automation_settings
            field if field.starts_with("campaign.asset_automation_settings") => {
                self.format_asset_automation_settings(dyn_msg)
            }
            // Special case for ad_group_ad.ad.responsive_search_ad.headlines/descriptions
            field
                if field.starts_with("ad_group_ad.ad.responsive_search_ad.headlines")
                    || field.starts_with("ad_group_ad.ad.responsive_search_ad.descriptions") =>
            {
                // The GAQL path stops at the repeated message, but users expect .text extracted
                self.format_value_at_path(dyn_msg, &format!("{}.text", field_name))
            }
            // General case: use reflection to walk the path
            _ => self.format_value_at_path(dyn_msg, field_name),
        }
    }

    /// Format asset_automation_settings as "TYPE:STATUS" pairs
    fn format_asset_automation_settings(&self, dyn_msg: &DynamicMessage) -> String {
        // Navigate to campaign message
        let campaign_field = dyn_msg
            .descriptor()
            .get_field_by_name("campaign")
            .expect("campaign field not found");

        if !dyn_msg.has_field(&campaign_field) {
            return String::new();
        }

        let campaign_value = dyn_msg.get_field(&campaign_field);
        let campaign_msg = match &*campaign_value {
            Value::Message(msg) => msg,
            _ => return String::new(),
        };

        // Get asset_automation_settings repeated field
        let settings_field = campaign_msg
            .descriptor()
            .get_field_by_name("asset_automation_settings")
            .expect("asset_automation_settings field not found");

        if !campaign_msg.has_field(&settings_field) {
            return String::new();
        }

        let settings_value = campaign_msg.get_field(&settings_field);
        let settings_list = match &*settings_value {
            Value::List(list) => list,
            _ => return String::new(),
        };

        // Format each item as "TYPE:STATUS"
        settings_list
            .iter()
            .filter_map(|item| match item {
                Value::Message(setting_msg) => {
                    let type_field = match setting_msg
                        .descriptor()
                        .get_field_by_name("asset_automation_type")
                    {
                        Some(f) => f,
                        None => return None,
                    };
                    let type_value = setting_msg.get_field(&type_field);

                    let status_field = match setting_msg
                        .descriptor()
                        .get_field_by_name("asset_automation_status")
                    {
                        Some(f) => f,
                        None => return None,
                    };
                    let status_value = setting_msg.get_field(&status_field);

                    let type_name = self.format_scalar(&type_value, &type_field);
                    let status_name = self.format_scalar(&status_value, &status_field);

                    Some(format!("{}:{}", type_name, status_name))
                }
                _ => None,
            })
            .collect::<Vec<_>>()
            .join(", ")
    }

    /// Format FieldMask as comma-separated list of paths
    fn format_field_mask(&self, field_mask: &DynamicMessage) -> String {
        let paths_field = match field_mask.descriptor().get_field_by_name("paths") {
            Some(f) => f,
            None => return String::new(),
        };

        // Don't check has_field for repeated fields - just get the value
        let paths_value = field_mask.get_field(&paths_field);
        match &*paths_value {
            Value::List(list) => list
                .iter()
                .filter_map(|item| match item {
                    Value::String(s) => Some(s.clone()),
                    _ => None,
                })
                .collect::<Vec<_>>()
                .join(", "),
            _ => String::new(),
        }
    }

    /// Format value at a dotted path
    fn format_value_at_path(&self, msg: &DynamicMessage, path: &str) -> String {
        let path_segments: Vec<&str> = path.split('.').collect();
        self.format_value_recursive(msg, &path_segments, None)
    }

    /// Recursively format value at a path
    fn format_value_recursive(
        &self,
        msg: &DynamicMessage,
        path: &[&str],
        _field_desc: Option<&prost_reflect::FieldDescriptor>,
    ) -> String {
        if path.is_empty() {
            // This shouldn't happen in normal usage
            return format!("{:?}", msg);
        }

        let segment = path[0];

        // Check for empty segment (from trailing dots or double dots)
        if segment.is_empty() {
            return "not implemented by googleads-rs".to_string();
        }

        let remaining = &path[1..];

        // Look up the field by name
        let desc = match msg.descriptor().get_field_by_name(segment) {
            Some(desc) => desc,
            None => return "not implemented by googleads-rs".to_string(),
        };

        // Check if field has presence and is unset
        if desc.supports_presence() && !msg.has_field(&desc) {
            // Before returning empty, validate that remaining path would be valid
            // This ensures invalid paths like "campaign.invalid_field" return "not implemented"
            // even when campaign is not set
            if !remaining.is_empty() {
                // Try to validate the remaining path by checking field existence
                if let prost_reflect::Kind::Message(msg_desc) = desc.kind() {
                    // Validate the next segment exists
                    if msg_desc.get_field_by_name(remaining[0]).is_none() {
                        return "not implemented by googleads-rs".to_string();
                    }
                }
            }
            return String::new();
        }

        let value = msg.get_field(&desc);

        match &*value {
            Value::Message(sub_msg) => {
                if remaining.is_empty() {
                    // Format the message directly
                    if sub_msg.descriptor().full_name() == "google.protobuf.FieldMask" {
                        self.format_field_mask(sub_msg)
                    } else {
                        // Partial paths (e.g., "campaign" without a field) are not supported
                        "not implemented by googleads-rs".to_string()
                    }
                } else {
                    // Continue traversing the path
                    self.format_value_recursive(sub_msg, remaining, None)
                }
            }
            Value::List(list) => {
                if remaining.is_empty() {
                    self.format_list(list, &desc)
                } else {
                    // Walk into each message item
                    list.iter()
                        .map(|item| match item {
                            Value::Message(sub) => {
                                self.format_value_recursive(sub, remaining, None)
                            }
                            _ => String::new(),
                        })
                        .collect::<Vec<_>>()
                        .join(", ")
                }
            }
            _ => {
                if remaining.is_empty() {
                    self.format_scalar(&value, &desc)
                } else {
                    // Can't recurse into scalar types - any remaining path is invalid
                    "not implemented by googleads-rs".to_string()
                }
            }
        }
    }

    /// Format a scalar value
    fn format_scalar(&self, value: &Value, field_desc: &prost_reflect::FieldDescriptor) -> String {
        match value {
            Value::EnumNumber(n) => {
                // Resolve enum number to name
                if let prost_reflect::Kind::Enum(enum_desc) = field_desc.kind() {
                    enum_desc
                        .get_value(*n)
                        .map(|v| v.name().to_string())
                        .unwrap_or_else(|| n.to_string())
                } else {
                    n.to_string()
                }
            }
            Value::String(s) => s.clone(),
            Value::Bool(b) => b.to_string(),
            Value::I32(i) => i.to_string(),
            Value::I64(i) => i.to_string(),
            Value::U32(u) => u.to_string(),
            Value::U64(u) => u.to_string(),
            Value::F32(f) => f.to_string(),
            Value::F64(d) => d.to_string(),
            Value::Bytes(b) => format!("{:?}", b),
            _ => format!("{:?}", value),
        }
    }

    /// Format a list of values
    fn format_list(&self, items: &[Value], field_desc: &prost_reflect::FieldDescriptor) -> String {
        if items.is_empty() {
            return String::new();
        }

        let is_message_list = items.iter().any(|v| matches!(v, Value::Message(_)));

        // For messages, use ; as separator (matches existing behavior)
        let sep = if is_message_list { "; " } else { ", " };

        items
            .iter()
            .map(|item| match item {
                Value::Message(msg) => self.format_message_compact(msg),
                _ => self.format_scalar(item, field_desc),
            })
            .collect::<Vec<_>>()
            .join(sep)
    }

    /// Format a message in a compact "field:value" format
    fn format_message_compact(&self, msg: &DynamicMessage) -> String {
        let fields: Vec<String> = msg
            .descriptor()
            .fields()
            .filter_map(|field_desc| {
                // Only show fields that are set
                if field_desc.supports_presence() && !msg.has_field(&field_desc) {
                    return None;
                }

                let value = msg.get_field(&field_desc);
                let formatted_value = match &*value {
                    Value::Message(sub_msg) => self.format_message_compact(sub_msg),
                    _ => self.format_scalar(&value, &field_desc),
                };

                if formatted_value.is_empty() {
                    None
                } else {
                    Some(format!("{}:{}", field_desc.name(), formatted_value))
                }
            })
            .collect();

        fields.join(" ")
    }
}