arcgis 0.1.3

Type-safe Rust SDK for the ArcGIS REST API with compile-time guarantees
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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
//! 📝 Feature Service - Batch Editing Operations
//!
//! Demonstrates atomic batch editing operations for feature services.
//! Learn how to efficiently add, update, and delete features in single transactions,
//! ensuring data integrity and optimal performance.
//!
//! # What You'll Learn
//!
//! - **Atomic editing**: Apply adds, updates, and deletes in one transaction
//! - **Bulk updates**: Update multiple features efficiently
//! - **Global IDs**: Use global IDs for replicated/offline editing scenarios
//! - **Table definitions**: Query table schema and metadata
//! - **Transaction control**: Rollback on failure, session management
//!
//! # Prerequisites
//!
//! - ArcGIS Feature Service with edit capabilities
//! - Appropriate authentication (API key or OAuth)
//! - Features with OBJECTID field (and optionally GlobalID)
//!
//! ## Environment Variables
//!
//! Set these in your `.env` file:
//!
//! ```env
//! # Feature service base URL
//! ARCGIS_FEATURE_URL=https://your-server.com/arcgis/rest/services/MyService/FeatureServer
//!
//! # Authentication (choose one)
//! ARCGIS_ENTERPRISE_KEY=your_enterprise_api_key
//! # OR
//! ARCGIS_FEATURES_KEY=your_features_api_key
//! # OR
//! ARCGIS_CLIENT_ID=your_oauth_client_id
//! ARCGIS_CLIENT_SECRET=your_oauth_client_secret
//! ```
//!
//! # Running
//!
//! ```bash
//! cargo run --example feature_service_batch_editing
//!
//! # With debug logging:
//! RUST_LOG=debug cargo run --example feature_service_batch_editing
//! ```
//!
//! # Real-World Use Cases
//!
//! - **Data import**: Bulk load features from external sources
//! - **Maintenance**: Update multiple asset statuses in one transaction
//! - **Quality control**: Batch update attributes after validation
//! - **Synchronization**: Apply offline edits using global IDs
//! - **Cleanup**: Delete obsolete features efficiently

use anyhow::Result;
use arcgis::example_tracker::ExampleTracker;
use arcgis::{
    ApiKeyAuth, ApiKeyTier, ArcGISClient, EditOptions, EnvConfig, Feature, FeatureServiceClient,
    LayerId, ObjectId,
};
use secrecy::ExposeSecret;
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize tracing for structured logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .init();

    // Start accountability tracking
    let tracker = ExampleTracker::new("feature_service_batch_editing")
        .methods(&[
            "get_table_definition",
            "apply_edits",
            "update_features",
            "apply_edits_with_global_ids",
        ])
        .service_type("FeatureServiceClient")
        .start();

    tracing::info!("📝 ArcGIS Feature Service - Batch Editing Examples");
    tracing::info!("Demonstrating atomic editing operations");
    tracing::info!("");

    // Load feature service URL from environment
    let config = EnvConfig::global();
    let feature_url = config
        .arcgis_feature_url
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!(
            "ARCGIS_FEATURE_URL not set in .env file.\n\
             Example: ARCGIS_FEATURE_URL=https://your-server.com/arcgis/rest/services/MyService/FeatureServer"
        ))?;

    tracing::info!("Feature Service: {}", feature_url);
    tracing::info!("");

    // Create authenticated client
    tracing::debug!("Creating authenticated client");

    // Use enterprise key for enterprise servers, fallback to features key
    let auth = if let Some(enterprise_key) = &config.arcgis_enterprise_key {
        tracing::debug!("Using ARCGIS_ENTERPRISE_KEY for authentication");
        ApiKeyAuth::new(enterprise_key.expose_secret())
    } else {
        tracing::debug!("Using ARCGIS_FEATURES_KEY for authentication");
        ApiKeyAuth::from_env(ApiKeyTier::Features)?
    };

    let client = ArcGISClient::new(auth);
    let fs_client = FeatureServiceClient::new(feature_url, &client);

    // Demonstrate batch editing operations
    demonstrate_get_table_definition(&fs_client).await?;
    demonstrate_apply_edits(&fs_client).await?;
    demonstrate_update_features(&fs_client).await?;
    demonstrate_apply_edits_with_global_ids(&fs_client).await?;

    tracing::info!("\n✅ All batch editing examples completed successfully!");
    tracing::info!("🎉 100% FeatureServiceClient batch editing coverage achieved!");
    print_best_practices();

    // Mark tracking as successful
    tracker.success();
    Ok(())
}

/// Demonstrates querying table definition metadata.
async fn demonstrate_get_table_definition(fs_client: &FeatureServiceClient<'_>) -> Result<()> {
    tracing::info!("\n=== Example 1: Get Table Definition ===");
    tracing::info!("Query schema and metadata for a table");
    tracing::info!("");

    // Query first table/layer (usually layer 0)
    let layer_id = LayerId::new(0);

    tracing::info!("Querying table definition for layer {}...", layer_id);

    let table_def = fs_client.get_table_definition(layer_id).await?;

    tracing::info!("✅ Table definition retrieved");
    tracing::info!("   Name: {}", table_def.name());

    if let Some(table_type) = table_def.table_type() {
        tracing::info!("   Type: {}", table_type);
    }

    let fields = table_def.fields();
    tracing::info!("   Fields: {} total", fields.len());

    // Show first few fields
    for (idx, field) in fields.iter().take(5).enumerate() {
        let nullable_str = field
            .nullable()
            .map(|n| if n { "nullable" } else { "not null" })
            .unwrap_or("unknown");

        tracing::info!(
            "      {}. {} ({:?}, {})",
            idx + 1,
            field.name(),
            field.field_type(),
            nullable_str
        );
    }

    if fields.len() > 5 {
        tracing::info!("      ... and {} more fields", fields.len() - 5);
    }

    // Assertions
    anyhow::ensure!(!fields.is_empty(), "Table should have at least one field");

    if let Some(object_id_field) = table_def.object_id_field() {
        tracing::info!("   Object ID field: {}", object_id_field);
    }

    if let Some(global_id_field) = table_def.global_id_field() {
        tracing::info!("   Global ID field: {}", global_id_field);
    }

    // Note: TableDefinition doesn't include capabilities
    // Use FeatureServiceClient.get_service_definition() for capabilities

    tracing::info!("");
    tracing::info!("💡 Table definition:");
    tracing::info!("   • Describes schema: fields, types, constraints");
    tracing::info!("   • Identifies key fields (OBJECTID, GlobalID)");
    tracing::info!("   • Lists capabilities (Create, Update, Delete, etc.)");
    tracing::info!("   • Essential for building dynamic editing UIs");

    Ok(())
}

/// Demonstrates atomic batch editing with apply_edits.
async fn demonstrate_apply_edits(fs_client: &FeatureServiceClient<'_>) -> Result<()> {
    tracing::info!("\n=== Example 2: Atomic Batch Editing (apply_edits) ===");
    tracing::info!("Add, update, and delete features in single transaction");
    tracing::info!("");

    let layer_id = LayerId::new(0);

    // Prepare features to add
    let mut new_attrs1 = HashMap::new();
    new_attrs1.insert("name".to_string(), json!("Batch Test Feature 1"));
    new_attrs1.insert("description".to_string(), json!("Created by apply_edits"));
    let feature_to_add1 = Feature::new(new_attrs1, None);

    let mut new_attrs2 = HashMap::new();
    new_attrs2.insert("name".to_string(), json!("Batch Test Feature 2"));
    new_attrs2.insert(
        "description".to_string(),
        json!("Also created by apply_edits"),
    );
    let feature_to_add2 = Feature::new(new_attrs2, None);

    tracing::info!("Preparing batch operation:");
    tracing::info!("   • Adding 2 new features");
    tracing::info!("   • Rollback enabled (all or nothing)");
    tracing::info!("");

    // Apply edits with rollback on failure
    let options = EditOptions {
        rollback_on_failure: Some(true),
        ..Default::default()
    };

    let result = fs_client
        .apply_edits(
            layer_id,
            Some(vec![feature_to_add1, feature_to_add2]),
            None, // No updates
            None, // No deletes
            options,
        )
        .await?;

    // Validate results
    tracing::info!("✅ Batch edit completed");
    tracing::info!("   Total success: {}", result.success_count());
    tracing::info!("   Total failure: {}", result.failure_count());
    tracing::info!("   Add results: {}", result.add_results().len());

    // Assertions
    anyhow::ensure!(result.add_results().len() == 2, "Should have 2 add results");

    anyhow::ensure!(
        result.all_succeeded(),
        "All operations should succeed. Failures: {:?}",
        result
            .add_results()
            .iter()
            .filter(|r| !r.success())
            .collect::<Vec<_>>()
    );

    // Get the created Object IDs for cleanup
    let created_ids: Vec<ObjectId> = result
        .add_results()
        .iter()
        .filter_map(|r| *r.object_id())
        .collect();

    anyhow::ensure!(created_ids.len() == 2, "Should have 2 created object IDs");

    tracing::info!("   Created IDs: {:?}", created_ids);

    // Clean up - delete the features we just created
    tracing::info!("");
    tracing::info!("Cleaning up created features...");

    let cleanup_result = fs_client
        .apply_edits(
            layer_id,
            None,              // No adds
            None,              // No updates
            Some(created_ids), // Delete the ones we created
            EditOptions::default(),
        )
        .await?;

    anyhow::ensure!(cleanup_result.all_succeeded(), "Cleanup should succeed");

    tracing::info!("✅ Cleanup completed");

    tracing::info!("");
    tracing::info!("💡 apply_edits advantages:");
    tracing::info!("   • Atomic: all operations succeed or all fail");
    tracing::info!("   • Efficient: single request for multiple operations");
    tracing::info!("   • Mixed operations: add + update + delete together");
    tracing::info!("   • Transaction control: rollback_on_failure option");

    Ok(())
}

/// Demonstrates bulk update operations.
async fn demonstrate_update_features(fs_client: &FeatureServiceClient<'_>) -> Result<()> {
    tracing::info!("\n=== Example 3: Bulk Feature Updates ===");
    tracing::info!("Update multiple features efficiently");
    tracing::info!("");

    let layer_id = LayerId::new(0);

    // First, create test features to update
    let mut attrs1 = HashMap::new();
    attrs1.insert("name".to_string(), json!("Update Test 1"));
    attrs1.insert("status".to_string(), json!("initial"));
    let feature1 = Feature::new(attrs1, None);

    let mut attrs2 = HashMap::new();
    attrs2.insert("name".to_string(), json!("Update Test 2"));
    attrs2.insert("status".to_string(), json!("initial"));
    let feature2 = Feature::new(attrs2, None);

    tracing::info!("Creating test features...");

    let add_result = fs_client
        .add_features(layer_id, vec![feature1, feature2], EditOptions::default())
        .await?;

    anyhow::ensure!(
        add_result.all_succeeded(),
        "Feature creation should succeed"
    );

    let created_ids: Vec<(i64, ObjectId)> = add_result
        .add_results()
        .iter()
        .enumerate()
        .filter_map(|(idx, r)| r.object_id().as_ref().map(|id| (idx as i64, *id)))
        .collect();

    anyhow::ensure!(created_ids.len() == 2, "Should create 2 features");

    tracing::info!("✅ Created {} test features", created_ids.len());
    tracing::info!("");

    // Now prepare bulk updates
    let mut update1_attrs = HashMap::new();
    update1_attrs.insert("OBJECTID".to_string(), json!(created_ids[0].1.get()));
    update1_attrs.insert("status".to_string(), json!("updated"));
    let update1 = Feature::new(update1_attrs, None);

    let mut update2_attrs = HashMap::new();
    update2_attrs.insert("OBJECTID".to_string(), json!(created_ids[1].1.get()));
    update2_attrs.insert("status".to_string(), json!("updated"));
    let update2 = Feature::new(update2_attrs, None);

    tracing::info!("Updating features in bulk...");

    let update_result = fs_client
        .update_features(
            layer_id,
            vec![update1, update2],
            EditOptions {
                rollback_on_failure: Some(true),
                ..Default::default()
            },
        )
        .await?;

    // Validate update results
    tracing::info!("✅ Bulk update completed");
    tracing::info!("   Updated: {}", update_result.success_count());
    tracing::info!("   Failed: {}", update_result.failure_count());

    // Assertions
    anyhow::ensure!(
        update_result.update_results().len() == 2,
        "Should have 2 update results"
    );

    anyhow::ensure!(update_result.all_succeeded(), "All updates should succeed");

    // Cleanup
    tracing::info!("");
    tracing::info!("Cleaning up test features...");

    let cleanup_ids: Vec<ObjectId> = created_ids.into_iter().map(|(_, id)| id).collect();

    let cleanup_result = fs_client
        .delete_features(layer_id, cleanup_ids, EditOptions::default())
        .await?;

    anyhow::ensure!(cleanup_result.all_succeeded(), "Cleanup should succeed");

    tracing::info!("✅ Cleanup completed");

    tracing::info!("");
    tracing::info!("💡 update_features:");
    tracing::info!("   • Bulk updates are more efficient than individual edits");
    tracing::info!("   • Features must include OBJECTID to identify which to update");
    tracing::info!("   • Partial updates: only specify fields to change");
    tracing::info!("   • Use rollback_on_failure for atomic bulk updates");

    Ok(())
}

/// Demonstrates editing with global IDs.
async fn demonstrate_apply_edits_with_global_ids(
    fs_client: &FeatureServiceClient<'_>,
) -> Result<()> {
    tracing::info!("\n=== Example 4: Global ID Editing ===");
    tracing::info!("Use global IDs for replicated/offline editing scenarios");
    tracing::info!("");

    let layer_id = LayerId::new(0);

    // Note: Global IDs are typically auto-generated by the service
    // For demonstration, we'll create features and then work with their global IDs

    tracing::info!("Creating test feature (global ID will be auto-generated)...");

    let mut new_attrs = HashMap::new();
    new_attrs.insert("name".to_string(), json!("Global ID Test Feature"));
    new_attrs.insert(
        "description".to_string(),
        json!("Testing global ID operations"),
    );
    let feature = Feature::new(new_attrs, None);

    let add_result = fs_client
        .add_features(layer_id, vec![feature], EditOptions::default())
        .await?;

    anyhow::ensure!(
        add_result.all_succeeded(),
        "Feature creation should succeed"
    );

    let first_result = add_result
        .add_results()
        .first()
        .ok_or_else(|| anyhow::anyhow!("No add results returned"))?;

    let object_id = first_result
        .object_id()
        .ok_or_else(|| anyhow::anyhow!("No object ID returned"))?;

    let global_id = first_result.global_id().clone().ok_or_else(|| {
        anyhow::anyhow!("No global ID returned (service may not support global IDs)")
    })?;

    tracing::info!("✅ Feature created");
    tracing::info!("   Object ID: {}", object_id);
    tracing::info!("   Global ID: {}", global_id);
    tracing::info!("");

    // Now demonstrate updating using global IDs
    tracing::info!("Updating feature using global ID...");

    let mut update_attrs = HashMap::new();
    update_attrs.insert("globalId".to_string(), json!(global_id));
    update_attrs.insert("status".to_string(), json!("updated via global ID"));
    let update_feature = Feature::new(update_attrs, None);

    let update_result = fs_client
        .apply_edits_with_global_ids(
            layer_id,
            None,                       // No adds
            Some(vec![update_feature]), // Update by global ID
            None,                       // No deletes
            EditOptions::default(),
        )
        .await?;

    tracing::info!("✅ Update via global ID completed");
    tracing::info!("   Success: {}", update_result.success_count());

    anyhow::ensure!(
        update_result.all_succeeded(),
        "Global ID update should succeed"
    );

    // Cleanup using global ID
    tracing::info!("");
    tracing::info!("Cleaning up using global ID...");

    let delete_result = fs_client
        .apply_edits_with_global_ids(
            layer_id,
            None,                              // No adds
            None,                              // No updates
            Some(vec![global_id.to_string()]), // Delete by global ID
            EditOptions::default(),
        )
        .await?;

    anyhow::ensure!(
        delete_result.all_succeeded(),
        "Global ID delete should succeed"
    );

    tracing::info!("✅ Cleanup completed");

    tracing::info!("");
    tracing::info!("💡 Global ID editing:");
    tracing::info!("   • Global IDs are stable across replicas and syncs");
    tracing::info!("   • Essential for disconnected/offline editing");
    tracing::info!("   • Used in multi-user collaborative workflows");
    tracing::info!("   • Survives data migration and replication");
    tracing::info!("   • Automatically generated by service (if enabled)");

    Ok(())
}

/// Prints best practices for batch editing.
fn print_best_practices() {
    tracing::info!("\n💡 Batch Editing Best Practices:");
    tracing::info!("   - Use apply_edits for mixed operations (add + update + delete)");
    tracing::info!("   - Enable rollback_on_failure for atomic transactions");
    tracing::info!("   - Batch multiple edits to reduce network round-trips");
    tracing::info!("   - Use update_features for bulk attribute updates");
    tracing::info!("   - Prefer global IDs for replicated/offline scenarios");
    tracing::info!("   - Always validate EditResult success before proceeding");
    tracing::info!("");
    tracing::info!("📊 Edit Operation Types:");
    tracing::info!("   • add_features - Add new features");
    tracing::info!("   • update_features - Update existing features by OBJECTID");
    tracing::info!("   • delete_features - Delete features by OBJECTID");
    tracing::info!("   • apply_edits - Batch add/update/delete in one transaction");
    tracing::info!("   • apply_edits_with_global_ids - Use global IDs instead of OBJECTIDs");
    tracing::info!("   • calculate_records - Bulk field calculations with SQL");
    tracing::info!("");
    tracing::info!("⚙️  Transaction Control:");
    tracing::info!("   • rollback_on_failure - All or nothing (recommended)");
    tracing::info!("   • session_id - Edit session for versioned geodatabases");
    tracing::info!("   • gdb_version - Target specific version");
    tracing::info!("   • use_global_ids - Use global IDs for identification");
    tracing::info!("");
    tracing::info!("📊 Coverage:");
    tracing::info!("   ✅ 4/6 batch editing methods demonstrated:");
    tracing::info!("      • apply_edits ✅");
    tracing::info!("      • update_features ✅");
    tracing::info!("      • apply_edits_with_global_ids ✅");
    tracing::info!("      • get_table_definition ✅");
    tracing::info!("      • truncate (skipped - destructive)");
    tracing::info!("      • get_service_definition (covered in other examples)");
}