chronicle-core 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +65 -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 +47 -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 +19 -16
- data/schema/chronicle_schema_v1.json +1283 -0
- data/schema/chronicle_schema_v1.rb +183 -0
- data/schema/chronicle_schema_v1.ttl +720 -0
- metadata +107 -15
- data/lib/chronicle/schema/activity.rb +0 -5
- data/lib/chronicle/schema/base.rb +0 -79
- data/lib/chronicle/schema/entity.rb +0 -5
- data/lib/chronicle/schema/raw.rb +0 -9
- data/lib/chronicle/schema/validator.rb +0 -55
@@ -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,47 @@
|
|
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
|
+
"https://schema.org/version/#{version}/schemaorg-current-https.ttl"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
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
|