22 stable releases
1.2.3 | Oct 5, 2023 |
---|---|
1.2.2 | Sep 28, 2023 |
1.1.8 | Aug 31, 2023 |
1.0.0 | Jun 1, 2023 |
#337 in Parser implementations
43 downloads per month
115KB
2K
SLoC
Parse the Metadata from an SAP OData V2 Service
This is a work in progress!
Parse the metadata XML describing an SAP OData V2 service and generate Rust modules: one for the Service Document and one for the metadata document.
-
<ComplexType>
and<EntityType>
elements are mapped to Ruststructs
-
<FunctionImport>
functionality will be supported in time, but is not currently available - Transform
Edm.DateTime
intochrono::NaiveDateTime
using a custom deserializer -
Edm.Decimal
fields are handled using theDecimal
deserializer in craterust_decimal
; however, this offers only partial support - OData metadata parsing functionality is only available when the
parser
crate feature is used - Generate a metadata module
- Populate the metadata module - I'm working on it...
Usage
You want to write a Rust application to consume the data exposed through an SAP OData V2 service.
The functionality in this crate is invoked by the build script in your application that generates a module of the same name as the OData service you wish to consume.
Together with the code in crate parse-sap-atom-feed
the coding in the generated module can then consume entity set data from the OData service.
Declare Build Dependency
In the Cargo.toml
of your application, define an entry in [build-dependencies]
that points to the parse-sap-odata
crate:
[build-dependencies]
parse-sap-odata = { version = "1.2", features = ["parser"]}
Your app will require at least these dependencies:
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
parse-sap-atom-feed = "0.2"
rust_decimal = { version = "1.30", features = ["serde-with-str"]}
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.4", features = ["serde"] }
XML Input Files
All metadata XML for the OData services your app consumes must be located in the ./odata
directory immediately under your app's top level directory.
Using the demo service GWSAMPLE_BASIC
available from SAP's Dev Center server, display the metadata XML for this service, then save that in file ./odata/gwsample_basic.xml
.
Create a Build Script
In your app's build script (build.rs
), run the generator for your desired OData service:
use parse_sap_odata::parser::gen_src;
fn main() {
gen_src(
"gwsample_basic", // metadata_file_name. The ".xml" extension is assumed
"GWSAMPLE_BASIC" // Value of the Namespace attribute on the <Schema> tag
);
}
More information about Rust build scripts is available on the documentation site.
Generated Output
If cargo
detects a build.rs
file in your project/crate, then it automatically populates the environment variable OUT_DIR
and runs build.rs
before compiling your application.
The OUT_DIR
variable then points to the directory into which all build script output is written.
The default directory name is target/[debug|release]/build/<your_package_name>/out
, and this is where you can find the generated struct
declarations for the OData service.
You can specify your own value for OUT_DIR
either by passing a value to cargo
's --out_dir
flag, or by defining your own location in a config.toml
file in the ./.cargo
directory.
See Cargo Configuration for more details.
All generated struct
s implement at least the following traits #[derive(Clone, Debug, Default)]
Referencing Generated Output
In the source code of your application, use the include!()
macro to pull in the generated source code, then bring the generated module into scope with a use
command:
// Include the generated code
include!(concat!(env!("OUT_DIR"), "/gwsample_basic.rs"));
use std::str::{self, FromStr};
use gwsample_basic::*;
// Use the BusinessPartner struct for example
fn main() {
let bp: BusinessPartner = Default::default();
println!("{:#?}", bp);
}
OData Complex Types
In the event an Entity Type definition uses a complex type, then the complex type is first created as a Rust struct
.
The field in Rust struct
that has this complex type is then defined using this struct
.
An example of this is the Address
property.
<EntityType Name="BusinessPartner" sap:content-version="1">
<Key>
<PropertyRef Name="BusinessPartnerID"/>
</Key>
<Property Name="Address" Type="GWSAMPLE_BASIC.CT_Address" Nullable="false"/>
<!-- SNIP -->
</EntityType>
The Rust struct
name is generated by trimming the namespace qualifier and (if present) the CT_
prefix
<ComplexType Name="CT_Address">
<Property Name="City" Type="Edm.String" MaxLength="40" sap:label="City" sap:semantics="city"/>
<Property Name="PostalCode" Type="Edm.String" MaxLength="10" sap:label="Postal Code" sap:semantics="zip"/>
<Property Name="Street" Type="Edm.String" MaxLength="60" sap:label="Street" sap:semantics="street"/>
<Property Name="Building" Type="Edm.String" MaxLength="10" sap:label="Building"/>
<Property Name="Country" Type="Edm.String" MaxLength="3" sap:label="Country" sap:semantics="country"/>
<Property Name="AddressType" Type="Edm.String" MaxLength="2" sap:label="Address Type"/>
</ComplexType>
So the above XML definition becomes:
#[derive(Clone, Debug, Default)]
pub struct Address {
pub address_type: Option<String>,
pub building: Option<String>,
pub city: Option<String>,
pub country: Option<String>,
pub postal_code: Option<String>,
pub street: Option<String>,
}
OData "Simple" Complex Types
The metadata for the GWSAMPLE_BASIC
OData service contains the following complex type:
<ComplexType Name="CT_String">
<Property Name="String" Type="Edm.String" Nullable="false" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false"/>
</ComplexType>
Allowing for the current situation in which additional attribute values and SAP Annotations are not preserved, this particular type turns out not to be complex at all — its just a String
.
In such cases, fields declared to be of these "simple" complex types (such as CT_String
), are collapsed down to the Rust native type of the single inner property — which in this example is simply a String
.
Entity Sets Enum
On the basis that a single OData service exposes a static list of entity sets, and that within the scope of any single request, you will only ever be interacting with a single entity set, it makes sense to treat each entity set name as an enum
variant.
Under the <Schema>
element in the OData service document, there is an <EntityContainer>
element.
All entity sets available through this OData service are identified here with their own <EntitySet Name="<some_name>">
tag.
The following naming convention is used: <odata_service_name>Entities
.
For example, the entity sets belonging to the OData service GWSAMPLE_BASIC
become the following enum
:
#[derive(Copy, Clone, Debug)]
pub enum GwsampleBasicEntities {
BusinessPartnerSet,
ProductSet,
SalesOrderSet,
SalesOrderLineItemSet,
ContactSet,
VhSexSet,
VhCountrySet,
VhAddressTypeSet,
VhCategorySet,
VhCurrencySet,
VhUnitQuantitySet,
VhUnitWeightSet,
VhUnitLengthSet,
VhProductTypeCodeSet,
VhBpRoleSet,
VhLanguageSet,
}
Three convenience functions are then implemented for enum GwsampleBasicEntities
:
impl GwsampleBasicEntities {
pub const fn value(&self) -> &'static str { /* SNIP */ }
pub fn iterator() -> impl Iterator<Item = GwsampleBasicEntities> { /* SNIP */ }
pub fn as_list() -> Vec<&'static str> { /* SNIP */ }
}
Entity Set Enum value
function
This function returns the name of the entity set variant as a static string slice:
GwsampleBasicEntities::ProductSet::value(); // -> "ProductSet"
Entity Set Enum iterator
function
For standard Rust enums
such as Option
and Result
, it makes little sense to attempt to loop over their variants simply because these enum
s exist specifically to gather together diverse types into a single object.
E.G. The Option
enum
exists to provide a type-safe mechanism for handling the possibility that a variable might not contain a value.
However, an OData service guarantees that the entity set names form an immutable, type-safe list.
Therefore, on the basis of this guarantee, the entity set names are placed into an enum
that implements an iterator
function over its variants.
Entity Set Enum as_list
function
By making use of the above iterator
and value
functions, the as_list
function returns the names of the entity sets as a vector of string slices.
Limitations and Issues
-
Currently when generating a Rust
struct
, only theName
andType
properties are extracted from the XML<EntityType>
declaration. Consider how the other XML attribute values and SAP annotations could be made available within the generated Ruststruct
. -
The
<FunctionImport>
,<Association>
and<AssociationSet>
metadata tags are parsed, but their contents is not currently acted upon. -
All SAP OData V2 Annotations are processed by serde; however, no action is yet taken based on the annotation values.
-
When calling some of the entity sets in the demo OData service
GWSAMPLE_BASIC
, certain XML properties are returned whose values are not valid XML. Consequently, whenquick_xml
attempts to parse such values, it simply throws its toys out the pram and doesn't want to play anymore.If you encounter such errors, the raw XML string must first be sanitised before attempting to parse it.
This functionality is described in the README of
parse-sap-odata-demo
. -
The
rust_decimal::serde::str
deserializer can offer only partial support for fields ofEdm.Decimal
because it knows nothing about the attributesPrecision
andScale
:<Property Name="Price" Type="Edm.Decimal" Precision="16" Scale="3" sap:unicode="false" sap:unit="CurrencyCode" sap:label="Unit Price"/>
Consequently, these attribute values are not considered when extracting a decimal value.
TODOs
- Improve support for fields of type
Edm.Decimal
. - Populate the empty OData metadata module.
- Support Function Imports.
Dependencies
~6–18MB
~225K SLoC