chronicle-core 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +1 -1
  3. data/.gitignore +3 -1
  4. data/.rubocop-plugin.yml +4 -0
  5. data/.rubocop.yml +16 -2
  6. data/Gemfile +2 -2
  7. data/Guardfile +3 -3
  8. data/LICENSE.txt +1 -1
  9. data/README.md +87 -2
  10. data/Rakefile +63 -1
  11. data/bin/console +6 -6
  12. data/chronicle-core.gemspec +32 -26
  13. data/lib/chronicle/core/version.rb +1 -3
  14. data/lib/chronicle/core.rb +1 -3
  15. data/lib/chronicle/models/base.rb +96 -0
  16. data/lib/chronicle/models/builder.rb +35 -0
  17. data/lib/chronicle/models/generation.rb +89 -0
  18. data/lib/chronicle/models/model_factory.rb +63 -0
  19. data/lib/chronicle/models.rb +17 -0
  20. data/lib/chronicle/schema/rdf_parsing/graph_transformer.rb +122 -0
  21. data/lib/chronicle/schema/rdf_parsing/rdf_serializer.rb +138 -0
  22. data/lib/chronicle/schema/rdf_parsing/schemaorg.rb +50 -0
  23. data/lib/chronicle/schema/rdf_parsing/ttl_graph_builder.rb +142 -0
  24. data/lib/chronicle/schema/rdf_parsing.rb +11 -0
  25. data/lib/chronicle/schema/schema_graph.rb +145 -0
  26. data/lib/chronicle/schema/schema_property.rb +81 -0
  27. data/lib/chronicle/schema/schema_type.rb +110 -0
  28. data/lib/chronicle/schema/types.rb +9 -0
  29. data/lib/chronicle/schema/validation/base_contract.rb +22 -0
  30. data/lib/chronicle/schema/validation/contract_factory.rb +133 -0
  31. data/lib/chronicle/schema/validation/edge_validator.rb +53 -0
  32. data/lib/chronicle/schema/validation/generation.rb +29 -0
  33. data/lib/chronicle/schema/validation/validator.rb +23 -0
  34. data/lib/chronicle/schema/validation.rb +41 -0
  35. data/lib/chronicle/schema.rb +9 -2
  36. data/lib/chronicle/serialization/hash_serializer.rb +5 -11
  37. data/lib/chronicle/serialization/jsonapi_serializer.rb +41 -26
  38. data/lib/chronicle/serialization/jsonld_serializer.rb +38 -0
  39. data/lib/chronicle/serialization/record.rb +90 -0
  40. data/lib/chronicle/serialization/serializer.rb +31 -18
  41. data/lib/chronicle/serialization.rb +6 -4
  42. data/lib/chronicle/utils/hash_utils.rb +19 -16
  43. data/schema/chronicle_schema_v1.json +1008 -0
  44. data/schema/chronicle_schema_v1.rb +147 -0
  45. data/schema/chronicle_schema_v1.ttl +562 -0
  46. metadata +107 -15
  47. data/lib/chronicle/schema/activity.rb +0 -5
  48. data/lib/chronicle/schema/base.rb +0 -79
  49. data/lib/chronicle/schema/entity.rb +0 -5
  50. data/lib/chronicle/schema/raw.rb +0 -9
  51. 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,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