1 unstable release
new 0.1.0 | May 14, 2025 |
---|
#302 in Data structures
150KB
3K
SLoC
OpenMenuStandard Rust Implementation - README
This repository contains a Rust implementation of the OpenMenuStandard (OMS) specification. The library provides a complete data model, validation, serialization/deserialization, and utility functions for working with OpenMenuStandard documents.
Features
- Complete implementation of the OpenMenuStandard 1.0 specification
- Comprehensive data model with all required and optional fields
- JSON serialization and deserialization using serde
- Document validation using validator
- Utility functions for creating, parsing, and manipulating OMS documents
- Support for OMS URL scheme parsing and generation
- Extensive test coverage
Getting Started
Prerequisites
Add the following dependencies to your Cargo.toml
:
[dependencies]
open-menu-standard = "0.1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
validator = { version = "0.16", features = ["derive"] }
thiserror = "1.0"
Basic Usage
use open_menu_standard::{create_minimal_document, parse_oms_document};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a minimal document
let document = create_minimal_document(
"subway-usa",
"Subway",
"restaurant",
"italian-bmt",
"Italian B.M.T.",
"sandwich"
)?;
// Serialize to JSON
let json = document.to_json()?;
println!("{}", json);
// Parse from JSON
let parsed_document = parse_oms_document(&json)?;
// Generate a URL for the document
let url = parsed_document.create_url().unwrap();
println!("OMS URL: {}", url);
Ok(())
}
Examples
Creating a Sandwich Menu
This example creates a menu for a sandwich shop with customization options and nutritional information.
use open_menu_standard::{
OmsDocument, Metadata, Vendor, Item, Nutrition, Customization,
CustomizationType, CustomizationDefault, CustomizationOption,
MeasurementValue, NutrientWithDetails,
};
use chrono::Utc;
use std::collections::HashMap;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create metadata
let metadata = Metadata {
created: Utc::now(),
source: "sandwich-builder-app".to_string(),
locale: "en-US".to_string(),
};
// Create vendor
let vendor = Vendor {
id: "subway-usa".to_string(),
name: "Subway".to_string(),
r#type: "restaurant".to_string(),
location_id: Some("store-1234".to_string()),
location_name: Some("Downtown".to_string()),
address: None,
contact: None,
hours: None,
cuisine: None,
services: None,
};
// Create nutrition information
let mut nutrition = Nutrition {
serving_size: Some(MeasurementValue {
value: 240.0,
unit: "g".to_string(),
}),
calories: Some(410.0),
protein: Some(MeasurementValue {
value: 22.0,
unit: "g".to_string(),
}),
fat: Some(NutrientWithDetails {
value: 16.0,
unit: "g".to_string(),
details: None,
}),
carbohydrates: Some(NutrientWithDetails {
value: 47.0,
unit: "g".to_string(),
details: None,
}),
sodium: None,
cholesterol: None,
vitamins: None,
minerals: None,
allergens: Some(vec!["wheat".to_string(), "dairy".to_string()]),
dietary_flags: Some(vec!["contains_gluten".to_string()]),
health_claims: None,
ingredients: None,
nutrition_standards: None,
};
// Create bread customization
let bread_customization = Customization {
id: "bread".to_string(),
name: "Bread Type".to_string(),
r#type: CustomizationType::SingleSelect,
required: true,
default: CustomizationDefault::String("italian-herbs".to_string()),
min_selections: None,
max_selections: None,
min: None,
max: None,
step: None,
unit_price_adjustment: None,
unit_nutrition_adjustments: None,
options: Some(vec![
CustomizationOption {
id: "italian-herbs".to_string(),
name: "Italian Herbs & Cheese".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: Some(vec!["wheat".to_string(), "dairy".to_string()]),
dietary_flags: Some(vec!["contains_gluten".to_string()]),
},
CustomizationOption {
id: "wheat".to_string(),
name: "Wheat Bread".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: Some(vec!["wheat".to_string()]),
dietary_flags: Some(vec!["contains_gluten".to_string()]),
},
CustomizationOption {
id: "flatbread".to_string(),
name: "Flatbread".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: Some(vec!["wheat".to_string()]),
dietary_flags: Some(vec!["contains_gluten".to_string()]),
},
]),
};
// Create cheese customization
let cheese_customization = Customization {
id: "cheese".to_string(),
name: "Cheese".to_string(),
r#type: CustomizationType::SingleSelect,
required: true,
default: CustomizationDefault::String("american".to_string()),
min_selections: None,
max_selections: None,
min: None,
max: None,
step: None,
unit_price_adjustment: None,
unit_nutrition_adjustments: None,
options: Some(vec![
CustomizationOption {
id: "american".to_string(),
name: "American Cheese".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: Some(vec!["dairy".to_string()]),
dietary_flags: None,
},
CustomizationOption {
id: "provolone".to_string(),
name: "Provolone Cheese".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: Some(vec!["dairy".to_string()]),
dietary_flags: None,
},
CustomizationOption {
id: "none".to_string(),
name: "No Cheese".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: None,
dietary_flags: None,
},
]),
};
// Create vegetables customization
let vegetables_customization = Customization {
id: "vegetables".to_string(),
name: "Vegetables".to_string(),
r#type: CustomizationType::MultiSelect,
required: false,
default: CustomizationDefault::StringArray(vec![
"lettuce".to_string(),
"tomato".to_string(),
"onion".to_string()
]),
min_selections: Some(0),
max_selections: Some(10),
min: None,
max: None,
step: None,
unit_price_adjustment: None,
unit_nutrition_adjustments: None,
options: Some(vec![
CustomizationOption {
id: "lettuce".to_string(),
name: "Lettuce".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: None,
dietary_flags: None,
},
CustomizationOption {
id: "tomato".to_string(),
name: "Tomato".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: None,
dietary_flags: None,
},
CustomizationOption {
id: "onion".to_string(),
name: "Onion".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: None,
dietary_flags: None,
},
CustomizationOption {
id: "cucumber".to_string(),
name: "Cucumber".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: None,
dietary_flags: None,
},
CustomizationOption {
id: "peppers".to_string(),
name: "Bell Peppers".to_string(),
price_adjustment: Some(0.0),
nutrition_adjustments: None,
allergens: None,
dietary_flags: None,
},
]),
};
// Create sandwich item
let item = Item {
id: "italian-bmt".to_string(),
name: "Italian B.M.T.".to_string(),
category: "sandwich".to_string(),
vendor_id: None,
description: Some("Italian B.M.T.® sandwich with Genoa salami, spicy pepperoni, and Black Forest ham".to_string()),
subcategory: None,
image_url: Some("https://example.com/images/italian-bmt.jpg".to_string()),
base_price: Some(7.99),
currency: Some("USD".to_string()),
nutrition: Some(nutrition),
customizations: Some(vec![
bread_customization,
cheese_customization,
vegetables_customization,
]),
selected_customizations: None,
quantity: None,
item_note: None,
calculated: None,
components: None,
availability: None,
popularity: None,
};
// Create the OMS document
let document = OmsDocument::new(metadata, vendor, vec![item]);
// Validate and serialize
document.validate()?;
let json = document.to_json()?;
println!("{}", json);
Ok(())
}
Parsing an OMS URL
use open_menu_standard::parse_oms_url;
fn main() {
let url = "omenu://order?v=subway-usa&l=store-1234&i=italian-bmt";
if let Some(params) = parse_oms_url(url) {
println!("Action: {}", params.get("action").unwrap_or(&"".to_string()));
println!("Vendor: {}", params.get("v").unwrap_or(&"".to_string()));
println!("Location: {}", params.get("l").unwrap_or(&"".to_string()));
println!("Item: {}", params.get("i").unwrap_or(&"".to_string()));
} else {
println!("Invalid OMS URL");
}
}
Setting Up NFC Tags
This library can be used to generate content for NFC tags that implement the tap-to-order functionality in the OpenMenuStandard specification:
use open_menu_standard::{
OmsDocument, parse_oms_document, create_minimal_document
};
// Create or load an OMS document
let document = create_minimal_document(
"burger-joint",
"Burger Joint",
"restaurant",
"cheeseburger",
"Classic Cheeseburger",
"burger"
)?;
// Generate an OMS URL for NFC tag
let url = document.create_url().unwrap();
println!("URL for NFC tag: {}", url);
// Alternatively, for more complex data, generate a compact JSON representation
let json = document.to_json()?;
println!("JSON for NFC tag: {}", json);
API Documentation
Core Types
OmsDocument
: The main container for an OpenMenuStandard documentMetadata
: Information about the document itselfVendor
: Information about the food service providerItem
: Representation of a food or beverage productNutrition
: Nutritional information about an itemCustomization
: Ways in which an item can be modifiedOrder
: Collection of items being ordered
Main Functions
OmsDocument::new()
: Create a new OMS documentOmsDocument::validate()
: Validate a document against the specificationOmsDocument::to_json()
: Serialize a document to JSONOmsDocument::from_json()
: Deserialize a document from JSONOmsDocument::create_url()
: Generate an OMS URL for the documentparse_oms_document()
: Parse a document from JSONcreate_minimal_document()
: Create a basic document with minimal fieldsparse_oms_url()
: Parse an OMS URL and extract parameters
Resources
License
This library is licensed under the MIT License - see the LICENSE file for details.
Dependencies
~8–21MB
~286K SLoC