rdfcodegen 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdfcodegen-0.1.0/PKG-INFO +169 -0
- rdfcodegen-0.1.0/README.md +160 -0
- rdfcodegen-0.1.0/pyproject.toml +17 -0
- rdfcodegen-0.1.0/src/rdfcodegen/__init__.py +269 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: rdfcodegen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: carlosmpv
|
|
6
|
+
Author-email: carlosmpv <carlosmpvaldes@gmail.com>
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# rdfcodegen
|
|
11
|
+
|
|
12
|
+
Generate typed code interfaces from RDF/Turtle ontologies (e.g., schema.org).
|
|
13
|
+
|
|
14
|
+
**Currently supports TypeScript output. Additional language generators planned.**
|
|
15
|
+
|
|
16
|
+
## In this README :point_down:
|
|
17
|
+
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Usage](#usage)
|
|
21
|
+
- [Command line](#command-line)
|
|
22
|
+
- [Python API](#python-api)
|
|
23
|
+
- [Filtering specific classes](#filtering-specific-classes)
|
|
24
|
+
- [How it works](#how-it-works)
|
|
25
|
+
- [Roadmap](#roadmap)
|
|
26
|
+
- [Projects using this template](#projects-using-this-template)
|
|
27
|
+
- [FAQ](#faq)
|
|
28
|
+
- [Contributing](#contributing)
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
**rdfcodegen** parses RDF/Turtle ontology files (like [schema.org](https://schema.org/)) and generates language-specific code with full type definitions, documentation, and inheritance.
|
|
33
|
+
|
|
34
|
+
:rocket: **What it produces:**
|
|
35
|
+
- Type-safe interfaces/classes extracted from RDFS/OWL class hierarchies.
|
|
36
|
+
- JSDoc/Docstring comments pulled from `rdfs:comment` annotations.
|
|
37
|
+
- Automatic dependency resolution — request one class and get all its parent types and field types transitively.
|
|
38
|
+
- Primitive type mapping (e.g., `schema:Text` → `string`, `schema:Number` → `number`).
|
|
39
|
+
|
|
40
|
+
:computer: **Supported output languages:**
|
|
41
|
+
- [x] **TypeScript** — `interface` definitions with optional properties (`?`) and array types.
|
|
42
|
+
- [ ] Go (planned)
|
|
43
|
+
- [ ] Python (planned)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install rdfcodegen
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
*Requires Python 3.9+ and [rdflib](https://github.com/RDFLib/rdflib).*
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
### Command line
|
|
56
|
+
|
|
57
|
+
Generate TypeScript interfaces for all schema.org types:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
rdfcodegen --ttl https://schema.org/version/latest/schemaorg-current-https.ttl \
|
|
61
|
+
--namespace https://schema.org/ \
|
|
62
|
+
--out schema.ts
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Generate only specific classes (and everything they depend on):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
rdfcodegen --class schema:Person --class schema:Organization --out schema.ts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Write to stdout (`-`):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
rdfcodegen > schema.ts
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**CLI options:**
|
|
78
|
+
|
|
79
|
+
| Option | Default | Description |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `--ttl` | `schema.org/version/latest/schemaorg-current-https.ttl` | URL or path to Turtle file |
|
|
82
|
+
| `--namespace` | `https://schema.org/` | Ontology namespace URI |
|
|
83
|
+
| `--out` | `-` | Output file (`-` for stdout) |
|
|
84
|
+
| `--class` | (all classes) | Generate specific classes (repeatable) |
|
|
85
|
+
|
|
86
|
+
### Python API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from rdfcodegen import TypescriptGenerator
|
|
90
|
+
|
|
91
|
+
gen = TypescriptGenerator()
|
|
92
|
+
|
|
93
|
+
# Generate all types
|
|
94
|
+
ts_code = gen.generate(
|
|
95
|
+
ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
|
|
96
|
+
namespace_url="https://schema.org/",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Generate a subset
|
|
100
|
+
ts_code = gen.generate(
|
|
101
|
+
chosen=["schema:Person", "schema:Event"],
|
|
102
|
+
ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
|
|
103
|
+
namespace_url="https://schema.org/",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with open("schema.ts", "w") as f:
|
|
107
|
+
f.write(ts_code)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Filtering specific classes
|
|
111
|
+
|
|
112
|
+
When you specify classes with `--class` (or the `chosen` parameter), the generator:
|
|
113
|
+
|
|
114
|
+
1. Includes the requested classes.
|
|
115
|
+
2. Recursively includes all **parent types** up the `rdfs:subClassOf` hierarchy.
|
|
116
|
+
3. Recursively includes all **field types** referenced via `schema:rangeIncludes`.
|
|
117
|
+
4. Skips primitive types (e.g., `schema:Text`, `schema:Number`) — these map directly to language primitives.
|
|
118
|
+
|
|
119
|
+
This ensures the generated output is always self-contained and compilable.
|
|
120
|
+
|
|
121
|
+
## How it works
|
|
122
|
+
|
|
123
|
+
1. **Parse** — The Turtle file is loaded into an RDF graph using `rdflib`.
|
|
124
|
+
2. **Discover classes** — All subjects typed as `rdfs:Class` within the target namespace are collected.
|
|
125
|
+
3. **Traverse hierarchy** — `rdfs:subClassOf` relationships are followed to build the class hierarchy.
|
|
126
|
+
4. **Collect properties** — `schema:domainIncludes` and `schema:rangeIncludes` are used to determine which fields belong to which classes and what types they return.
|
|
127
|
+
5. **Extract documentation** — `rdfs:comment` values become JSDoc/docstring comments.
|
|
128
|
+
6. **Resolve dependencies** — If filtering is requested, a dependency graph walk collects all transitive type references.
|
|
129
|
+
7. **Generate output** — A language-specific generator (e.g., `TypescriptGenerator`) emits the final code.
|
|
130
|
+
|
|
131
|
+
### TypeScript output example
|
|
132
|
+
|
|
133
|
+
From `schema:Person`, the generator produces:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT DIRECTLY.
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A person (alive, dead, undead, or fictional).
|
|
140
|
+
*/
|
|
141
|
+
export interface SchemaPerson extends SchemaThing {
|
|
142
|
+
/**
|
|
143
|
+
* An additional name for a Person, can be used for a middle name.
|
|
144
|
+
*/
|
|
145
|
+
additionalName?: string[];
|
|
146
|
+
/**
|
|
147
|
+
* Physical address of the item.
|
|
148
|
+
*/
|
|
149
|
+
address?: (SchemaPostalAddress | string)[];
|
|
150
|
+
/**
|
|
151
|
+
* Date of birth.
|
|
152
|
+
*/
|
|
153
|
+
birthDate?: string[];
|
|
154
|
+
// ... more fields
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Roadmap
|
|
159
|
+
|
|
160
|
+
- [ ] Typescript types generator
|
|
161
|
+
- [ ] Helper function to generate JSON-LD from those structs
|
|
162
|
+
- [ ] Go struct generator with JSON tags
|
|
163
|
+
- [ ] Python dataclass/pydantic generator
|
|
164
|
+
- [ ] Choose other languages
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
If you find a bug :bug:, please open a [bug report](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
|
|
169
|
+
If you have an idea for an improvement or new feature :rocket:, please open a [feature request](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=Feature+request&template=feature_request.md&title=).
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# rdfcodegen
|
|
2
|
+
|
|
3
|
+
Generate typed code interfaces from RDF/Turtle ontologies (e.g., schema.org).
|
|
4
|
+
|
|
5
|
+
**Currently supports TypeScript output. Additional language generators planned.**
|
|
6
|
+
|
|
7
|
+
## In this README :point_down:
|
|
8
|
+
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Usage](#usage)
|
|
12
|
+
- [Command line](#command-line)
|
|
13
|
+
- [Python API](#python-api)
|
|
14
|
+
- [Filtering specific classes](#filtering-specific-classes)
|
|
15
|
+
- [How it works](#how-it-works)
|
|
16
|
+
- [Roadmap](#roadmap)
|
|
17
|
+
- [Projects using this template](#projects-using-this-template)
|
|
18
|
+
- [FAQ](#faq)
|
|
19
|
+
- [Contributing](#contributing)
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
**rdfcodegen** parses RDF/Turtle ontology files (like [schema.org](https://schema.org/)) and generates language-specific code with full type definitions, documentation, and inheritance.
|
|
24
|
+
|
|
25
|
+
:rocket: **What it produces:**
|
|
26
|
+
- Type-safe interfaces/classes extracted from RDFS/OWL class hierarchies.
|
|
27
|
+
- JSDoc/Docstring comments pulled from `rdfs:comment` annotations.
|
|
28
|
+
- Automatic dependency resolution — request one class and get all its parent types and field types transitively.
|
|
29
|
+
- Primitive type mapping (e.g., `schema:Text` → `string`, `schema:Number` → `number`).
|
|
30
|
+
|
|
31
|
+
:computer: **Supported output languages:**
|
|
32
|
+
- [x] **TypeScript** — `interface` definitions with optional properties (`?`) and array types.
|
|
33
|
+
- [ ] Go (planned)
|
|
34
|
+
- [ ] Python (planned)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install rdfcodegen
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
*Requires Python 3.9+ and [rdflib](https://github.com/RDFLib/rdflib).*
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Command line
|
|
47
|
+
|
|
48
|
+
Generate TypeScript interfaces for all schema.org types:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rdfcodegen --ttl https://schema.org/version/latest/schemaorg-current-https.ttl \
|
|
52
|
+
--namespace https://schema.org/ \
|
|
53
|
+
--out schema.ts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Generate only specific classes (and everything they depend on):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
rdfcodegen --class schema:Person --class schema:Organization --out schema.ts
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Write to stdout (`-`):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
rdfcodegen > schema.ts
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**CLI options:**
|
|
69
|
+
|
|
70
|
+
| Option | Default | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `--ttl` | `schema.org/version/latest/schemaorg-current-https.ttl` | URL or path to Turtle file |
|
|
73
|
+
| `--namespace` | `https://schema.org/` | Ontology namespace URI |
|
|
74
|
+
| `--out` | `-` | Output file (`-` for stdout) |
|
|
75
|
+
| `--class` | (all classes) | Generate specific classes (repeatable) |
|
|
76
|
+
|
|
77
|
+
### Python API
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from rdfcodegen import TypescriptGenerator
|
|
81
|
+
|
|
82
|
+
gen = TypescriptGenerator()
|
|
83
|
+
|
|
84
|
+
# Generate all types
|
|
85
|
+
ts_code = gen.generate(
|
|
86
|
+
ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
|
|
87
|
+
namespace_url="https://schema.org/",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Generate a subset
|
|
91
|
+
ts_code = gen.generate(
|
|
92
|
+
chosen=["schema:Person", "schema:Event"],
|
|
93
|
+
ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
|
|
94
|
+
namespace_url="https://schema.org/",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with open("schema.ts", "w") as f:
|
|
98
|
+
f.write(ts_code)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Filtering specific classes
|
|
102
|
+
|
|
103
|
+
When you specify classes with `--class` (or the `chosen` parameter), the generator:
|
|
104
|
+
|
|
105
|
+
1. Includes the requested classes.
|
|
106
|
+
2. Recursively includes all **parent types** up the `rdfs:subClassOf` hierarchy.
|
|
107
|
+
3. Recursively includes all **field types** referenced via `schema:rangeIncludes`.
|
|
108
|
+
4. Skips primitive types (e.g., `schema:Text`, `schema:Number`) — these map directly to language primitives.
|
|
109
|
+
|
|
110
|
+
This ensures the generated output is always self-contained and compilable.
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
1. **Parse** — The Turtle file is loaded into an RDF graph using `rdflib`.
|
|
115
|
+
2. **Discover classes** — All subjects typed as `rdfs:Class` within the target namespace are collected.
|
|
116
|
+
3. **Traverse hierarchy** — `rdfs:subClassOf` relationships are followed to build the class hierarchy.
|
|
117
|
+
4. **Collect properties** — `schema:domainIncludes` and `schema:rangeIncludes` are used to determine which fields belong to which classes and what types they return.
|
|
118
|
+
5. **Extract documentation** — `rdfs:comment` values become JSDoc/docstring comments.
|
|
119
|
+
6. **Resolve dependencies** — If filtering is requested, a dependency graph walk collects all transitive type references.
|
|
120
|
+
7. **Generate output** — A language-specific generator (e.g., `TypescriptGenerator`) emits the final code.
|
|
121
|
+
|
|
122
|
+
### TypeScript output example
|
|
123
|
+
|
|
124
|
+
From `schema:Person`, the generator produces:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT DIRECTLY.
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A person (alive, dead, undead, or fictional).
|
|
131
|
+
*/
|
|
132
|
+
export interface SchemaPerson extends SchemaThing {
|
|
133
|
+
/**
|
|
134
|
+
* An additional name for a Person, can be used for a middle name.
|
|
135
|
+
*/
|
|
136
|
+
additionalName?: string[];
|
|
137
|
+
/**
|
|
138
|
+
* Physical address of the item.
|
|
139
|
+
*/
|
|
140
|
+
address?: (SchemaPostalAddress | string)[];
|
|
141
|
+
/**
|
|
142
|
+
* Date of birth.
|
|
143
|
+
*/
|
|
144
|
+
birthDate?: string[];
|
|
145
|
+
// ... more fields
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Roadmap
|
|
150
|
+
|
|
151
|
+
- [ ] Typescript types generator
|
|
152
|
+
- [ ] Helper function to generate JSON-LD from those structs
|
|
153
|
+
- [ ] Go struct generator with JSON tags
|
|
154
|
+
- [ ] Python dataclass/pydantic generator
|
|
155
|
+
- [ ] Choose other languages
|
|
156
|
+
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
If you find a bug :bug:, please open a [bug report](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
|
|
160
|
+
If you have an idea for an improvement or new feature :rocket:, please open a [feature request](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=Feature+request&template=feature_request.md&title=).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rdfcodegen"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "carlosmpv", email = "carlosmpvaldes@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
rdfcodegen = "rdfcodegen:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import rdflib
|
|
10
|
+
from rdflib import RDF, RDFS, URIRef
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class FieldDef:
|
|
15
|
+
field: str
|
|
16
|
+
comment: tuple[str, ...]
|
|
17
|
+
types: tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ClassDef:
|
|
22
|
+
comment: tuple[str, ...]
|
|
23
|
+
parents: tuple[str, ...]
|
|
24
|
+
fields: tuple[FieldDef, ...]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RDFCodeGenerator(ABC):
|
|
28
|
+
def _qname_or_iri(self, g: rdflib.Graph, term: URIRef) -> str:
|
|
29
|
+
try:
|
|
30
|
+
return g.namespace_manager.normalizeUri(term)
|
|
31
|
+
except Exception:
|
|
32
|
+
return str(term)
|
|
33
|
+
|
|
34
|
+
def clean_comment(self, text: str) -> str:
|
|
35
|
+
return " ".join(text.split()).replace("*/", "* /")
|
|
36
|
+
|
|
37
|
+
def parse_ttl(
|
|
38
|
+
self,
|
|
39
|
+
ttl_url: str,
|
|
40
|
+
namespace_url: str,
|
|
41
|
+
) -> dict[str, ClassDef]:
|
|
42
|
+
schema = rdflib.Namespace(namespace_url)
|
|
43
|
+
g = rdflib.Graph()
|
|
44
|
+
g.parse(ttl_url, format="turtle")
|
|
45
|
+
|
|
46
|
+
classes: set[URIRef] = set()
|
|
47
|
+
props_by_class: dict[URIRef, set[URIRef]] = defaultdict(set)
|
|
48
|
+
range_by_prop: dict[URIRef, set[URIRef]] = defaultdict(set)
|
|
49
|
+
class_comments: dict[URIRef, list[str]] = defaultdict(list)
|
|
50
|
+
prop_comments: dict[URIRef, list[str]] = defaultdict(list)
|
|
51
|
+
parents_by_class: dict[URIRef, set[URIRef]] = defaultdict(set)
|
|
52
|
+
|
|
53
|
+
for s in g.subjects(RDF.type, RDFS.Class):
|
|
54
|
+
if isinstance(s, URIRef) and str(s).startswith(str(schema)):
|
|
55
|
+
classes.add(s)
|
|
56
|
+
|
|
57
|
+
# fecha a hierarquia antes de gerar saída
|
|
58
|
+
queue = list(classes)
|
|
59
|
+
while queue:
|
|
60
|
+
cls = queue.pop()
|
|
61
|
+
for parent in g.objects(cls, RDFS.subClassOf):
|
|
62
|
+
if isinstance(parent, URIRef) and str(parent).startswith(str(schema)):
|
|
63
|
+
if parent not in classes:
|
|
64
|
+
classes.add(parent)
|
|
65
|
+
queue.append(parent)
|
|
66
|
+
parents_by_class[cls].add(parent)
|
|
67
|
+
|
|
68
|
+
for cls in classes:
|
|
69
|
+
for c in g.objects(cls, RDFS.comment):
|
|
70
|
+
if isinstance(c, rdflib.Literal):
|
|
71
|
+
class_comments[cls].append(str(c))
|
|
72
|
+
|
|
73
|
+
for prop, cls in g.subject_objects(schema.domainIncludes):
|
|
74
|
+
if isinstance(prop, URIRef) and isinstance(cls, URIRef):
|
|
75
|
+
if str(prop).startswith(str(schema)) and str(cls).startswith(str(schema)):
|
|
76
|
+
props_by_class[cls].add(prop)
|
|
77
|
+
|
|
78
|
+
for prop, rng in g.subject_objects(schema.rangeIncludes):
|
|
79
|
+
if isinstance(prop, URIRef) and isinstance(rng, URIRef):
|
|
80
|
+
if str(prop).startswith(str(schema)) and str(rng).startswith(str(schema)):
|
|
81
|
+
range_by_prop[prop].add(rng)
|
|
82
|
+
|
|
83
|
+
all_props = {p for props in props_by_class.values() for p in props}
|
|
84
|
+
for prop in all_props:
|
|
85
|
+
for c in g.objects(prop, RDFS.comment):
|
|
86
|
+
if isinstance(c, rdflib.Literal):
|
|
87
|
+
prop_comments[prop].append(str(c))
|
|
88
|
+
|
|
89
|
+
out: dict[str, ClassDef] = {}
|
|
90
|
+
for cls in sorted(classes, key=str):
|
|
91
|
+
class_key = self._qname_or_iri(g, cls)
|
|
92
|
+
fields: list[FieldDef] = []
|
|
93
|
+
|
|
94
|
+
for prop in sorted(props_by_class.get(cls, set()), key=str):
|
|
95
|
+
fields.append(
|
|
96
|
+
FieldDef(
|
|
97
|
+
field=self._qname_or_iri(g, prop),
|
|
98
|
+
comment=tuple(dict.fromkeys(prop_comments.get(prop, []))),
|
|
99
|
+
types=tuple(sorted({
|
|
100
|
+
self._qname_or_iri(g, t) for t in range_by_prop.get(prop, set())
|
|
101
|
+
})),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
out[class_key] = ClassDef(
|
|
106
|
+
comment=tuple(dict.fromkeys(class_comments.get(cls, []))),
|
|
107
|
+
parents=tuple(
|
|
108
|
+
self._qname_or_iri(g, p)
|
|
109
|
+
for p in sorted(parents_by_class.get(cls, set()), key=str)
|
|
110
|
+
),
|
|
111
|
+
fields=tuple(fields),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
def generate(self, chosen: list[str] | None = None, **kwargs) -> str:
|
|
117
|
+
chosen = chosen or []
|
|
118
|
+
out = self.parse_ttl(**kwargs)
|
|
119
|
+
|
|
120
|
+
if not chosen:
|
|
121
|
+
return self._generate(out)
|
|
122
|
+
|
|
123
|
+
filtered: dict[str, ClassDef] = {}
|
|
124
|
+
visited: set[str] = set()
|
|
125
|
+
|
|
126
|
+
def collect(key: str) -> None:
|
|
127
|
+
if key in visited or key not in out:
|
|
128
|
+
return
|
|
129
|
+
visited.add(key)
|
|
130
|
+
filtered[key] = out[key]
|
|
131
|
+
|
|
132
|
+
deps = set(out[key].parents)
|
|
133
|
+
for field in out[key].fields:
|
|
134
|
+
deps.update(field.types)
|
|
135
|
+
|
|
136
|
+
for dep in deps:
|
|
137
|
+
collect(dep)
|
|
138
|
+
|
|
139
|
+
for item in chosen:
|
|
140
|
+
collect(item)
|
|
141
|
+
|
|
142
|
+
return self._generate(filtered)
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def _generate(self, defs: dict[str, ClassDef]) -> str:
|
|
146
|
+
...
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TypescriptGenerator(RDFCodeGenerator):
|
|
150
|
+
primitive_ts_types = {
|
|
151
|
+
"schema:Boolean": "boolean",
|
|
152
|
+
"schema:Date": "string",
|
|
153
|
+
"schema:DateTime": "string",
|
|
154
|
+
"schema:Number": "number",
|
|
155
|
+
"schema:Float": "number",
|
|
156
|
+
"schema:Integer": "number",
|
|
157
|
+
"schema:Quantity": "number",
|
|
158
|
+
"schema:Distance": "number",
|
|
159
|
+
"schema:Duration": "number",
|
|
160
|
+
"schema:Energy": "number",
|
|
161
|
+
"schema:Mass": "number",
|
|
162
|
+
"schema:Text": "string",
|
|
163
|
+
"schema:CssSelectorType": "string",
|
|
164
|
+
"schema:PronounceableText": "string",
|
|
165
|
+
"schema:URL": "string",
|
|
166
|
+
"schema:XPathType": "string",
|
|
167
|
+
"schema:Time": "string",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def schema_to_ts_types(self, org: dict[str, ClassDef]) -> dict[str, ClassDef]:
|
|
171
|
+
def as_ts_class(name: str) -> str:
|
|
172
|
+
name = name.removeprefix("schema:")
|
|
173
|
+
return f"Schema{name}"
|
|
174
|
+
|
|
175
|
+
def as_ts_field(name: str) -> str:
|
|
176
|
+
return name.removeprefix("schema:")
|
|
177
|
+
|
|
178
|
+
result: dict[str, ClassDef] = {}
|
|
179
|
+
|
|
180
|
+
for class_name, data in org.items():
|
|
181
|
+
if class_name in self.primitive_ts_types:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
parents = tuple(
|
|
185
|
+
sorted({
|
|
186
|
+
self.primitive_ts_types[p] if p in self.primitive_ts_types else as_ts_class(p)
|
|
187
|
+
for p in data.parents
|
|
188
|
+
if p not in self.primitive_ts_types
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
fields: list[FieldDef] = []
|
|
193
|
+
for item in data.fields:
|
|
194
|
+
types = tuple(
|
|
195
|
+
sorted({
|
|
196
|
+
self.primitive_ts_types[t] if t in self.primitive_ts_types else as_ts_class(t)
|
|
197
|
+
for t in item.types
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
fields.append(
|
|
201
|
+
FieldDef(
|
|
202
|
+
field=as_ts_field(item.field),
|
|
203
|
+
comment=item.comment,
|
|
204
|
+
types=types,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
result[as_ts_class(class_name)] = ClassDef(
|
|
209
|
+
comment=data.comment,
|
|
210
|
+
parents=parents,
|
|
211
|
+
fields=tuple(fields),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
def jsdoc(self, lines: tuple[str, ...], indent: str = "") -> list[str]:
|
|
217
|
+
lines = tuple([self.clean_comment(line) for line in lines if line.strip()])
|
|
218
|
+
if not lines:
|
|
219
|
+
return []
|
|
220
|
+
return [f"{indent}/**", *[f"{indent} * {line}" for line in lines], f"{indent} */"]
|
|
221
|
+
|
|
222
|
+
def ts_type(self, types: tuple[str, ...]) -> str:
|
|
223
|
+
if not types:
|
|
224
|
+
return "unknown[]"
|
|
225
|
+
if len(types) == 1:
|
|
226
|
+
return f"{types[0]}[]"
|
|
227
|
+
return f"({ ' | '.join(types) })[]"
|
|
228
|
+
|
|
229
|
+
def _generate(self, defs: dict[str, ClassDef]) -> str:
|
|
230
|
+
defs = self.schema_to_ts_types(defs)
|
|
231
|
+
|
|
232
|
+
result_lines = ["// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT DIRECTLY."]
|
|
233
|
+
|
|
234
|
+
for class_name in sorted(defs):
|
|
235
|
+
data = defs[class_name]
|
|
236
|
+
result_lines += self.jsdoc(data.comment)
|
|
237
|
+
|
|
238
|
+
extends_part = f" extends {', '.join(data.parents)}" if data.parents else ""
|
|
239
|
+
result_lines.append(f"export interface {class_name}{extends_part} {{")
|
|
240
|
+
|
|
241
|
+
for field in data.fields:
|
|
242
|
+
result_lines += self.jsdoc(field.comment, indent=" ")
|
|
243
|
+
result_lines.append(f" {field.field}?: {self.ts_type(field.types)};")
|
|
244
|
+
|
|
245
|
+
result_lines.append("}\n")
|
|
246
|
+
|
|
247
|
+
return "\n".join(result_lines).rstrip() + "\n"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main() -> None:
|
|
251
|
+
parser = argparse.ArgumentParser()
|
|
252
|
+
parser.add_argument("--ttl", default="https://schema.org/version/latest/schemaorg-current-https.ttl")
|
|
253
|
+
parser.add_argument("--namespace", default="https://schema.org/")
|
|
254
|
+
parser.add_argument("--out", default="-")
|
|
255
|
+
parser.add_argument("--class", dest="classes", action="append", default=[])
|
|
256
|
+
args = parser.parse_args()
|
|
257
|
+
|
|
258
|
+
gen = TypescriptGenerator()
|
|
259
|
+
result = gen.generate(args.classes, ttl_url=args.ttl, namespace_url=args.namespace)
|
|
260
|
+
|
|
261
|
+
if args.out == "-":
|
|
262
|
+
sys.stdout.write(result)
|
|
263
|
+
else:
|
|
264
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
265
|
+
f.write(result)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
main()
|