nsip 0.4.0

NSIP Search API client for nsipsearch.nsip.org/api
Documentation
# Getting Started with NSIP

> **Learning Goal:** By the end of this tutorial, you will have a working Rust program that connects to the NSIP Search API, lists sheep breed groups, searches for animals, and retrieves detailed genetic data.

**Time to complete:** 15 minutes
**Prerequisites:** Rust 1.92+ installed ([rustup.rs](https://rustup.rs/))

---

## What You Will Build

A command-line Rust program that:

1. Connects to the NSIP Search API
2. Lists available breed groups
3. Searches for animals by breed
4. Retrieves detailed genetic data for a specific animal

---

## Step 1: Create a New Project

Open a terminal and create a new Rust project:

```bash
cargo new nsip-demo
cd nsip-demo
```

Add the required dependencies:

```bash
cargo add nsip tokio --features tokio/full
```

Your `Cargo.toml` should now include:

```toml
[dependencies]
nsip = "0.3"
tokio = { version = "1", features = ["full"] }
```

**What just happened?** You created a new Rust binary project and added the `nsip` crate (the NSIP API client) and `tokio` (an async runtime required for making HTTP requests).

---

## Step 2: List Breed Groups

Replace the contents of `src/main.rs` with:

```rust
use nsip::NsipClient;

#[tokio::main]
async fn main() -> Result<(), nsip::Error> {
    // Create a new client with default settings
    let client = NsipClient::new();

    // Fetch all breed groups from the NSIP database
    let breed_groups = client.breed_groups().await?;

    println!("Available Breed Groups:\n");
    for group in &breed_groups {
        println!("  {} (ID: {})", group.name, group.id);
        for breed in &group.breeds {
            println!("    - {} (ID: {})", breed.name, breed.id);
        }
        println!();
    }

    Ok(())
}
```

Run the program:

```bash
cargo run
```

You should see output similar to:

```
Available Breed Groups:

  USA Hair (ID: 61)
    - Katahdin (ID: 640)
    - Dorper (ID: 644)
    - St. Croix (ID: 648)
    ...
```

**What just happened?**

- `NsipClient::new()` creates a client with default settings (30-second timeout, 3 retries on server errors).
- `breed_groups()` is an async method that fetches all available sheep breeds from the NSIP database.
- The API organizes breeds into groups (USA Hair, USA Terminal, USA Maternal, USA Range, etc.). Each group contains one or more breeds.
- The `?` operator propagates any errors up to `main`, which returns `Result`.

---

## Step 3: Search for Animals

Now replace `src/main.rs` with a program that searches for animals:

```rust
use nsip::{NsipClient, SearchCriteria};

#[tokio::main]
async fn main() -> Result<(), nsip::Error> {
    let client = NsipClient::new();

    // Build search criteria using the builder pattern
    let criteria = SearchCriteria::new()
        .with_status("CURRENT")
        .with_gender("Male");

    // Search for animals in breed 640 (first page, 5 results)
    let results = client
        .search_animals(
            0,              // page number (0-based)
            5,              // results per page
            Some(640),      // breed_id
            None,           // sorted_trait (no sorting)
            None,           // reverse sort
            Some(&criteria),
        )
        .await?;

    println!("Found {} animals total\n", results.total_count);
    println!("Showing page {} ({} results):\n", results.page, results.results.len());

    for animal in &results.results {
        println!("  {}", animal);
    }

    Ok(())
}
```

Run it:

```bash
cargo run
```

**What just happened?**

- `SearchCriteria::new()` creates an empty filter. The builder methods (`with_status`, `with_gender`) add constraints.
- `with_status("CURRENT")` limits results to active, living animals.
- `with_gender("Male")` filters to rams only. Valid values are `"Male"`, `"Female"`, and `"Both"`.
- `search_animals()` takes pagination parameters (page number and page size), an optional breed ID, optional sorting, and optional search criteria.
- The `results.results` field contains the matching animals as JSON values. The `total_count` tells you how many animals matched overall.

---

## Step 4: Get Animal Details

To fetch detailed genetic information for a specific animal, use the `animal_details` method. Replace `src/main.rs`:

```rust
use nsip::NsipClient;

#[tokio::main]
async fn main() -> Result<(), nsip::Error> {
    let client = NsipClient::new();

    // Look up a specific animal by search string
    let details = client.animal_details("400001").await?;

    println!("Animal: {}", details.lpn_id);

    if let Some(breed) = &details.breed {
        println!("Breed: {}", breed);
    }
    if let Some(gender) = &details.gender {
        println!("Gender: {}", gender);
    }
    if let Some(status) = &details.status {
        println!("Status: {}", status);
    }
    if let Some(dob) = &details.date_of_birth {
        println!("Date of Birth: {}", dob);
    }

    // Display EBV traits
    if !details.traits.is_empty() {
        println!("\nEBV Traits:");
        for (abbreviation, trait_data) in &details.traits {
            print!("  {} ({}) = {:.2}",
                abbreviation,
                trait_data.name,
                trait_data.value,
            );
            if let Some(acc) = trait_data.accuracy {
                print!("  (accuracy: {}%)", acc);
            }
            if let Some(units) = &trait_data.units {
                print!("  {}", units);
            }
            println!();
        }
    }

    Ok(())
}
```

Run it:

```bash
cargo run
```

**What just happened?**

- `animal_details()` fetches comprehensive data for a single animal, including breed information, status, and all EBV (Estimated Breeding Value) traits.
- The `traits` field is a `HashMap<String, Trait>` keyed by trait abbreviation (e.g., `"BWT"` for Birth Weight, `"WWT"` for Weaning Weight).
- Each `Trait` contains the full name, numeric value, optional accuracy percentage, and optional units.
- Most fields on `AnimalDetails` are `Option` types because not all data is available for every animal.

---

## Step 5: Fetch a Complete Profile

The `search_by_lpn` method combines details, lineage, and progeny into a single `AnimalProfile`:

```rust
use nsip::NsipClient;

#[tokio::main]
async fn main() -> Result<(), nsip::Error> {
    let client = NsipClient::new();

    let profile = client.search_by_lpn("400001").await?;

    // Details
    println!("Animal: {}", profile.details.lpn_id);
    if let Some(breed) = &profile.details.breed {
        println!("Breed: {}", breed);
    }

    // Lineage
    if let Some(sire) = &profile.lineage.sire {
        println!("Sire: {}", sire.lpn_id);
    }
    if let Some(dam) = &profile.lineage.dam {
        println!("Dam: {}", dam.lpn_id);
    }

    // Progeny summary
    println!("Total progeny: {}", profile.progeny.total_count);
    for offspring in &profile.progeny.animals {
        print!("  {} ", offspring.lpn_id);
        if let Some(sex) = &offspring.sex {
            print!("({})", sex);
        }
        println!();
    }

    Ok(())
}
```

**What just happened?**

- `search_by_lpn()` returns an `AnimalProfile` that bundles three pieces of data: `details` (an `AnimalDetails`), `lineage` (a `Lineage` with sire, dam, and multi-generational pedigree), and `progeny` (a `Progeny` with offspring list).
- Unlike `animal_details()`, which returns only the animal's own data, `search_by_lpn()` gives you the full picture in one call.
- The `lineage.generations` field contains a nested vector of ancestors organized by generation depth.

---

## Step 6: Handle Errors

NSIP API calls can fail for various reasons. Here is how to handle errors gracefully:

```rust
use nsip::{Error, NsipClient};

#[tokio::main]
async fn main() {
    let client = NsipClient::new();

    match client.animal_details("INVALID_ID").await {
        Ok(details) => {
            println!("Found: {}", details.lpn_id);
        }
        Err(Error::NotFound(msg)) => {
            eprintln!("Animal not found: {}", msg);
        }
        Err(Error::Timeout(msg)) => {
            eprintln!("Request timed out: {}", msg);
        }
        Err(Error::Api { status, message }) => {
            eprintln!("API error (HTTP {}): {}", status, message);
        }
        Err(Error::Connection(msg)) => {
            eprintln!("Connection failed: {}", msg);
        }
        Err(e) => {
            eprintln!("Unexpected error: {}", e);
        }
    }
}
```

**What just happened?**

- The `nsip::Error` enum has six variants covering all failure modes: `Validation`, `Api`, `NotFound`, `Timeout`, `Connection`, and `Parse`.
- Pattern matching lets you provide specific user-facing messages for each error type.
- See the [Error Handling Reference]../reference/ERROR-HANDLING.md for full details on each variant.

---

## What You Learned

In this tutorial you:

- Created an `NsipClient` to connect to the NSIP Search API
- Used `breed_groups()` to discover available sheep breeds
- Built search filters with `SearchCriteria` and the builder pattern
- Retrieved individual animal details and EBV traits
- Fetched complete profiles including lineage and progeny
- Handled API errors with pattern matching

---

## Next Steps

Now that you have a working setup, continue with these tutorials:

- [Your First API Query]FIRST-API-QUERY.md -- a deeper dive into searching and filtering animals
- [Interpreting Results]INTERPRETING-RESULTS.md -- understand what the genetic data means
- [MCP Server Setup]MCP-SERVER-SETUP.md -- connect AI assistants to NSIP data

For task-oriented instructions, see the How-To Guides:

- [How to Configure Timeout and Retries]../how-to/CONFIGURE-CLIENT.md
- [How to Compare Animals]../how-to/COMPARE-ANIMALS.md

For background reading:

- [Understanding EBVs]../explanation/EBV-EXPLAINED.md -- what Estimated Breeding Values mean
- [Error Handling Reference]../reference/ERROR-HANDLING.md -- complete error type documentation