Parse the Metadata from an SAP OData Service

This is a work in progress!

Parse the metadata XML describing an SAP OData service and generate basic Rust entities for each EDM type:

TODO

Currently when generating a Rust struct, only the Name and Type properties are extracted from the XML <EntityType> declaration.
Consider how the other XML attribute values and SAP annotations could be made available within the Rust struct.


Usage

Declare Build Dependency

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 also (almost certainly) require these dependencies

toml [dependencies] rust_decimal = "^1.30" uuid = "^1.4" chrono = "^0.4"

Create a Build Script

In your app's build script (build.rs), run the generator for your desired OData service:

```rust use parsesapodata::utils::parseodata::gensrc;

fn main() { gensrc( metadatafilename: "gwsamplebasic", namespace: "GWSAMPLE_BASIC" ); } ```

See the Rust documentation page for build scripts for more information.

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 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

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/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 structs implement #[derive(Clone, Debug, Default)]


Referencing Generated Output

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); } ```


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.

```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

[derive(Clone, Debug, Default)]

pub struct Address { pub addresstype: Option, pub building: Option, pub city: Option, pub country: Option, pub postalcode: Option, pub street: Option, } ```


OData "Simple" Complex Types

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.


Entity Sets Enum

Under the <Schema> element in the service document of your selected OData service, there is an <EntityContainer> element. All entity sets available through this OData service have a corresponding <EntitySet Name="<some_name>"> tag.

These values are then turned into an enum.

Using GWSAMPLE_BASIC as the example, the following enum is created using the naming convention <service_name>Entities:

```rust

[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:

Entity Set Enum value function

This function returns the entity set name as a static string slice:

rust GwsampleBasicEntities::ProductSet::value(); // "ProductSet"

Entity Set Enum iterator function

Unlike the Option enum whose members are Some(<T>) and None (where <T> could be absolutely any type), all the members here are entity sets; so under these circumstances it is convenient (and safe) to be able to iterate over the enum

This is the under-lying helper function that allows the drop-down list of entity sets to be generated

Entity Set Enum as_list function

Returns the names of the entity sets as a vector of string slices.


TODOs

  1. Consider fetching the metadata at build time — but this raises the question of whether allowing a build script to look outside its sandbox is an anti-pattern...
  2. Support Function Imports

Testing this Crate Locally

Prerequisites

You must already have a userid and password for the SAP Dev Center server sapes5.sapdevcenter.com

  1. Clone this repo
  2. Change into the repo's build_test_crate subdirectory.
  3. Create a .env file containing your userid and password in the following format

    SAP_USER=<your userid> SAP_PASSWORD=<your password>

  4. Run cargo run
  5. Open your browser and go to http://localhost:8080
  6. Select the name of the entity set whose data you want to see Start screen
  7. You will then see the first 100 entries from the selected entity set in JSON format.