Skip to content

Instantly share code, notes, and snippets.

@cgrothaus
Last active March 18, 2022 15:57
Show Gist options
  • Save cgrothaus/db2066578d88f889d319b21818d3de07 to your computer and use it in GitHub Desktop.
Save cgrothaus/db2066578d88f889d319b21818d3de07 to your computer and use it in GitHub Desktop.
SAP OData metadata documentation generator
# Local environment variables like secrets
.env
# Bundler directory
.bundle
# Cached rubocop files
.rubocop-http*
# Intermediate output file
odata-classes.mmd
inherit_from:
- https://raw.githubusercontent.com/zweitag/code-style/main/rubocop/.rubocop.yml

Generate SAP OData documentation

  1. Query the SAP OData service $metadata endpoint, and save the XML response as odata-metadata.xml
  2. maybe autoformat (with VS Code) the file contents for better readability and a clean git diff
  3. run bundle install
  4. run bundle exec ruby generate_odata_documentation.rb

This generates a odata-classes.svg class diagram and a odata-metadata.md Markdown document, which includes the diagram.

ruby File.read(File.join(__dir__, ".ruby-version")).strip
source "https://rubygems.org"
gem "dotenv"
gem "liquid"
gem "pry"
gem "rexml"
GEM
remote: https://rubygems.org/
specs:
coderay (1.1.3)
dotenv (2.7.6)
liquid (5.2.0)
method_source (1.0.0)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
rexml (3.2.5)
PLATFORMS
arm64-darwin-21
DEPENDENCIES
dotenv
liquid
pry
rexml
RUBY VERSION
ruby 3.1.1p18
BUNDLED WITH
2.3.9
require "pry"
require "liquid"
require "net/http"
require "rexml/document"
require "uri"
@metadata_filename = "odata-metadata.xml"
@markdown_template_content = File.read("metadata.template.md")
@markdown_output_filename = "odata-metadata.md"
@classdiagram_template_content = File.read("metadata.template.mmd")
@classdiagram_mermaid_temp_output_filename = "odata-classes.mmd"
@classdiagram_svg_output_filename = "odata-classes.svg"
def main
root = parse_xml_document(@metadata_filename)
xml_metadata_schema = get_xml_metadata_schema(root)
template_args = build_template_args(xml_metadata_schema)
render_classdiagram_mermaid_template(template_args)
render_markdown_template(template_args.merge("classdiagram_svg_filename" => @classdiagram_svg_output_filename))
end
def parse_xml_document(xml_filename)
xmldoc = REXML::Document.new(File.new(xml_filename))
xmldoc.root
end
def get_xml_metadata_schema(root)
xml_metadata_schemas = root.get_elements("//Schema")
raise "We expect exactly one 'Schema': #{xml_metadata_schemas.inspect}" unless xml_metadata_schemas.size == 1
xml_metadata_schemas.first
end
def build_template_args(xml_metadata_schema)
odata_namespace_name = attribute_value(xml_metadata_schema, "Namespace")
xml_entities = xml_metadata_schema.get_elements("EntityType")
xml_associations = xml_metadata_schema.get_elements("Association")
associations = xml_associations.map { |xa| parse_association(xa, odata_namespace_name) }
entities = xml_entities.map { |xe| parse_entity(xe, associations) }
{
"namespace" => odata_namespace_name,
"entities" => entities,
"associations" => associations
}
end
def parse_association(xml_association, odata_namespace_name)
xml_association_ends = xml_association.get_elements("End")
raise "We expect exactly two association 'End' elements: #{xml_association.inspect}" unless xml_association_ends.size == 2
from = attribute_value(xml_association_ends[0], "Type").delete_prefix("#{odata_namespace_name}.")
from_multiplicity = attribute_value(xml_association_ends[0], "Multiplicity")
to = attribute_value(xml_association_ends[1], "Type").delete_prefix("#{odata_namespace_name}.")
to_multiplicity = attribute_value(xml_association_ends[1], "Multiplicity")
{from:, from_multiplicity:, to:, to_multiplicity:}.transform_keys(&:to_s)
end
def parse_entity(xml_entity, associations)
xml_properties = xml_entity.get_elements("Property")
name = attribute_value(xml_entity, "Name")
key_properties = xml_entity.get_elements("Key/PropertyRef").map { |pr| attribute_value(pr, "Name") }
properties = xml_properties.map { |xp| parse_property(xp, key_properties) }
associations_with = associations
.map { |association| association.values_at("from", "to") }
.filter { |association_entities| association_entities.include?(name) }
.flat_map { |association_entities| association_entities - [name] }
{name:, properties:, associations_with:}.transform_keys(&:to_s)
end
def parse_property(xml_property, key_properties)
prop_names = %w[Name Type Nullable label MaxLength Precision Scale filterable]
property = prop_names.to_h { |prop| [prop, attribute_value(xml_property, prop)] }
property["Type"] = property["Type"]&.delete_prefix("Edm.")
property["IsKey"] = key_properties.include?(property["Name"])
property["Nullable"] = property["Nullable"] != "false"
property["filterable"] = property["filterable"] != "false"
property
end
def attribute_value(xml_element, attribute_name)
xml_element.attribute(attribute_name)&.value
end
def render_markdown_template(template_args)
template = Liquid::Template.parse(@markdown_template_content, error_mode: :strict)
rendered_output = template.render(template_args)
File.write(@markdown_output_filename, rendered_output)
end
def render_classdiagram_mermaid_template(template_args)
template = Liquid::Template.parse(@classdiagram_template_content, error_mode: :strict)
rendered_output = template.render(template_args)
File.write(@classdiagram_mermaid_temp_output_filename, rendered_output)
File.write(@classdiagram_svg_output_filename, post_kroki_mermaid_svg(rendered_output))
end
def post_kroki_mermaid_svg(data)
res = ::Net::HTTP.post(
URI("https://kroki.io/mermaid/svg"),
data,
"Content-Type" => "text/plain"
)
res.body
end
main

OData Namespace "{{ namespace }}"

Overview class diagram

![{{ classdiagram_svg_filename }}]({{ classdiagram_svg_filename }})

Entities

{% for entity in entities -%}

{{ entity.name }}

{% unless entity.associations_with == empty -%} Has associations with: {% for association in entity.associations_with %}{{ association }}{% unless forloop.last == true %}, {% endunless %}{% endfor %}

{% endunless -%}

Key? Name Type Nullable Label MaxLength Precision Scale Filterable
{% for property in entity.properties -%}
{% if property.IsKey %}X{% endif %} {{ property.Name }} {{ property.Type }} {% if property.Nullable %}X{% else %}-{% endif %} {{ property.label }} {{ property.MaxLength }} {{ property.Precision }} {{ property.Scale }} {% if property.filterable %}X{% else %}-{% endif %}
{% endfor %}
{% endfor %}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

OData Namespace "MY_SAP_ODATA_SERVICE"

Overview class diagram

odata-classes.svg

Entities

Driver

Has associations with: Car

Key? Name Type Nullable Label MaxLength Precision Scale Filterable
X DriverId String - Driver Id 36 X
Salutation String X Anrede 15 -
Name1 String - Name 35 -
Name2 String X Name 2 35 -
City String - Ort 35 -
PostalCode String - Postleitzahl 10 -
Street String - Straße 60 -
HouseNum String - Hausnummer 10 -

Car

Has associations with: Driver

Key? Name Type Nullable Label MaxLength Precision Scale Filterable
X CarId String - Car Id 12 X
PowerKW Decimal - Motorleistung kw 6 2 X
Cylinders Int32 - Anzahl Zylinder X
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:sap="http://www.sap.com/Protocols/SAPData">
<edmx:DataServices m:DataServiceVersion="2.0">
<Schema Namespace="MY_SAP_ODATA_SERVICE" xml:lang="de" sap:schema-version="1" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<EntityType Name="Driver" sap:content-version="1">
<Key>
<PropertyRef Name="DriverId" />
</Key>
<Property Name="DriverId" Type="Edm.String" Nullable="false" MaxLength="36" sap:unicode="false" sap:label="Driver Id" sap:creatable="false" sap:updatable="false" sap:sortable="false" />
<Property Name="Salutation" Type="Edm.String" MaxLength="15" sap:unicode="false" sap:label="Anrede" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
<Property Name="Name1" Type="Edm.String" Nullable="false" MaxLength="35" sap:unicode="false" sap:label="Name" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
<Property Name="Name2" Type="Edm.String" MaxLength="35" sap:unicode="false" sap:label="Name 2" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
<Property Name="City" Type="Edm.String" Nullable="false" MaxLength="35" sap:unicode="false" sap:label="Ort" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
<Property Name="PostalCode" Type="Edm.String" Nullable="false" MaxLength="10" sap:unicode="false" sap:label="Postleitzahl" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
<Property Name="Street" Type="Edm.String" Nullable="false" MaxLength="60" sap:unicode="false" sap:label="Straße" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
<Property Name="HouseNum" Type="Edm.String" Nullable="false" MaxLength="10" sap:unicode="false" sap:label="Hausnummer" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false" />
</EntityType>
<EntityType Name="Car" sap:content-version="1">
<Key>
<PropertyRef Name="CarId" />
</Key>
<Property Name="CarId" Type="Edm.String" Nullable="false" MaxLength="12" sap:unicode="false" sap:label="Car Id" sap:creatable="false" sap:updatable="false" sap:sortable="false" />
<Property Name="PowerKW" Type="Edm.Decimal" Nullable="false" Precision="6" Scale="2" sap:unicode="false" sap:label="Motorleistung kw" sap:creatable="false" sap:updatable="false" sap:sortable="false" />
<Property Name="Cylinders" Type="Edm.Int32" Nullable="false" sap:unicode="false" sap:label="Anzahl Zylinder" sap:creatable="false" sap:updatable="false" sap:sortable="false" />
</EntityType>
<Association Name="DriverToCars" sap:content-version="1">
<End Type="MY_SAP_ODATA_SERVICE.Driver" Multiplicity="1" Role="FromRole_DriverToCars" />
<End Type="MY_SAP_ODATA_SERVICE.Car" Multiplicity="*" Role="ToRole_DriverToCars" />
</Association>
<EntityContainer Name="MY_SAP_ODATA_SERVICE_Entities" m:IsDefaultEntityContainer="true" sap:supported-formats="atom json xlsx">
<EntitySet Name="DriverSet" EntityType="MY_SAP_ODATA_SERVICE.Driver" sap:deletable="false" sap:pageable="false" sap:requires-filter="true" sap:content-version="1" />
<EntitySet Name="CarSet" EntityType="MY_SAP_ODATA_SERVICE.Car" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:pageable="false" sap:requires-filter="true" sap:content-version="1" />
<AssociationSet Name="DriverToCars_AssocSet" Association="MY_SAP_ODATA_SERVICE.DriverToCars" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:content-version="1">
<End EntitySet="DriverSet" Role="FromRole_DriverToCars" />
<End EntitySet="CarSet" Role="ToRole_DriverToCars" />
</AssociationSet>
</EntityContainer>
<atom:link rel="self" href="https://netweaver.example.com/sap/opu/odata/sap/MY_SAP_ODATA_SERVICE/$metadata" xmlns:atom="http://www.w3.org/2005/Atom" />
<atom:link rel="latest-version" href="https://netweaver.example.com/sap/opu/odata/sap/MY_SAP_ODATA_SERVICE/$metadata" xmlns:atom="http://www.w3.org/2005/Atom" />
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment