use crate::ArcGISEnvelope;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Default, Serialize, Deserialize, derive_builder::Builder, derive_getters::Getters,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct ServiceDefinition {
#[serde(default)]
#[builder(setter(into))]
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
service_description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
has_static_data: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
max_record_count: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
supported_query_formats: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
capabilities: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
layers: Vec<LayerDefinition>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
tables: Vec<TableDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
spatial_reference: Option<SpatialReferenceDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
initial_extent: Option<ArcGISEnvelope>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
allow_geometry_updates: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
units: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
xss_prevention_info: Option<XssPreventionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
editor_tracking_info: Option<EditorTrackingInfo>,
}
impl ServiceDefinitionBuilder {
pub fn add_layer(mut self, layer: LayerDefinition) -> Self {
self.layers.get_or_insert_with(Vec::new).push(layer);
self
}
pub fn add_table(mut self, table: TableDefinition) -> Self {
self.tables.get_or_insert_with(Vec::new).push(table);
self
}
}
#[derive(
Debug, Clone, Serialize, Deserialize, derive_builder::Builder, derive_getters::Getters,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct LayerDefinition {
#[builder(setter(into))]
id: u32,
#[builder(setter(into))]
name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
#[builder(default)]
layer_type: Option<String>,
#[serde(rename = "geometryType")]
geometry_type: GeometryTypeDefinition,
#[serde(default)]
#[builder(default)]
fields: Vec<FieldDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
object_id_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
global_id_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
display_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
copyright_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
default_visibility: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
templates: Vec<FeatureTemplate>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
indexes: Vec<Index>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
edit_fields_info: Option<EditFieldsInfo>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
relationships: Vec<LayerRelationship>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
is_data_branch_versioned: Option<bool>,
}
impl LayerDefinitionBuilder {
pub fn add_field(mut self, field: FieldDefinition) -> Self {
self.fields.get_or_insert_with(Vec::new).push(field);
self
}
pub fn add_template(mut self, template: FeatureTemplate) -> Self {
self.templates.get_or_insert_with(Vec::new).push(template);
self
}
pub fn add_index(mut self, index: Index) -> Self {
self.indexes.get_or_insert_with(Vec::new).push(index);
self
}
pub fn add_relationship(mut self, relationship: LayerRelationship) -> Self {
self.relationships
.get_or_insert_with(Vec::new)
.push(relationship);
self
}
}
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
derive_builder::Builder,
derive_getters::Getters,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct FieldDefinition {
#[builder(setter(into))]
name: String,
#[serde(rename = "type")]
field_type: FieldType,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
length: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
nullable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
editable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
default_value: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
domain: Option<Domain>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
model_name: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FieldType {
#[serde(rename = "esriFieldTypeSmallInteger")]
SmallInteger,
#[serde(rename = "esriFieldTypeInteger")]
Integer,
#[serde(rename = "esriFieldTypeBigInteger")]
BigInteger,
#[serde(rename = "esriFieldTypeSingle")]
Single,
#[serde(rename = "esriFieldTypeDouble")]
Double,
#[serde(rename = "esriFieldTypeString")]
String,
#[serde(rename = "esriFieldTypeDate")]
Date,
#[serde(rename = "esriFieldTypeDateOnly")]
DateOnly,
#[serde(rename = "esriFieldTypeTimeOnly")]
TimeOnly,
#[serde(rename = "esriFieldTypeTimestampOffset")]
TimestampOffset,
#[serde(rename = "esriFieldTypeOID")]
Oid,
#[serde(rename = "esriFieldTypeGeometry")]
Geometry,
#[serde(rename = "esriFieldTypeBlob")]
Blob,
#[serde(rename = "esriFieldTypeRaster")]
Raster,
#[serde(rename = "esriFieldTypeGUID")]
Guid,
#[serde(rename = "esriFieldTypeGlobalID")]
GlobalId,
#[serde(rename = "esriFieldTypeXML")]
Xml,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GeometryTypeDefinition {
#[serde(rename = "esriGeometryPoint")]
Point,
#[serde(rename = "esriGeometryMultipoint")]
Multipoint,
#[serde(rename = "esriGeometryPolyline")]
Polyline,
#[serde(rename = "esriGeometryPolygon")]
Polygon,
#[serde(rename = "esriGeometryEnvelope")]
Envelope,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SpatialReferenceDefinition {
Wkid {
wkid: i32,
},
WkidWithLatest {
wkid: i32,
latest_wkid: i32,
},
Wkt {
wkt: String,
},
}
#[derive(
Debug, Clone, Default, Serialize, Deserialize, derive_builder::Builder, derive_getters::Getters,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct EditorTrackingInfo {
enable_editor_tracking: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
enable_ownership_access_control: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
allow_others_to_query: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
allow_others_to_update: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
allow_others_to_delete: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, derive_getters::Getters)]
#[serde(rename_all = "camelCase")]
pub struct XssPreventionInfo {
xss_prevention_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
xss_prevention_rule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
xss_input_rule: Option<String>,
}
#[derive(
Debug, Clone, Default, Serialize, Deserialize, derive_builder::Builder, derive_getters::Getters,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct TableDefinition {
#[builder(setter(into))]
id: u32,
#[builder(setter(into))]
name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
#[builder(default)]
table_type: Option<String>,
#[serde(default)]
#[builder(default)]
fields: Vec<FieldDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
object_id_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
global_id_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
display_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
copyright_text: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
templates: Vec<FeatureTemplate>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
indexes: Vec<Index>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
edit_fields_info: Option<EditFieldsInfo>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[builder(default)]
relationships: Vec<LayerRelationship>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
is_data_branch_versioned: Option<bool>,
}
impl TableDefinitionBuilder {
pub fn add_field(mut self, field: FieldDefinition) -> Self {
self.fields.get_or_insert_with(Vec::new).push(field);
self
}
pub fn add_template(mut self, template: FeatureTemplate) -> Self {
self.templates.get_or_insert_with(Vec::new).push(template);
self
}
pub fn add_index(mut self, index: Index) -> Self {
self.indexes.get_or_insert_with(Vec::new).push(index);
self
}
pub fn add_relationship(mut self, relationship: LayerRelationship) -> Self {
self.relationships
.get_or_insert_with(Vec::new)
.push(relationship);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Domain {
#[serde(rename = "codedValue")]
CodedValue(CodedValueDomain),
#[serde(rename = "range")]
Range(RangeDomain),
#[serde(rename = "inherited")]
Inherited,
}
#[derive(
Debug,
Clone,
Default,
PartialEq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct CodedValueDomain {
#[builder(setter(into))]
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
description: Option<String>,
#[builder(default)]
coded_values: Vec<CodedValue>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
merge_policy: Option<MergePolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
split_policy: Option<SplitPolicy>,
}
impl CodedValueDomainBuilder {
pub fn add_coded_value(mut self, coded_value: CodedValue) -> Self {
self.coded_values
.get_or_insert_with(Vec::new)
.push(coded_value);
self
}
}
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, derive_getters::Getters, derive_new::new,
)]
pub struct CodedValue {
name: String,
code: CodedValueCode,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CodedValueCode {
String(String),
Number(f64),
}
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct RangeDomain {
#[builder(setter(into))]
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
description: Option<String>,
range: [f64; 2],
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
merge_policy: Option<MergePolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
split_policy: Option<SplitPolicy>,
}
impl Default for RangeDomain {
fn default() -> Self {
Self {
name: String::new(),
description: None,
range: [0.0, 0.0],
merge_policy: None,
split_policy: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MergePolicy {
#[serde(rename = "esriMPTDefaultValue")]
DefaultValue,
#[serde(rename = "esriMPTSumValues")]
SumValues,
#[serde(rename = "esriMPTAreaWeighted")]
AreaWeighted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RelationshipCardinality {
#[serde(rename = "esriRelCardinalityOneToOne")]
OneToOne,
#[serde(rename = "esriRelCardinalityOneToMany")]
OneToMany,
#[serde(rename = "esriRelCardinalityManyToMany")]
ManyToMany,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RelationshipRole {
#[serde(rename = "esriRelRoleOrigin")]
Origin,
#[serde(rename = "esriRelRoleDestination")]
Destination,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct LayerRelationship {
id: i32,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
name: Option<String>,
role: RelationshipRole,
cardinality: RelationshipCardinality,
related_table_id: i32,
key_field: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
composite_key_field_name: Option<String>,
}
impl Default for LayerRelationship {
fn default() -> Self {
Self {
id: 0,
name: None,
role: RelationshipRole::Origin,
cardinality: RelationshipCardinality::OneToMany,
related_table_id: 0,
key_field: String::new(),
composite_key_field_name: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SplitPolicy {
#[serde(rename = "esriSPTDefaultValue")]
DefaultValue,
#[serde(rename = "esriSPTDuplicate")]
Duplicate,
#[serde(rename = "esriSPTGeometryRatio")]
GeometryRatio,
}
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct FeatureTemplate {
#[builder(setter(into))]
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
description: Option<String>,
prototype: TemplatePrototype,
drawing_tool: DrawingTool,
}
impl Default for FeatureTemplate {
fn default() -> Self {
Self {
name: String::new(),
description: None,
prototype: TemplatePrototype::default(),
drawing_tool: DrawingTool::None,
}
}
}
#[derive(
Debug,
Clone,
Default,
PartialEq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
pub struct TemplatePrototype {
#[builder(default)]
attributes: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DrawingTool {
#[serde(rename = "esriFeatureEditToolNone")]
None,
#[serde(rename = "esriFeatureEditToolPoint")]
Point,
#[serde(rename = "esriFeatureEditToolLine")]
Line,
#[serde(rename = "esriFeatureEditToolPolygon")]
Polygon,
#[serde(rename = "esriFeatureEditToolAutoCompletePolygon")]
AutoCompletePolygon,
#[serde(rename = "esriFeatureEditToolCircle")]
Circle,
#[serde(rename = "esriFeatureEditToolEllipse")]
Ellipse,
#[serde(rename = "esriFeatureEditToolRectangle")]
Rectangle,
#[serde(rename = "esriFeatureEditToolFreehand")]
Freehand,
}
#[derive(
Debug,
Clone,
Default,
PartialEq,
Eq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct Index {
#[builder(setter(into))]
name: String,
#[serde(
serialize_with = "serialize_index_fields",
deserialize_with = "deserialize_index_fields"
)]
fields: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
is_ascending: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
is_unique: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
description: Option<String>,
}
fn serialize_index_fields<S>(fields: &[String], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&fields.join(","))
}
fn deserialize_index_fields<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(s.split(',').map(|s| s.trim().to_string()).collect())
}
#[derive(
Debug,
Clone,
Default,
PartialEq,
Eq,
Serialize,
Deserialize,
derive_getters::Getters,
derive_builder::Builder,
)]
#[builder(setter(into, strip_option), default)]
#[serde(rename_all = "camelCase")]
pub struct EditFieldsInfo {
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
creation_date_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
creator_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
edit_date_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
editor_field: Option<String>,
}
impl Default for LayerDefinition {
fn default() -> Self {
Self {
id: 0,
name: String::new(),
layer_type: None,
geometry_type: GeometryTypeDefinition::Point,
fields: Vec::new(),
object_id_field: None,
global_id_field: None,
display_field: None,
description: None,
copyright_text: None,
default_visibility: None,
templates: Vec::new(),
indexes: Vec::new(),
edit_fields_info: None,
relationships: Vec::new(),
is_data_branch_versioned: None,
}
}
}
impl Default for FieldDefinition {
fn default() -> Self {
Self {
name: String::new(),
field_type: FieldType::String,
alias: None,
length: None,
nullable: None,
editable: None,
required: None,
default_value: None,
domain: None,
model_name: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)]
pub enum ServiceDefinitionValidationError {
#[display(
"{entity_type} '{}' (id={}): no ObjectID field found. \
Add a field with FieldType::Oid, nullable: false, editable: false. \
See https://developers.arcgis.com/rest/services-reference/enterprise/layer-feature-service/",
name,
id
)]
MissingObjectId {
entity_type: &'static str,
name: String,
id: u32,
},
#[display(
"{entity_type} '{}' (id={}): {} ObjectID fields found; only one is allowed.",
name,
id,
count
)]
MultipleObjectIds {
entity_type: &'static str,
name: String,
id: u32,
count: usize,
},
#[display(
"{entity_type} '{}' (id={}), field '{}': \
ObjectID field must have nullable=false and editable=false.",
name,
id,
field_name
)]
OidFieldInvalidConfig {
entity_type: &'static str,
name: String,
id: u32,
field_name: String,
},
#[display(
"{entity_type} '{}' (id={}): is_data_branch_versioned is true but no GlobalID field found. \
Add a field with FieldType::GlobalId, nullable: false, editable: false, length: 38, \
or remove is_data_branch_versioned.",
name,
id
)]
MissingGlobalIdForVersioning {
entity_type: &'static str,
name: String,
id: u32,
},
#[display(
"{entity_type} '{}' (id={}), field '{}': \
GlobalID field must have nullable=false and editable=false.",
name,
id,
field_name
)]
GlobalIdFieldInvalidConfig {
entity_type: &'static str,
name: String,
id: u32,
field_name: String,
},
#[display(
"{entity_type} '{}' (id={}): duplicate field name '{}'. \
Field names must be unique within a layer or table.",
name,
id,
field_name
)]
DuplicateFieldName {
entity_type: &'static str,
name: String,
id: u32,
field_name: String,
},
#[display(
"{entity_type} '{}' (id={}): {ref_type}_field references '{}' \
but no field with that name exists.",
name,
id,
field_name
)]
FieldRefNotFound {
entity_type: &'static str,
name: String,
id: u32,
ref_type: &'static str,
field_name: String,
},
#[display(
"Duplicate ID {} used by '{}' and '{}'. \
Layer and table IDs must be unique within the service.",
id,
first_name,
second_name
)]
DuplicateId {
id: u32,
first_name: String,
second_name: String,
},
}
struct FieldValidationCtx<'a> {
entity_type: &'static str,
name: &'a str,
id: u32,
fields: &'a [FieldDefinition],
object_id_field: &'a Option<String>,
global_id_field: &'a Option<String>,
display_field: &'a Option<String>,
is_data_branch_versioned: &'a Option<bool>,
}
fn validate_fields(
ctx: &FieldValidationCtx<'_>,
errors: &mut Vec<ServiceDefinitionValidationError>,
) {
let entity_type = ctx.entity_type;
let name = ctx.name;
let id = ctx.id;
let fields = ctx.fields;
let object_id_field = ctx.object_id_field;
let global_id_field = ctx.global_id_field;
let display_field = ctx.display_field;
let is_data_branch_versioned = ctx.is_data_branch_versioned;
let mut seen = std::collections::HashSet::new();
for field in fields {
let lower = field.name.to_lowercase();
if !seen.insert(lower) {
errors.push(ServiceDefinitionValidationError::DuplicateFieldName {
entity_type,
name: name.to_string(),
id,
field_name: field.name.clone(),
});
}
}
let oid_fields: Vec<&FieldDefinition> = fields
.iter()
.filter(|f| f.field_type == FieldType::Oid)
.collect();
match oid_fields.len() {
0 => errors.push(ServiceDefinitionValidationError::MissingObjectId {
entity_type,
name: name.to_string(),
id,
}),
1 => {
let oid = oid_fields[0];
if oid.nullable == Some(true) || oid.editable == Some(true) {
errors.push(ServiceDefinitionValidationError::OidFieldInvalidConfig {
entity_type,
name: name.to_string(),
id,
field_name: oid.name.clone(),
});
}
}
count => errors.push(ServiceDefinitionValidationError::MultipleObjectIds {
entity_type,
name: name.to_string(),
id,
count,
}),
}
if is_data_branch_versioned == &Some(true) {
let gid_fields: Vec<&FieldDefinition> = fields
.iter()
.filter(|f| f.field_type == FieldType::GlobalId)
.collect();
if gid_fields.is_empty() {
errors.push(
ServiceDefinitionValidationError::MissingGlobalIdForVersioning {
entity_type,
name: name.to_string(),
id,
},
);
} else {
for gid in &gid_fields {
if gid.nullable == Some(true) || gid.editable == Some(true) {
errors.push(
ServiceDefinitionValidationError::GlobalIdFieldInvalidConfig {
entity_type,
name: name.to_string(),
id,
field_name: gid.name.clone(),
},
);
}
}
}
}
let field_names: std::collections::HashSet<String> =
fields.iter().map(|f| f.name.to_lowercase()).collect();
if let Some(oid_ref) = object_id_field {
if !field_names.contains(&oid_ref.to_lowercase()) {
errors.push(ServiceDefinitionValidationError::FieldRefNotFound {
entity_type,
name: name.to_string(),
id,
ref_type: "object_id",
field_name: oid_ref.clone(),
});
}
}
if let Some(gid_ref) = global_id_field {
if !field_names.contains(&gid_ref.to_lowercase()) {
errors.push(ServiceDefinitionValidationError::FieldRefNotFound {
entity_type,
name: name.to_string(),
id,
ref_type: "global_id",
field_name: gid_ref.clone(),
});
}
}
if let Some(disp_ref) = display_field {
if !field_names.contains(&disp_ref.to_lowercase()) {
errors.push(ServiceDefinitionValidationError::FieldRefNotFound {
entity_type,
name: name.to_string(),
id,
ref_type: "display",
field_name: disp_ref.clone(),
});
}
}
}
impl LayerDefinition {
#[tracing::instrument(skip(self), fields(name = %self.name, id = self.id))]
pub fn validate(&self) -> Result<(), Vec<ServiceDefinitionValidationError>> {
let mut errors = Vec::new();
validate_fields(
&FieldValidationCtx {
entity_type: "Layer",
name: &self.name,
id: self.id,
fields: &self.fields,
object_id_field: &self.object_id_field,
global_id_field: &self.global_id_field,
display_field: &self.display_field,
is_data_branch_versioned: &self.is_data_branch_versioned,
},
&mut errors,
);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl TableDefinition {
#[tracing::instrument(skip(self), fields(name = %self.name, id = self.id))]
pub fn validate(&self) -> Result<(), Vec<ServiceDefinitionValidationError>> {
let mut errors = Vec::new();
validate_fields(
&FieldValidationCtx {
entity_type: "Table",
name: &self.name,
id: self.id,
fields: &self.fields,
object_id_field: &self.object_id_field,
global_id_field: &self.global_id_field,
display_field: &self.display_field,
is_data_branch_versioned: &self.is_data_branch_versioned,
},
&mut errors,
);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl ServiceDefinition {
#[tracing::instrument(
skip(self),
fields(
name = %self.name,
layers = self.layers.len(),
tables = self.tables.len()
)
)]
pub fn validate(&self) -> Result<(), Vec<ServiceDefinitionValidationError>> {
let mut errors = Vec::new();
for layer in &self.layers {
if let Err(layer_errors) = layer.validate() {
errors.extend(layer_errors);
}
}
for table in &self.tables {
if let Err(table_errors) = table.validate() {
errors.extend(table_errors);
}
}
let mut id_registry: std::collections::HashMap<u32, String> =
std::collections::HashMap::new();
for layer in &self.layers {
if let Some(existing) = id_registry.insert(layer.id, layer.name.clone()) {
errors.push(ServiceDefinitionValidationError::DuplicateId {
id: layer.id,
first_name: existing,
second_name: layer.name.clone(),
});
}
}
for table in &self.tables {
if let Some(existing) = id_registry.insert(table.id, table.name.clone()) {
errors.push(ServiceDefinitionValidationError::DuplicateId {
id: table.id,
first_name: existing,
second_name: table.name.clone(),
});
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}