use crate::model::{
ApiKind, ContractMethod, ContractRenderRequest, ContractService, ContractTypedItem,
ReadReturnKind, ValueType,
};
use std::fmt::{Display, Formatter};
pub const STRING_BYTE_CAPACITY: usize = 32;
pub const STRING_REGISTER_WIDTH: u16 = 17;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AreaKind {
Coil,
DiscreteInput,
HoldingRegister,
InputRegister,
}
impl AreaKind {
pub fn c_label(&self) -> &'static str {
match self {
Self::Coil => "线圈",
Self::DiscreteInput => "离散输入",
Self::HoldingRegister => "保持寄存器",
Self::InputRegister => "输入寄存器",
}
}
pub fn function_code_label(&self) -> &'static str {
match self {
Self::Coil => "0x05/0x0F",
Self::DiscreteInput => "0x02",
Self::HoldingRegister => "0x06/0x10",
Self::InputRegister => "0x04",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MethodWireKind {
Bit,
Register,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlannedValue {
Dto {
type_name: String,
items: Vec<ItemPlan>,
},
Scalar {
item: ItemPlan,
},
Parameters {
items: Vec<ItemPlan>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemPlan {
pub name: String,
pub summary: Option<String>,
pub value_type: ValueType,
pub offset: u16,
pub width: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MethodPlan {
pub api_kind: ApiKind,
pub interface_name: String,
pub interface_summary: Option<String>,
pub method_name: String,
pub method_summary: Option<String>,
pub c_method_name: String,
pub rust_method_name: String,
pub area_kind: AreaKind,
pub wire_kind: MethodWireKind,
pub start_address: u16,
pub quantity: u16,
pub planned_value: PlannedValue,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractPlan {
pub services: Vec<ContractService>,
pub methods: Vec<MethodPlan>,
pub total_coils: u16,
pub total_discrete_inputs: u16,
pub total_holding_registers: u16,
pub total_input_registers: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractPlanError {
message: String,
}
impl ContractPlanError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl Display for ContractPlanError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for ContractPlanError {}
pub fn build_plan(request: &ContractRenderRequest) -> Result<ContractPlan, ContractPlanError> {
if request.transports.is_empty() {
return Err(ContractPlanError::new("至少选择一种传输类型"));
}
if request.services.is_empty() {
return Err(ContractPlanError::new("至少定义一个 Api 服务"));
}
let mut coil_cursor = 0_u16;
let mut discrete_input_cursor = 0_u16;
let mut holding_register_cursor = 0_u16;
let mut input_register_cursor = 0_u16;
let mut methods = Vec::new();
for service in &request.services {
if service.methods.is_empty() {
return Err(ContractPlanError::new(format!(
"服务 {} 至少需要一个方法",
service.interface_name
)));
}
for method in &service.methods {
let normalized_interface_name = normalize_type_name(&service.interface_name);
let c_method_name = to_snake_case(&method.method_name);
let rust_method_name = c_method_name.clone();
let (area_kind, wire_kind, planned_value) = plan_method_value(service, method)?;
let quantity = planned_value_width(&planned_value);
let start_address = match area_kind {
AreaKind::Coil => {
let start = coil_cursor;
coil_cursor = checked_next_cursor(coil_cursor, quantity, "线圈")?;
start
}
AreaKind::DiscreteInput => {
let start = discrete_input_cursor;
discrete_input_cursor =
checked_next_cursor(discrete_input_cursor, quantity, "离散输入")?;
start
}
AreaKind::HoldingRegister => {
let start = holding_register_cursor;
holding_register_cursor =
checked_next_cursor(holding_register_cursor, quantity, "保持寄存器")?;
start
}
AreaKind::InputRegister => {
let start = input_register_cursor;
input_register_cursor =
checked_next_cursor(input_register_cursor, quantity, "输入寄存器")?;
start
}
};
methods.push(MethodPlan {
api_kind: service.api_kind.clone(),
interface_name: normalized_interface_name,
interface_summary: trim_option(&service.interface_summary),
method_name: method.method_name.trim().to_string(),
method_summary: trim_option(&method.summary),
c_method_name,
rust_method_name,
area_kind,
wire_kind,
start_address,
quantity,
planned_value,
});
}
}
Ok(ContractPlan {
services: request.services.clone(),
methods,
total_coils: coil_cursor,
total_discrete_inputs: discrete_input_cursor,
total_holding_registers: holding_register_cursor,
total_input_registers: input_register_cursor,
})
}
fn checked_next_cursor(cursor: u16, quantity: u16, label: &str) -> Result<u16, ContractPlanError> {
cursor
.checked_add(quantity)
.ok_or_else(|| ContractPlanError::new(format!("{label}地址规划超出 16 位地址上限")))
}
fn plan_method_value(
service: &ContractService,
method: &ContractMethod,
) -> Result<(AreaKind, MethodWireKind, PlannedValue), ContractPlanError> {
let method_name = method.method_name.trim();
if method_name.is_empty() {
return Err(ContractPlanError::new("方法名不能为空"));
}
match service.api_kind {
ApiKind::Write => {
if method.parameters.is_empty() {
return Err(ContractPlanError::new(format!(
"写方法 {method_name} 至少需要一个参数"
)));
}
let bit_only = method
.parameters
.iter()
.all(|parameter| matches!(parameter.value_type, ValueType::Boolean));
let items = build_item_plans(&method.parameters, bit_only)?;
let planned_value = PlannedValue::Parameters { items };
Ok((
if bit_only {
AreaKind::Coil
} else {
AreaKind::HoldingRegister
},
if bit_only {
MethodWireKind::Bit
} else {
MethodWireKind::Register
},
planned_value,
))
}
ApiKind::StaticRead | ApiKind::RuntimeRead => {
let read_return_kind = method
.read_return_kind
.clone()
.unwrap_or(ReadReturnKind::Dto);
let planned_value = match read_return_kind {
ReadReturnKind::Dto => {
if method.read_fields.is_empty() {
return Err(ContractPlanError::new(format!(
"读方法 {method_name} 选择 DTO 返回时至少需要一个字段"
)));
}
let dto_type_name = normalize_type_name(
method
.read_return_type_name
.as_deref()
.unwrap_or(method_name),
);
let bit_only = method
.read_fields
.iter()
.all(|field| matches!(field.value_type, ValueType::Boolean));
let items = build_item_plans(&method.read_fields, bit_only)?;
(
if matches!(service.api_kind, ApiKind::RuntimeRead) && bit_only {
AreaKind::DiscreteInput
} else {
AreaKind::InputRegister
},
if bit_only {
MethodWireKind::Bit
} else {
MethodWireKind::Register
},
PlannedValue::Dto {
type_name: dto_type_name,
items,
},
)
}
other => {
let scalar_type = read_return_kind_to_value_type(&other);
let bit_only = matches!(scalar_type, ValueType::Boolean)
&& matches!(service.api_kind, ApiKind::RuntimeRead);
let item = ItemPlan {
name: "value".to_string(),
summary: trim_option(&method.summary),
value_type: scalar_type.clone(),
offset: 0,
width: value_type_width(&scalar_type, bit_only),
};
(
if bit_only {
AreaKind::DiscreteInput
} else {
AreaKind::InputRegister
},
if bit_only {
MethodWireKind::Bit
} else {
MethodWireKind::Register
},
PlannedValue::Scalar { item },
)
}
};
Ok(planned_value)
}
}
}
fn build_item_plans(
items: &[ContractTypedItem],
bit_only: bool,
) -> Result<Vec<ItemPlan>, ContractPlanError> {
let mut offset = 0_u16;
let mut planned = Vec::with_capacity(items.len());
for item in items {
let item_name = item.name.trim();
if item_name.is_empty() {
return Err(ContractPlanError::new("字段或参数名不能为空"));
}
let width = value_type_width(&item.value_type, bit_only);
planned.push(ItemPlan {
name: to_snake_case(item_name),
summary: trim_option(&item.summary),
value_type: item.value_type.clone(),
offset,
width,
});
offset = checked_next_cursor(offset, width, "方法块")?;
}
Ok(planned)
}
fn planned_value_width(value: &PlannedValue) -> u16 {
match value {
PlannedValue::Dto { items, .. } | PlannedValue::Parameters { items } => items
.iter()
.map(|item| item.width)
.fold(0_u16, |summary, width| summary.saturating_add(width)),
PlannedValue::Scalar { item } => item.width,
}
}
fn value_type_width(value_type: &ValueType, bit_only: bool) -> u16 {
if bit_only {
return 1;
}
match value_type {
ValueType::Boolean => 1,
ValueType::Int => 2,
ValueType::String | ValueType::Bytes => STRING_REGISTER_WIDTH,
}
}
fn read_return_kind_to_value_type(read_return_kind: &ReadReturnKind) -> ValueType {
match read_return_kind {
ReadReturnKind::Boolean => ValueType::Boolean,
ReadReturnKind::Int => ValueType::Int,
ReadReturnKind::String => ValueType::String,
ReadReturnKind::Bytes => ValueType::Bytes,
ReadReturnKind::Dto => ValueType::Bytes,
}
}
pub fn trim_option(value: &Option<String>) -> Option<String> {
value.as_ref().and_then(|item| {
let normalized = item.trim();
if normalized.is_empty() {
None
} else {
Some(normalized.to_string())
}
})
}
pub fn normalize_type_name(raw: &str) -> String {
let words = split_words(raw);
if words.is_empty() {
return "GeneratedType".to_string();
}
words
.into_iter()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(first) => {
let mut normalized = String::new();
normalized.extend(first.to_uppercase());
normalized.push_str(chars.as_str());
normalized
}
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("")
}
pub fn to_upper_snake_case(raw: &str) -> String {
let words = split_words(raw);
if words.is_empty() {
return "GENERATED_ITEM".to_string();
}
words
.into_iter()
.map(|word| word.to_uppercase())
.collect::<Vec<_>>()
.join("_")
}
pub fn to_snake_case(raw: &str) -> String {
let words = split_words(raw);
if words.is_empty() {
return "generated_item".to_string();
}
let normalized = words
.into_iter()
.map(|word| word.to_lowercase())
.collect::<Vec<_>>()
.join("_");
if normalized
.chars()
.next()
.map(|character| character.is_ascii_digit())
.unwrap_or(false)
{
format!("generated_{normalized}")
} else {
normalized
}
}
fn split_words(raw: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let mut previous_is_lower_or_digit = false;
for character in raw.chars() {
if !character.is_ascii_alphanumeric() {
if !current.is_empty() {
words.push(current.clone());
current.clear();
}
previous_is_lower_or_digit = false;
continue;
}
let is_boundary = character.is_ascii_uppercase() && previous_is_lower_or_digit;
if is_boundary && !current.is_empty() {
words.push(current.clone());
current.clear();
}
current.push(character);
previous_is_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
}
if !current.is_empty() {
words.push(current);
}
words
}