pallet_schemas/
types.rs

1//! Types for the Schema Pallet
2use crate::{Config, Error};
3use common_primitives::schema::{
4	IntentGroupId, IntentId, IntentSetting, IntentSettings, MappedEntityIdentifier, ModelType,
5	NameLookupResponse, PayloadLocation, SchemaId, SchemaStatus, SchemaVersion,
6	SchemaVersionResponse,
7};
8use core::fmt::Debug;
9use frame_support::{ensure, pallet_prelude::ConstU32, traits::StorageVersion, BoundedVec};
10use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
11use scale_info::TypeInfo;
12use sp_runtime::DispatchError;
13extern crate alloc;
14use alloc::{string::String, vec, vec::Vec};
15use frame_support::traits::Len;
16
17/// Current storage version of the schemas pallet.
18pub const SCHEMA_STORAGE_VERSION: StorageVersion = StorageVersion::new(5);
19
20/// The maximum size of a fully qualified name, including all parts and separators
21pub const SCHEMA_NAME_BYTES_MAX: u32 = 32; // Hard limit of 32 bytes
22/// A fully qualified name has the following structure PROTOCOL.DESCRIPTOR
23pub type SchemaNamePayload = BoundedVec<u8, ConstU32<SCHEMA_NAME_BYTES_MAX>>;
24/// schema namespace type
25pub type SchemaProtocolName = BoundedVec<u8, ConstU32<PROTOCOL_NAME_MAX>>;
26/// schema descriptor type
27pub type SchemaDescriptor = BoundedVec<u8, ConstU32<DESCRIPTOR_MAX>>;
28/// The minimum size of a namespace in schema
29pub const PROTOCOL_NAME_MIN: u32 = 3;
30/// The maximum size of a namespace in schema
31pub const PROTOCOL_NAME_MAX: u32 = SCHEMA_NAME_BYTES_MAX - (DESCRIPTOR_MIN + 1);
32/// The minimum size of a schema descriptor
33pub const DESCRIPTOR_MIN: u32 = 1;
34/// The maximum size of a schema descriptor
35pub const DESCRIPTOR_MAX: u32 = SCHEMA_NAME_BYTES_MAX - (PROTOCOL_NAME_MIN + 1);
36/// Maximum number of intents allowed per delegation group
37pub const MAX_INTENTS_PER_DELEGATION_GROUP: u32 = 10;
38/// separator character
39pub const SEPARATOR_CHAR: char = '.';
40/// maximum number of versions for a certain schema name
41/// -1 is to avoid overflow when converting the (index + 1) to `SchemaVersion` in `SchemaVersionId`
42pub const MAX_NUMBER_OF_VERSIONS: u32 = SchemaVersion::MAX as u32 - 1;
43
44/// Type alias for SchemaPayload
45#[allow(type_alias_bounds)]
46pub type SchemaPayload<T: Config> =
47	BoundedVec<u8, <T as Config>::SchemaModelMaxBytesBoundedVecLimit>;
48
49#[derive(Debug, serde::Serialize, serde::Deserialize)]
50/// Genesis Schemas need a way to load up and this is it!
51pub struct GenesisSchema {
52	/// The SchemaId
53	pub schema_id: SchemaId,
54	/// The IntentId of the Intent that this Schema implements
55	pub intent_id: IntentId,
56	/// The type of model (AvroBinary, Parquet, etc.)
57	pub model_type: ModelType,
58	/// The Payload Model
59	pub model: String,
60	/// The status of the schema
61	pub status: SchemaStatus,
62}
63
64#[derive(Debug, serde::Serialize, serde::Deserialize)]
65/// Genesis Intents need a way to load up and this is it!
66pub struct GenesisIntent {
67	/// The IntentId
68	pub intent_id: IntentId,
69	/// The payload location
70	pub payload_location: PayloadLocation,
71	/// Schema Full Name: {Protocol}.{Descriptor}
72	pub name: String,
73	/// Settings
74	pub settings: Vec<IntentSetting>,
75}
76
77#[derive(Debug, serde::Serialize, serde::Deserialize)]
78/// Genesis IntentGroups need a way to load up and this is it!
79pub struct GenesisIntentGroup {
80	/// The IntentGroupId
81	pub intent_group_id: IntentGroupId,
82	/// The name: {Protocol}.{Descriptor}
83	pub name: String,
84	/// The list of Intents in the group
85	pub intent_ids: Vec<IntentId>,
86}
87
88#[derive(Debug, serde::Serialize, serde::Deserialize)]
89/// Overall Genesis config loading structure for the entire pallet
90pub struct GenesisSchemasPalletConfig {
91	/// Maximum schema identifier at genesis
92	pub schema_identifier_max: Option<SchemaId>,
93	/// Maximum Intent identifier at genesis
94	pub intent_identifier_max: Option<IntentId>,
95	/// Maximum IntentGroup identifier at genesis
96	pub intent_group_identifier_max: Option<IntentGroupId>,
97	/// Maximum schema size in bytes at genesis
98	pub max_schema_model_size: Option<u32>,
99	/// The list of Schemas
100	pub schemas: Option<Vec<GenesisSchema>>,
101	/// The list of Intents
102	pub intents: Option<Vec<GenesisIntent>>,
103	/// The list of IntentGroups
104	pub intent_groups: Option<Vec<GenesisIntentGroup>>,
105}
106
107#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq, MaxEncodedLen)]
108#[scale_info(skip_type_params(T))]
109/// A structure defining an IntentGroup
110pub struct IntentGroup<T: Config> {
111	/// The list of Intents in the DelegationGroup
112	pub intent_ids: BoundedVec<IntentId, T::MaxIntentsPerIntentGroup>,
113}
114
115#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq, MaxEncodedLen)]
116/// A structure defining Intent information
117pub struct IntentInfo {
118	/// The payload location
119	pub payload_location: PayloadLocation,
120	/// additional control settings for the schema
121	pub settings: IntentSettings,
122}
123
124#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq, MaxEncodedLen)]
125/// A structure defining Schema information (excluding the payload)
126pub struct SchemaInfo {
127	/// The IntentId of the Intent that this Schema implements
128	pub intent_id: IntentId,
129	/// The type of model (AvroBinary, Parquet, etc.)
130	pub model_type: ModelType,
131	/// The payload location (inherited from the Intent)
132	pub payload_location: PayloadLocation,
133	/// additional control settings (inherited from the Intent)
134	pub settings: IntentSettings,
135	/// The status of the Schema
136	pub status: SchemaStatus,
137}
138
139#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq, MaxEncodedLen)]
140/// A structure defining name of a registered entity in this pallet.
141/// Names consist of a `protocol` (namespace) and a `descriptor`. Currently, names may be registered
142/// for `Intent`s and `IntentGroup`s. See [name-resolution](https://github.com/frequency-chain/frequency/blob/main/designdocs/schemas_protocols_intents.md#9-name-resolution) for more info.
143/// (Note, the type name `SchemaName` is a relic from the previous implementation of this pallet;
144/// Schemas themselves can no longer be directly associated with names.)
145pub struct SchemaName {
146	/// Protocol portion of the name
147	pub namespace: SchemaProtocolName,
148	/// Descriptor portion of the name
149	pub descriptor: SchemaDescriptor,
150}
151
152/// A structure defining a fully qualified name of an entity
153// TODO: type alias for now, until we finish converting the Schemas and deprecate the old methods
154pub type FullyQualifiedName = SchemaName;
155
156#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq, MaxEncodedLen, Default)]
157/// A structure defining a vector of [`SchemaId`](common_primitives::schema::SchemaId)s representing monotonically increasing versions
158/// of a `Schema`.
159// TODO: Remove once Runtime APIs returning this structure have been removed
160pub struct SchemaVersionId {
161	/// the index of each item + 1 is considered as their version.
162	/// Ex: the schemaId located in `ids[2]` is for version number 3
163	pub ids: BoundedVec<SchemaId, ConstU32<MAX_NUMBER_OF_VERSIONS>>,
164}
165
166impl SchemaName {
167	/// parses and verifies the request and returns the SchemaName type if successful
168	///
169	/// Note: passing `require_descriptor: false` is intended for RPC methods that search the
170	/// name registry by protocol. Operations that validate name creation should always pass
171	/// `require_descriptor: true`.
172	///
173	/// # Errors
174	/// * [`Error::InvalidSchemaNameEncoding`] - The name has an invalid encoding.
175	/// * [`Error::InvalidSchemaNameCharacters`] - The name contains invalid characters.
176	/// * [`Error::InvalidSchemaNameStructure`] - The name has an invalid structure (i.e., not `protocol.descriptor`).
177	/// * [`Error::InvalidSchemaNameLength`] - The name exceeds the allowed overall name length.
178	/// * [`Error::InvalidSchemaNamespaceLength`] - The protocol portion of the name exceeds the max allowed length.
179	/// * [`Error::InvalidSchemaDescriptorLength`] - The descriptor portion of the name exceeds the max allowed length.
180	pub fn try_parse<T: Config>(
181		payload: SchemaNamePayload,
182		require_descriptor: bool,
183	) -> Result<SchemaName, DispatchError> {
184		// check if all ascii
185		let mut str = String::from_utf8(payload.into_inner())
186			.map_err(|_| Error::<T>::InvalidSchemaNameEncoding)?;
187		ensure!(str.is_ascii(), Error::<T>::InvalidSchemaNameEncoding);
188
189		// to canonical form
190		str = String::from(str.to_lowercase().trim());
191
192		// only allow the following:
193		// - alphanumeric characters
194		// - '-' (hyphen)
195		// - SEPARATOR_CHAR (period)
196		ensure!(
197			str.chars()
198				.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == SEPARATOR_CHAR),
199			Error::<T>::InvalidSchemaNameCharacters
200		);
201
202		// split to namespace and descriptor
203		let chunks: Vec<_> = str.split(SEPARATOR_CHAR).collect();
204		ensure!(
205			chunks.len() == 2 || (chunks.len() == 1 && !require_descriptor),
206			Error::<T>::InvalidSchemaNameStructure
207		);
208
209		// check namespace
210		let namespace = BoundedVec::try_from(chunks[0].as_bytes().to_vec())
211			.map_err(|_| Error::<T>::InvalidSchemaNamespaceLength)?;
212		ensure!(
213			PROTOCOL_NAME_MIN <= namespace.len() as u32,
214			Error::<T>::InvalidSchemaNamespaceLength
215		);
216
217		// check that it starts with an alphabetic character.
218		// this ensures:
219		// - does not start with '-'
220		// - does not start with a number
221		// - does not start with a SEPARATOR_CHAR
222		// (This also handles the case where the value is all numeric, because it would also
223		// start with a decimal digit.)
224		ensure!(namespace[0].is_ascii_alphabetic(), Error::<T>::InvalidSchemaNameStructure);
225		// should not end with -
226		ensure!(!namespace.ends_with(b"-"), Error::<T>::InvalidSchemaNameStructure);
227
228		// check descriptor
229		let descriptor = match chunks.len() == 2 {
230			true => {
231				let descriptor = BoundedVec::try_from(chunks[1].as_bytes().to_vec())
232					.map_err(|_| Error::<T>::InvalidSchemaDescriptorLength)?;
233				ensure!(
234					DESCRIPTOR_MIN <= descriptor.len() as u32,
235					Error::<T>::InvalidSchemaDescriptorLength
236				);
237				// check that it starts with an alphabetic character.
238				// this ensures:
239				// - does not start with '-'
240				// - does not start with a number
241				// - does not start with a SEPARATOR_CHAR
242				// (This also handles the case where the value is all numeric, because it would also
243				// start with a decimal digit.)
244				ensure!(
245					descriptor[0].is_ascii_alphabetic(),
246					Error::<T>::InvalidSchemaNameStructure
247				);
248				// should end with -
249				ensure!(!descriptor.ends_with(b"-"), Error::<T>::InvalidSchemaNameStructure);
250				descriptor
251			},
252			false => BoundedVec::default(),
253		};
254
255		Ok(SchemaName { namespace, descriptor })
256	}
257
258	/// get the combined name namespace.descriptor
259	pub fn get_combined_name(&self) -> Vec<u8> {
260		[
261			self.namespace.clone().into_inner(),
262			vec![SEPARATOR_CHAR as u8],
263			self.descriptor.clone().into_inner(),
264		]
265		.concat()
266	}
267
268	/// creates a new SchemaName using provided descriptor
269	pub fn new_with_descriptor(&self, descriptor: SchemaDescriptor) -> Self {
270		Self { namespace: self.namespace.clone(), descriptor }
271	}
272
273	/// returns true if the descriptor exists
274	pub fn descriptor_exists(&self) -> bool {
275		self.descriptor.len() > 0
276	}
277}
278
279impl SchemaVersionId {
280	/// adds a new schema id and returns the version for that schema_id
281	pub fn add<T: Config>(&mut self, schema_id: SchemaId) -> Result<SchemaVersion, DispatchError> {
282		let is_new = !self.ids.iter().any(|id| id == &schema_id);
283		ensure!(is_new, Error::<T>::SchemaIdAlreadyExists);
284		self.ids
285			.try_push(schema_id)
286			.map_err(|_| Error::<T>::ExceedsMaxNumberOfVersions)?;
287		let version = self.ids.len() as SchemaVersion;
288		Ok(version)
289	}
290
291	/// convert into a response vector
292	pub fn convert_to_response(&self, schema_name: &SchemaName) -> Vec<SchemaVersionResponse> {
293		self.ids
294			.iter()
295			.enumerate()
296			.map(|(index, schema_id)| SchemaVersionResponse {
297				schema_name: schema_name.get_combined_name(),
298				schema_id: *schema_id,
299				schema_version: (index + 1) as SchemaVersion,
300			})
301			.collect()
302	}
303}
304
305/// trait to create a response from a name and entity identifier
306pub trait ConvertToResponse<I, R> {
307	/// method to convert to response
308	fn convert_to_response(&self, name: &I) -> R;
309}
310
311impl ConvertToResponse<Vec<u8>, Vec<SchemaVersionResponse>> for Vec<SchemaId> {
312	fn convert_to_response(&self, schema_name: &Vec<u8>) -> Vec<SchemaVersionResponse> {
313		self.iter()
314			.enumerate()
315			.map(|(index, id)| SchemaVersionResponse {
316				schema_name: schema_name.clone(),
317				schema_id: *id,
318				schema_version: (index + 1) as SchemaVersion,
319			})
320			.collect()
321	}
322}
323
324impl ConvertToResponse<SchemaName, NameLookupResponse> for MappedEntityIdentifier {
325	fn convert_to_response(&self, name: &SchemaName) -> NameLookupResponse {
326		NameLookupResponse { name: name.get_combined_name(), entity_id: *self }
327	}
328}