This is a work in progress!
Parse the metadata XML describing an SAP OData service and generate basic Rust entities for each EDM type:
ComplexType
EntityType
FunctionImport
Limitations
Currently when generating a Rust
struct
, only theName
andType
properties are extracted from the XML<EntityType>
declaration.TODO
Consider how the other XML attribute values and SAP annotations could be made available within the generated Ruststruct
.
In the Cargo.toml
of your application, define an entry in [build-dependencies]
that points to the parse-sap-odata
crate:
toml
[build-dependencies]
parse-sap-odata = "^1.1.0"
Your app will require at least these dependencies:
toml
[dependencies]
chrono = { version = "^0.4", features = ["serde"] }
rust_decimal = "^1.30"
serde = { version = "^1.0", features = ["derive"] }
uuid = { version = "^1.4", features = ["serde"] }
In your app's build script (build.rs
), run the generator for your desired OData service:
```rust use parsesapodata::parser::gen_src;
fn main() {
gensrc(
"gwsamplebasic", // metadatafilename. The ".xml" extension is assumed
"GWSAMPLE_BASIC" // Value of the Namespace attribute on the
More information about Rust build scripts is available on the documentation site.
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 above example from SAP's Dev Center server, the OData metadata XML for service GWSAMPLE_BASIC
must be located in file ./odata/gwsample_basic.xml
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/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 #[derive(Clone, Debug, Default)]
Since cargo
runs the build script before compiling your application code, the source code of your application can reference the generated structs
like this:
```rust // Include the generated code include!(concat!(env!("OUTDIR"), "/gwsamplebasic.rs"));
// Use the BusinessPartner struct for example fn main() { let bp: BusinessPartner = Default::default(); println!("{:#?}", bp); } ```
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.
```xml
```
The Rust struct
name is generated by trimming the namespace qualifier and (if present) the CT_
prefix
xml
<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:
```rust
pub struct Address {
pub addresstype: Option
The metadata for the GWSAMPLE_BASIC
OData service contains the following complex type:
xml
<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
.
On the basis that a single OData service exposes a static list of entity sets, and 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
:
```rust
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
:
rust
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 */ }
}
value
functionThis function returns the name of the entity set variant as a static string slice:
rust
GwsampleBasicEntities::ProductSet::value(); // -> "ProductSet"
iterator
functionFor 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, it is now both safe and meaningful to implement an iterator
function.
as_list
functionBy 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.
serde
directives for the generated structs
The prefered test tool is nextest
which can be installed from here.
If you have not installed nextest
, cargo test
can be used instead.
```bash $ cargo nextest run Finished test [unoptimized + debuginfo] target(s) in 0.15s Starting 9 tests across 1 binary PASS [ 0.032s] parse-sap-odata edmx::dataservices::schema::association::referentialconstraint::unittests::shouldparsereferntialconstraint PASS [ 0.033s] parse-sap-odata edmx::dataservices::schema::association::unittests::shouldparseassociationset PASS [ 0.035s] parse-sap-odata edmx::dataservices::schema::entitycontainer::unittests::shouldparseassociationset PASS [ 0.037s] parse-sap-odata edmx::dataservices::schema::entitycontainer::unittests::shouldparseentitycontainer PASS [ 0.036s] parse-sap-odata edmx::dataservices::schema::entitycontainer::unittests::shouldparseentityset PASS [ 0.034s] parse-sap-odata property::unittests::shouldconvertunsafepropertyname PASS [ 0.033s] parse-sap-odata property::unittests::shouldnotconvertsafepropertyname PASS [ 0.038s] parse-sap-odata edmx::dataservices::schema::entitytype::unittests::shouldparseentitytypebusinesspartner
Summary [ 0.046s] 9 tests run: 9 passed, 0 skipped
```
The build_test_app
subdirectory contains a test Rust app that consumes the SAP sample OData service GWSAMPLE_BASIC
.
You must already have a userid and password for the SAP Dev Center server sapes5.sapdevcenter.com
cd parse_sap_odata/build_test_app
Create a .env
file containing your SAP DevCenter userid and password in the following format
SAP_USER=<your userid>
SAP_PASSWORD=<your password>
build_test_app
build_test_app
run cargo run