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.
Files changed (52) 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 +26 -0
  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 +108 -16
  47. data/lib/chronicle/schema/activity.rb +0 -5
  48. data/lib/chronicle/schema/base.rb +0 -64
  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
  52. 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