chronicle-core 0.2.1 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +1 -1
- data/.gitignore +3 -1
- data/.rubocop-plugin.yml +4 -0
- data/.rubocop.yml +16 -2
- data/Gemfile +2 -2
- data/Guardfile +3 -3
- data/LICENSE.txt +1 -1
- data/README.md +87 -2
- data/Rakefile +63 -1
- data/bin/console +6 -6
- data/chronicle-core.gemspec +32 -26
- data/lib/chronicle/core/version.rb +1 -3
- data/lib/chronicle/core.rb +1 -3
- data/lib/chronicle/models/base.rb +96 -0
- data/lib/chronicle/models/builder.rb +35 -0
- data/lib/chronicle/models/generation.rb +89 -0
- data/lib/chronicle/models/model_factory.rb +63 -0
- data/lib/chronicle/models.rb +17 -0
- data/lib/chronicle/schema/rdf_parsing/graph_transformer.rb +122 -0
- data/lib/chronicle/schema/rdf_parsing/rdf_serializer.rb +138 -0
- data/lib/chronicle/schema/rdf_parsing/schemaorg.rb +50 -0
- data/lib/chronicle/schema/rdf_parsing/ttl_graph_builder.rb +142 -0
- data/lib/chronicle/schema/rdf_parsing.rb +11 -0
- data/lib/chronicle/schema/schema_graph.rb +145 -0
- data/lib/chronicle/schema/schema_property.rb +81 -0
- data/lib/chronicle/schema/schema_type.rb +110 -0
- data/lib/chronicle/schema/types.rb +9 -0
- data/lib/chronicle/schema/validation/base_contract.rb +22 -0
- data/lib/chronicle/schema/validation/contract_factory.rb +133 -0
- data/lib/chronicle/schema/validation/edge_validator.rb +53 -0
- data/lib/chronicle/schema/validation/generation.rb +29 -0
- data/lib/chronicle/schema/validation/validator.rb +23 -0
- data/lib/chronicle/schema/validation.rb +41 -0
- data/lib/chronicle/schema.rb +9 -2
- data/lib/chronicle/serialization/hash_serializer.rb +5 -11
- data/lib/chronicle/serialization/jsonapi_serializer.rb +41 -26
- data/lib/chronicle/serialization/jsonld_serializer.rb +38 -0
- data/lib/chronicle/serialization/record.rb +90 -0
- data/lib/chronicle/serialization/serializer.rb +31 -18
- data/lib/chronicle/serialization.rb +6 -4
- data/lib/chronicle/utils/hash_utils.rb +26 -0
- data/schema/chronicle_schema_v1.json +1008 -0
- data/schema/chronicle_schema_v1.rb +147 -0
- data/schema/chronicle_schema_v1.ttl +562 -0
- metadata +108 -16
- data/lib/chronicle/schema/activity.rb +0 -5
- data/lib/chronicle/schema/base.rb +0 -64
- data/lib/chronicle/schema/entity.rb +0 -5
- data/lib/chronicle/schema/raw.rb +0 -9
- data/lib/chronicle/schema/validator.rb +0 -55
- data/lib/chronicle/utils/hash.rb +0 -15
@@ -0,0 +1,122 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module RDFParsing
|
4
|
+
# A class that inteprets a DSL to transform a base schema graph into
|
5
|
+
# a new one by walking through the types and properties of the base
|
6
|
+
# and selecting which ones to include in the new graph.
|
7
|
+
class GraphTransformer
|
8
|
+
attr_reader :graph, :base_graph, :new_graph
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@base_graph = nil
|
12
|
+
@new_graph = Chronicle::Schema::SchemaGraph.new(default_namespace: 'https://schema.chronicle.app/')
|
13
|
+
@current_parent = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.transform(&)
|
17
|
+
transformer = new
|
18
|
+
transformer.start_evaluating(&)
|
19
|
+
|
20
|
+
# TODO: figure out if we need to this still
|
21
|
+
transformer.new_graph.properties.each do |property|
|
22
|
+
property.domain = property.domain.select do |type_id|
|
23
|
+
transformer.new_graph.find_type_by_id(type_id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
transformer.new_graph.build_references!
|
28
|
+
transformer.new_graph
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.transform_from_file(definition_file_path)
|
32
|
+
dsl_commands = File.read(definition_file_path)
|
33
|
+
transform_from_string(dsl_commands)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.transform_from_string(dsl_definition)
|
37
|
+
transform do
|
38
|
+
instance_eval(dsl_definition)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def start_evaluating(&)
|
43
|
+
instance_eval(&) if block_given?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def version(version)
|
49
|
+
@new_graph.version = version
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_base_graph(name, version)
|
53
|
+
case name
|
54
|
+
when 'schema.org'
|
55
|
+
@base_graph = Chronicle::Schema::RDFParsing::Schemaorg.graph_for_version(version)
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Unknown base graph: #{name}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def pick_type(subtype_identifier, &)
|
62
|
+
id = @base_graph.identifier_to_uri(subtype_identifier)
|
63
|
+
type = @base_graph.find_type_by_id(id)
|
64
|
+
raise ArgumentError, "Subtype not found: #{subtype_identifier}" unless type
|
65
|
+
|
66
|
+
new_subtype = @new_graph.add_type(subtype_identifier)
|
67
|
+
new_subtype.comment = type.comment
|
68
|
+
new_subtype.see_also = type.id
|
69
|
+
|
70
|
+
@current_parent&.add_subtype_id(new_subtype.id)
|
71
|
+
|
72
|
+
previous_parent = @current_parent
|
73
|
+
@current_parent = new_subtype
|
74
|
+
|
75
|
+
instance_eval(&) if block_given?
|
76
|
+
|
77
|
+
@current_parent = previous_parent
|
78
|
+
end
|
79
|
+
|
80
|
+
def pick_all_subtypes(&)
|
81
|
+
@base_graph.find_type(@current_parent.short_id.to_sym).subtype_ids.each do |subtype_id|
|
82
|
+
identifier = @base_graph.id_to_identifier(subtype_id)
|
83
|
+
pick_type(identifier, &)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def apply_property(property_identifier, many: false, required: false)
|
88
|
+
property = @base_graph.find_property(property_identifier)
|
89
|
+
raise ArgumentError, "Property not found: #{property_identifier}" unless property
|
90
|
+
|
91
|
+
new_property = @new_graph.add_property(property_identifier)
|
92
|
+
new_property.range = property.range.map do |p|
|
93
|
+
base_range_type = @base_graph.find_type_by_id(p)
|
94
|
+
@new_graph.identifier_to_uri(base_range_type.identifier)
|
95
|
+
end
|
96
|
+
new_property.comment = property.comment
|
97
|
+
new_property.many = many
|
98
|
+
new_property.required = required
|
99
|
+
new_property.domain += [@current_parent.id]
|
100
|
+
new_property.see_also = property.id
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_property(property_identifier, many: false, required: false, comment: nil)
|
104
|
+
new_property = @new_graph.add_property(property_identifier)
|
105
|
+
new_property.domain += [@current_parent.id]
|
106
|
+
# TODO: expand this to handle multiple ranges
|
107
|
+
new_property.range = [@new_graph.identifier_to_uri(:Text)]
|
108
|
+
new_property.comment = comment
|
109
|
+
new_property.many = many
|
110
|
+
new_property.required = required
|
111
|
+
end
|
112
|
+
|
113
|
+
def pick_all_properties
|
114
|
+
@base_graph.find_type_by_id(@current_parent.id).properties.each do |property|
|
115
|
+
identifier = @base_graph.id_to_identifier(property.id)
|
116
|
+
apply_property(identifier)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'rdf/turtle'
|
2
|
+
|
3
|
+
module Chronicle
|
4
|
+
module Schema
|
5
|
+
module RDFParsing
|
6
|
+
PREFIXES = {
|
7
|
+
schemaorg: 'https://schema.org/',
|
8
|
+
owl: 'http://www.w3.org/2002/07/owl#',
|
9
|
+
dc: 'http://purl.org/dc/terms/',
|
10
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
11
|
+
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
12
|
+
xml: 'http://www.w3.org/XML/1998/namespace',
|
13
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# Take a graph and serialize it as a ttl string
|
17
|
+
class RDFSerializer
|
18
|
+
attr_reader :graph
|
19
|
+
|
20
|
+
def initialize(graph)
|
21
|
+
raise ArgumentError, 'graph must be a SchemaGraph' unless graph.is_a?(Chronicle::Schema::SchemaGraph)
|
22
|
+
|
23
|
+
@graph = graph
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.serialize(graph, include_generator_comment: true)
|
27
|
+
new(graph).serialize(include_generator_comment:)
|
28
|
+
end
|
29
|
+
|
30
|
+
def serialize(include_generator_comment: true)
|
31
|
+
schema_graph = RDF::Graph.new
|
32
|
+
|
33
|
+
schema_graph << ontology_triple
|
34
|
+
schema_graph << version_triple
|
35
|
+
|
36
|
+
graph.types.each do |klass|
|
37
|
+
serialize_class(klass).each do |triple|
|
38
|
+
# binding.pry
|
39
|
+
schema_graph << triple
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
graph.properties.each do |property|
|
44
|
+
serialize_property(property).each do |triple|
|
45
|
+
schema_graph << triple
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
prefixes = {
|
50
|
+
'': default_namespace
|
51
|
+
}.merge(PREFIXES)
|
52
|
+
|
53
|
+
output_str = ''
|
54
|
+
output_str += generation_header if include_generator_comment
|
55
|
+
output_str + schema_graph.dump(:ttl, prefixes:)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def default_namespace
|
61
|
+
@graph.default_namespace
|
62
|
+
end
|
63
|
+
|
64
|
+
def generation_header
|
65
|
+
<<~TTL
|
66
|
+
# This file was generated from schema/chronicle_schema_v#{graph.version}.rb.
|
67
|
+
#
|
68
|
+
# Do not edit this file directly, as it will be overwritten.
|
69
|
+
#
|
70
|
+
# To generate a new version, run `rake generate`
|
71
|
+
|
72
|
+
TTL
|
73
|
+
end
|
74
|
+
|
75
|
+
def ontology_triple
|
76
|
+
RDF::Statement(RDF::URI.new('https://schema.chronicle.app'), RDF.type, RDF::OWL.Ontology)
|
77
|
+
end
|
78
|
+
|
79
|
+
def version_triple
|
80
|
+
RDF::Statement(RDF::URI.new('https://schema.chronicle.app'), RDF::OWL.versionInfo, graph.version.to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
def serialize_class(klass)
|
84
|
+
statements = []
|
85
|
+
statements << RDF::Statement(RDF::URI.new(klass.id), RDF.type, RDF::RDFS.Class)
|
86
|
+
statements << RDF::Statement(RDF::URI.new(klass.id), RDF::RDFS.comment, klass.comment) if klass.comment
|
87
|
+
|
88
|
+
klass.subtype_ids.each do |subtype_id|
|
89
|
+
statements << RDF::Statement(RDF::URI.new(subtype_id), RDF::RDFS.subClassOf, RDF::URI.new(klass.id))
|
90
|
+
end
|
91
|
+
|
92
|
+
if klass.see_also
|
93
|
+
statements << RDF::Statement(RDF::URI.new(klass.id), RDF::RDFS.seeAlso, RDF::URI.new(klass.see_also))
|
94
|
+
end
|
95
|
+
|
96
|
+
statements
|
97
|
+
end
|
98
|
+
|
99
|
+
def serialize_property(property)
|
100
|
+
statements = []
|
101
|
+
|
102
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF.type, RDF::RDFV.Property)
|
103
|
+
|
104
|
+
property.range.each do |range|
|
105
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF::URI.new("#{@graph.default_namespace}rangeIncludes"),
|
106
|
+
RDF::URI.new(range))
|
107
|
+
end
|
108
|
+
|
109
|
+
property.domain.each do |domain|
|
110
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF::URI.new("#{@graph.default_namespace}domainIncludes"),
|
111
|
+
RDF::URI.new(domain))
|
112
|
+
end
|
113
|
+
|
114
|
+
if property.comment
|
115
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF::RDFS.comment,
|
116
|
+
property.comment)
|
117
|
+
end
|
118
|
+
|
119
|
+
if property.see_also
|
120
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF::RDFS.seeAlso, RDF::URI.new(property.see_also))
|
121
|
+
end
|
122
|
+
|
123
|
+
if property.required?
|
124
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF::OWL.minCardinality,
|
125
|
+
RDF::Literal.new(1, datatype: RDF::XSD.integer))
|
126
|
+
end
|
127
|
+
|
128
|
+
unless property.many
|
129
|
+
statements << RDF::Statement(RDF::URI.new(property.id), RDF::OWL.maxCardinality,
|
130
|
+
RDF::Literal.new(1, datatype: RDF::XSD.integer))
|
131
|
+
end
|
132
|
+
|
133
|
+
statements
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Chronicle
|
5
|
+
module Schema
|
6
|
+
module RDFParsing
|
7
|
+
module Schemaorg
|
8
|
+
@memoized_graphs = {}
|
9
|
+
|
10
|
+
DEFAULT_NAMESPACE = 'https://schema.org/'.freeze
|
11
|
+
|
12
|
+
def self.graph_for_version(version)
|
13
|
+
@memoized_graphs[version] ||= build_graph(version)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.build_graph(version)
|
17
|
+
ttl = ttl_for_version(version)
|
18
|
+
Chronicle::Schema::RDFParsing::TTLGraphBuilder.build_from_ttl(ttl, default_namespace: DEFAULT_NAMESPACE)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.ttl_for_version(version)
|
22
|
+
url = url_for_version(version)
|
23
|
+
ttl_via_download(url)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.ttl_via_download(url)
|
27
|
+
uri = URI(url)
|
28
|
+
response = Net::HTTP.get_response(uri)
|
29
|
+
raise "Error: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
30
|
+
|
31
|
+
response.body
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.seed_graph_from_file(version, file_path)
|
35
|
+
ttl = File.read(file_path)
|
36
|
+
graph = Chronicle::Schema::RDFParsing::TTLGraphBuilder.build_from_ttl(ttl,
|
37
|
+
default_namespace: DEFAULT_NAMESPACE)
|
38
|
+
@memoized_graphs[version] = graph
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.url_for_version(version)
|
42
|
+
# Previously just used this one but it's missing ontologies
|
43
|
+
# 'https://raw.githubusercontent.com/schemaorg/schemaorg/main/data/schema.ttl'
|
44
|
+
|
45
|
+
"https://raw.githubusercontent.com/schemaorg/schemaorg/main/data/releases/#{version}/schemaorg-all-https.ttl"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
require 'rdf/turtle'
|
3
|
+
require_relative '../schema_graph'
|
4
|
+
require_relative '../schema_type'
|
5
|
+
require_relative '../schema_property'
|
6
|
+
|
7
|
+
module Chronicle
|
8
|
+
module Schema
|
9
|
+
module RDFParsing
|
10
|
+
class TTLGraphBuilder
|
11
|
+
attr_reader :ttl_str, :ttl_graph, :graph
|
12
|
+
|
13
|
+
def initialize(ttl_str, default_namespace: 'https://schema.org/')
|
14
|
+
@ttl_str = ttl_str
|
15
|
+
@default_namespace = default_namespace
|
16
|
+
@graph = Chronicle::Schema::SchemaGraph.new(default_namespace:)
|
17
|
+
end
|
18
|
+
|
19
|
+
def build
|
20
|
+
reader = RDF::Reader.for(:ttl).new(@ttl_str)
|
21
|
+
@ttl_graph = RDF::Graph.new << reader
|
22
|
+
|
23
|
+
@graph.version = get_version
|
24
|
+
@graph.types = build_type_graph
|
25
|
+
@graph.properties = build_property_graph
|
26
|
+
# build_datatype_graph
|
27
|
+
@graph.build_references!
|
28
|
+
@graph
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build_from_file(file_path, default_namespace:)
|
32
|
+
new(File.read(file_path), default_namespace:).build
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.build_from_ttl(ttl_str, default_namespace:)
|
36
|
+
new(ttl_str, default_namespace:).build
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# TODO: make this use proper schema
|
42
|
+
def get_version
|
43
|
+
@ttl_graph.query([nil, RDF::OWL.versionInfo, nil]).map(&:object).map(&:to_s).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_type_graph
|
47
|
+
types = all_types.map do |type_id|
|
48
|
+
comment = comment_of_class(type_id)
|
49
|
+
Chronicle::Schema::SchemaType.new(type_id) do |t|
|
50
|
+
t.comment = comment
|
51
|
+
t.namespace = @default_namespace
|
52
|
+
t.see_also = see_also(type_id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
types.each do |schema_type|
|
57
|
+
schema_type.subtype_ids = subtypes_of_class(schema_type.id)
|
58
|
+
end
|
59
|
+
|
60
|
+
types
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_property_graph
|
64
|
+
all_properties.map do |property_id|
|
65
|
+
Chronicle::Schema::SchemaProperty.new(property_id) do |p|
|
66
|
+
p.range = range_of_property(property_id)
|
67
|
+
p.domain = domain_of_property(property_id)
|
68
|
+
p.comment = comment_of_property(property_id)
|
69
|
+
p.required = property_required?(property_id)
|
70
|
+
p.many = property_many?(property_id)
|
71
|
+
p.namespace = @default_namespace
|
72
|
+
p.see_also = see_also(property_id)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def all_types
|
78
|
+
@ttl_graph.query([nil, RDF.type, RDF::RDFS.Class])
|
79
|
+
.map(&:subject)
|
80
|
+
.map(&:to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
def all_properties
|
84
|
+
@ttl_graph.query([nil, RDF.type, RDF::RDFV.Property])
|
85
|
+
.map(&:subject)
|
86
|
+
.map(&:to_s)
|
87
|
+
end
|
88
|
+
|
89
|
+
def all_datatypes; end
|
90
|
+
|
91
|
+
def subtypes_of_class(type_id)
|
92
|
+
@ttl_graph.query([nil, RDF::RDFS.subClassOf, RDF::URI.new(type_id)]).map(&:subject).map(&:to_s)
|
93
|
+
end
|
94
|
+
|
95
|
+
def parents_of_class(type_id)
|
96
|
+
@ttl_graph.query([RDF::URI.new(type_id), RDF::RDFS.subClassOf, nil]).map(&:object).map(&:to_s)
|
97
|
+
end
|
98
|
+
|
99
|
+
def comment_of_class(type_id)
|
100
|
+
@ttl_graph.query([RDF::URI.new(type_id), RDF::RDFS.comment, nil]).map(&:object).map(&:to_s).first
|
101
|
+
end
|
102
|
+
|
103
|
+
def comment_of_property(property_id)
|
104
|
+
@ttl_graph.query([RDF::URI.new(property_id), RDF::RDFS.comment, nil]).map(&:object).map(&:to_s).first
|
105
|
+
end
|
106
|
+
|
107
|
+
def range_of_property(property_id)
|
108
|
+
@ttl_graph.query([
|
109
|
+
RDF::URI.new(property_id),
|
110
|
+
RDF::URI.new("#{@default_namespace}rangeIncludes"),
|
111
|
+
nil
|
112
|
+
]).map(&:object).map(&:to_s)
|
113
|
+
end
|
114
|
+
|
115
|
+
def domain_of_property(property_id)
|
116
|
+
@ttl_graph.query([
|
117
|
+
RDF::URI.new(property_id),
|
118
|
+
RDF::URI.new("#{@default_namespace}domainIncludes"),
|
119
|
+
nil
|
120
|
+
]).map(&:object).map(&:to_s)
|
121
|
+
end
|
122
|
+
|
123
|
+
def property_required?(property_id)
|
124
|
+
min_cardinalities = @ttl_graph.query([RDF::URI.new(property_id), RDF::OWL.minCardinality,
|
125
|
+
nil]).map(&:object).map(&:to_i)
|
126
|
+
min_cardinalities.empty? ? false : min_cardinalities.first.positive?
|
127
|
+
end
|
128
|
+
|
129
|
+
def property_many?(property_id)
|
130
|
+
max_cardinalities = @ttl_graph.query([RDF::URI.new(property_id), RDF::OWL.maxCardinality,
|
131
|
+
nil]).map(&:object)
|
132
|
+
|
133
|
+
max_cardinalities.empty? ? true : max_cardinalities.map(&:to_i).first > 1
|
134
|
+
end
|
135
|
+
|
136
|
+
def see_also(id)
|
137
|
+
@ttl_graph.query([RDF::URI.new(id), RDF::RDFS.seeAlso, nil]).map(&:object).map(&:to_s).first
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module RDFParsing
|
4
|
+
end
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative 'rdf_parsing/ttl_graph_builder'
|
9
|
+
require_relative 'rdf_parsing/graph_transformer'
|
10
|
+
require_relative 'rdf_parsing/rdf_serializer'
|
11
|
+
require_relative 'rdf_parsing/schemaorg'
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module Chronicle
|
4
|
+
module Schema
|
5
|
+
# Represents a RDF graph as a DAG of types and their properties, built
|
6
|
+
# from a TTL string
|
7
|
+
class SchemaGraph
|
8
|
+
include TSort
|
9
|
+
|
10
|
+
attr_accessor :types, :properties, :datatypes, :version, :default_namespace
|
11
|
+
|
12
|
+
def initialize(default_namespace: 'https://schema.chronicle.app/')
|
13
|
+
@default_namespace = default_namespace
|
14
|
+
@types = []
|
15
|
+
@properties = []
|
16
|
+
@datatypes = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build_from_json(json)
|
20
|
+
graph = new
|
21
|
+
graph.version = json['version']
|
22
|
+
json['types'].each do |type_data|
|
23
|
+
id = graph.id_to_identifier(type_data['id'])
|
24
|
+
graph.add_type(id).tap do |klass|
|
25
|
+
klass.comment = type_data['comment']
|
26
|
+
klass.subtype_ids = type_data['subtype_ids']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
json['properties'].each do |property_data|
|
30
|
+
id = graph.id_to_identifier(property_data['id'])
|
31
|
+
graph.add_property(id).tap do |property|
|
32
|
+
property.comment = property_data['comment']
|
33
|
+
property.domain = property_data['domain']
|
34
|
+
property.range = property_data['range']
|
35
|
+
property.many = property_data['many']
|
36
|
+
property.required = property_data['required']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
graph.build_references!
|
40
|
+
graph
|
41
|
+
end
|
42
|
+
|
43
|
+
def pretty_print(pp)
|
44
|
+
pp.text('SchemaGraph')
|
45
|
+
pp.nest(2) do
|
46
|
+
pp.breakable
|
47
|
+
pp.text("Num types: #{types.size}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
"#<SchemaGraph:#{object_id}>"
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
{
|
57
|
+
version: @version,
|
58
|
+
types: types.map(&:to_h),
|
59
|
+
properties: properties.map(&:to_h)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_references!
|
64
|
+
@types.each do |klass|
|
65
|
+
klass.subtypes = klass.subtype_ids.map { |id| find_type_by_id(id) }
|
66
|
+
klass.supertypes = @types.select { |c| c.subtype_ids.include?(klass.id) }
|
67
|
+
end
|
68
|
+
|
69
|
+
@properties.each do |property|
|
70
|
+
property.domain.each do |type_id|
|
71
|
+
klass = find_type_by_id(type_id)
|
72
|
+
klass.properties << property if klass
|
73
|
+
end
|
74
|
+
|
75
|
+
# prune unknown range values from property
|
76
|
+
property.range = property.range.select do |range|
|
77
|
+
find_type_by_id(range)
|
78
|
+
end
|
79
|
+
|
80
|
+
property.range_types = property.range.map { |id| find_type_by_id(id) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_type_by_id(id)
|
85
|
+
@types.find { |c| c.id == id }
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_type(identifier)
|
89
|
+
@types.find { |c| c.id == identifier_to_uri(identifier) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_property(identifier)
|
93
|
+
@properties.find { |p| p.id == identifier_to_uri(identifier) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_property_by_id(id)
|
97
|
+
@properties.find { |p| p.id == id }
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_type(identifier)
|
101
|
+
find_type(identifier) || add_new_type(identifier)
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_property(identifier)
|
105
|
+
find_property(identifier) || add_new_property(identifier)
|
106
|
+
end
|
107
|
+
|
108
|
+
def id_to_identifier(id)
|
109
|
+
id.gsub(@default_namespace, '')
|
110
|
+
end
|
111
|
+
|
112
|
+
def identifier_to_uri(identifier)
|
113
|
+
"#{@default_namespace}#{identifier}"
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def add_new_type(identifier)
|
119
|
+
new_type = SchemaType.new(identifier_to_uri(identifier)) do |t|
|
120
|
+
t.namespace = @default_namespace
|
121
|
+
end
|
122
|
+
|
123
|
+
@types << new_type unless @types.include?(new_type)
|
124
|
+
new_type
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_new_property(identifier)
|
128
|
+
new_property = SchemaProperty.new(identifier_to_uri(identifier))
|
129
|
+
@properties << new_property unless @properties.include?(new_property)
|
130
|
+
new_property
|
131
|
+
end
|
132
|
+
|
133
|
+
def tsort_each_node(&block)
|
134
|
+
@types.each do |node|
|
135
|
+
block.call(node)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def tsort_each_child(node, &)
|
140
|
+
puts "tsort_each_child called with node: #{node&.id}"
|
141
|
+
node&.subtypes&.each(&)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
# Represents a property in the RDF graph
|
4
|
+
class SchemaProperty
|
5
|
+
attr_reader :id
|
6
|
+
attr_accessor :domain,
|
7
|
+
:range,
|
8
|
+
:comment,
|
9
|
+
:many,
|
10
|
+
:required,
|
11
|
+
:namespace,
|
12
|
+
:range_types, # FIXME
|
13
|
+
:see_also
|
14
|
+
|
15
|
+
def initialize(id)
|
16
|
+
@id = id
|
17
|
+
@domain = []
|
18
|
+
@range = []
|
19
|
+
@many = false
|
20
|
+
@required = false
|
21
|
+
|
22
|
+
yield self if block_given?
|
23
|
+
end
|
24
|
+
|
25
|
+
def pretty_print(pp)
|
26
|
+
pp.text("SchemaProperty: #{id}")
|
27
|
+
pp.nest(2) do
|
28
|
+
pp.breakable
|
29
|
+
pp.text("Range: #{range}")
|
30
|
+
pp.breakable
|
31
|
+
pp.text("Domain: #{domain}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_h
|
36
|
+
output = {
|
37
|
+
id:,
|
38
|
+
domain:,
|
39
|
+
range:,
|
40
|
+
many: @many,
|
41
|
+
required: @required
|
42
|
+
}
|
43
|
+
output[:comment] = @comment if @comment
|
44
|
+
output[:see_also] = @see_also if @see_also
|
45
|
+
output
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
id == other.id
|
50
|
+
end
|
51
|
+
|
52
|
+
def required?
|
53
|
+
@required
|
54
|
+
end
|
55
|
+
|
56
|
+
def many?
|
57
|
+
@many
|
58
|
+
end
|
59
|
+
|
60
|
+
def identifier
|
61
|
+
@id.split('/').last&.to_sym
|
62
|
+
end
|
63
|
+
|
64
|
+
# FIXME
|
65
|
+
def id_snakecase
|
66
|
+
@id.split('/').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
67
|
+
end
|
68
|
+
|
69
|
+
# FIXME: refactor this and the next
|
70
|
+
def range_identifiers
|
71
|
+
range.map do |r|
|
72
|
+
r.split('/').last&.to_sym
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def full_range_identifiers
|
77
|
+
range_types.map(&:descendants).flatten.map { |x| x.id.split('/').last&.to_sym } + range_identifiers
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|