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.
- 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 +19 -16
- 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 +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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3820f9aac588a93691f2c5d82b7f84ee4f5786dc5652baf301ce0dd5df7810aa
|
4
|
+
data.tar.gz: 7f57cb66da0f22510ba66c2ea5783428a6c8c2135ea9c311ddb9e8a1ee896f2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bdf2808fd3a81d4a911825c4b8823bf9bff506a1203d9e282851bcb88058a9440821e762225c322aae3decf2c8490ecd566ffe7d6bcef21e7981da828339dc0
|
7
|
+
data.tar.gz: c22405deb6ceb94be72df6f40b32eb2f7dd950887412461f77f86506486dd7886a037f3ae3baba733db69be1bf104d7aec3a526592ae3ec5ad7955b7e129885b
|
data/.github/workflows/rspec.yml
CHANGED
data/.gitignore
CHANGED
data/.rubocop-plugin.yml
ADDED
data/.rubocop.yml
CHANGED
@@ -1,5 +1,19 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
TargetRubyVersion: 3.1
|
4
|
+
SuggestExtensions: false
|
5
|
+
|
1
6
|
Style/FrozenStringLiteralComment:
|
2
7
|
Enabled: false
|
3
8
|
|
4
|
-
|
5
|
-
Enabled: false
|
9
|
+
Metrics:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Layout/ArgumentAlignment:
|
13
|
+
EnforcedStyle: with_fixed_indentation
|
14
|
+
|
15
|
+
Layout/MultilineMethodCallIndentation:
|
16
|
+
EnforcedStyle: indented
|
17
|
+
|
18
|
+
Gemspec/DevelopmentDependencies:
|
19
|
+
EnforcedStyle: gemspec
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
guard :rspec, cmd:
|
2
|
-
require
|
1
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
2
|
+
require 'guard/rspec/dsl'
|
3
3
|
|
4
4
|
watch(%r{^spec/.+_spec\.rb$})
|
5
5
|
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
6
|
-
watch('spec/spec_helper.rb') {
|
6
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
7
7
|
end
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,88 @@
|
|
1
|
-
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/chronicle-core.svg)](https://badge.fury.io/rb/chronicle-core) [![Ruby](https://github.com/chronicle-app/chronicle-core/actions/workflows/ruby.yml/badge.svg)](https://github.com/chronicle-app/chronicle-core/actions/workflows/ruby.yml) [![Docs](https://img.shields.io/badge/docs-rubydoc.info-blue)](https://www.rubydoc.info/gems/chronicle-core/)
|
2
|
+
|
3
|
+
Core schema-related code for [Chronicle](https://github.com/chronicle-app/)
|
4
|
+
|
5
|
+
## Schema
|
6
|
+
|
7
|
+
Chronicle Schema is a set of types and properties that are used to describe personal digital history. The schema is a derivative of [Schema.org](https://schema.org). For the most part, it is a subset of Schema.org types and properties.
|
8
|
+
|
9
|
+
There are a few top-level properties that are unique to Chronicle:
|
10
|
+
- `source` - the source of the record (e.g. `spotify`, `twitter`, `instagram`)
|
11
|
+
- `sourceId` - the unique identifier of the record in the source system
|
12
|
+
- `slug` - a human-readable identifier for the record (e.g. `bridge-over-troubled-water`)
|
13
|
+
|
14
|
+
To view all valid types and properties in Chronicle Schema, see the `schema/` diretory.
|
15
|
+
|
16
|
+
TODO: generate doc files from schema automatically.
|
17
|
+
|
18
|
+
To update the schema, edit `schema/chronicle_schema_VERSION.rb` and run `rake schema:generate`.
|
19
|
+
|
20
|
+
## Models
|
21
|
+
|
22
|
+
For each valid type in Chronicle Schema, there is a corresponding model class that can instantiated as an immutable Ruby object.
|
23
|
+
|
24
|
+
### Usage
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
require 'chronicle/models'
|
28
|
+
|
29
|
+
song = Chronicle::Models::MusicRecording.new do |r|
|
30
|
+
r.name = 'Bridge Over Troubled Water'
|
31
|
+
r.in_album = 'Aretha Live at Fillmore West'
|
32
|
+
r.by_artist = [Chronicle::Models::MusicGroup.new(name: 'Aretha Franklin')]
|
33
|
+
r.duration = "PT5M49S"
|
34
|
+
r.source = 'spotify'
|
35
|
+
r.source_id = '0e0isrwFsu5W0KJqxkfPpX?si=3d2f08d6d63149bb'
|
36
|
+
end
|
37
|
+
|
38
|
+
puts song.to_json
|
39
|
+
# => {"@type":"MusicRecording","name":"Bridge Over Troubled Water","in_album":"Aretha Live at Fillmore West","by_artist":[{"@type":"MusicGroup","name":"Aretha Franklin"}],"duration":"PT5M49S","source":"spotify","sourceId":"0e0isrwFsu5W0KJqxkfPpX?si=3d2f08d6d63149bb"}
|
40
|
+
```
|
41
|
+
|
42
|
+
## Validation
|
43
|
+
|
44
|
+
chronicle-core provides a validator that checks if a given JSON object conforms to the Chronicle Schema. The validator assumes the JSON is serialized as JSON-LD.
|
45
|
+
|
46
|
+
### Usage
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# TODO
|
50
|
+
```
|
51
|
+
|
52
|
+
## Serialization
|
53
|
+
|
54
|
+
chronicle-core provides the following serializers for records:
|
55
|
+
- JSON
|
56
|
+
- JSONAPI
|
57
|
+
- JSON-LD
|
58
|
+
|
59
|
+
## Development
|
60
|
+
|
61
|
+
To run the tests:
|
62
|
+
|
63
|
+
```bash
|
64
|
+
# Run once
|
65
|
+
bundle exec rspec
|
66
|
+
|
67
|
+
# Run continuously
|
68
|
+
bundle exec guard
|
69
|
+
```
|
70
|
+
|
71
|
+
## Get in touch
|
72
|
+
|
73
|
+
- [@hyfen](https://twitter.com/hyfen) on Twitter
|
74
|
+
- [@hyfen](https://github.com/hyfen) on Github
|
75
|
+
- Email: andrew@hyfen.net
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/chronicle-app/chronicle-core. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
80
|
+
|
81
|
+
## License
|
82
|
+
|
83
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
84
|
+
|
85
|
+
## Code of Conduct
|
86
|
+
|
87
|
+
Everyone interacting in the Chronicle::ETL project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/chronicle-app/chronicle-core/blob/main/CODE_OF_CONDUCT.md).
|
2
88
|
|
3
|
-
Home of some shared code for [Chronicle](https://github.com/chronicle-app/). Currently, this means the models and a validator for Chronicle Schema.
|
data/Rakefile
CHANGED
@@ -1,4 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
4
|
task default: %i[]
|
5
|
+
|
6
|
+
namespace :generate do
|
7
|
+
DEFAULT_SCHEMA_FILE = File.join(File.dirname(__FILE__), 'lib', 'chronicle', 'schema', 'data', 'schema.ttl')
|
8
|
+
|
9
|
+
desc 'Generate everything'
|
10
|
+
task :all, :version do |_t, args|
|
11
|
+
version = args[:version] || latest_version
|
12
|
+
Rake::Task['generate:generate'].invoke(version)
|
13
|
+
Rake::Task['generate:cache_schema'].invoke(version)
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate schema'
|
17
|
+
task :generate, :version do |_t, args|
|
18
|
+
require 'pry'
|
19
|
+
require 'chronicle/schema'
|
20
|
+
require 'chronicle/schema/rdf_parsing'
|
21
|
+
require 'json'
|
22
|
+
|
23
|
+
version = args[:version] || latest_version
|
24
|
+
puts "Generating schema with version: #{version}"
|
25
|
+
|
26
|
+
schema_dsl_file = File.join(File.dirname(__FILE__), 'schema', "chronicle_schema_v#{version}.rb")
|
27
|
+
|
28
|
+
new_graph = Chronicle::Schema::RDFParsing::GraphTransformer.transform_from_file(schema_dsl_file)
|
29
|
+
|
30
|
+
ttl_str = Chronicle::Schema::RDFParsing::RDFSerializer.serialize(new_graph)
|
31
|
+
output_filename = schema_dsl_file.gsub(/\.rb$/, '.ttl')
|
32
|
+
|
33
|
+
File.write(output_filename, ttl_str)
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Cache schema from ttl file'
|
37
|
+
task :cache_schema, :version do |_t, args|
|
38
|
+
require 'chronicle/schema'
|
39
|
+
require 'chronicle/schema/rdf_parsing'
|
40
|
+
|
41
|
+
version = args[:version] || latest_version
|
42
|
+
puts "Caching schema with version: #{version}"
|
43
|
+
|
44
|
+
ttl_file = File.join(File.dirname(__FILE__), 'schema', "chronicle_schema_v#{version}.ttl")
|
45
|
+
graph = Chronicle::Schema::RDFParsing::TTLGraphBuilder.build_from_file(ttl_file, default_namespace: 'https://schema.chronicle.app/')
|
46
|
+
|
47
|
+
output_filename = File.join(File.dirname(__FILE__), 'schema',
|
48
|
+
"chronicle_schema_v#{version}.json")
|
49
|
+
|
50
|
+
output_str = JSON.pretty_generate(graph.to_h)
|
51
|
+
|
52
|
+
File.write(output_filename, output_str)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Highest version schema file in schema/ directory
|
56
|
+
def latest_version
|
57
|
+
schema_files = Dir.glob(File.join(File.dirname(__FILE__), 'schema', 'chronicle_schema_v*.rb'))
|
58
|
+
versions = schema_files.map { |f| f.match(/chronicle_schema_v(.*)\.rb/)&.captures&.first }
|
59
|
+
sorted_versions = versions.sort_by { |version| Gem::Version.new(version) }
|
60
|
+
sorted_versions.last
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
task :generate, :version do |_t, args|
|
65
|
+
Rake::Task['generate:all'].invoke(args[:version])
|
66
|
+
end
|
data/bin/console
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'chronicle/core'
|
6
6
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
9
9
|
|
10
10
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
-
|
12
|
-
|
11
|
+
require 'pry'
|
12
|
+
Pry.start
|
13
13
|
|
14
|
-
require "irb"
|
15
|
-
IRB.start(__FILE__)
|
14
|
+
# require "irb"
|
15
|
+
# IRB.start(__FILE__)
|
data/chronicle-core.gemspec
CHANGED
@@ -1,42 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative 'lib/chronicle/core/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name =
|
6
|
+
spec.name = 'chronicle-core'
|
7
7
|
spec.version = Chronicle::Core::VERSION
|
8
|
-
spec.authors = [
|
9
|
-
spec.email = [
|
8
|
+
spec.authors = ['Andrew Louis']
|
9
|
+
spec.email = ['andrew@hyfen.net']
|
10
10
|
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
15
|
-
spec.required_ruby_version =
|
11
|
+
spec.summary = 'Core libraries for Chronicle'
|
12
|
+
spec.description = 'Core libraries for Chronicle including models and schema definitions.'
|
13
|
+
spec.homepage = 'https://github.com/chronicle-app'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.1'
|
16
16
|
|
17
17
|
if spec.respond_to?(:metadata)
|
18
|
-
spec.metadata['allowed_push_host'] =
|
18
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
19
19
|
|
20
|
-
spec.metadata[
|
21
|
-
spec.metadata[
|
22
|
-
spec.metadata[
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
21
|
+
spec.metadata['source_code_uri'] = 'https://github.com/chronicle-app/chronicle-core'
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/chronicle-app/chronicle-core/releases'
|
23
23
|
else
|
24
|
-
raise
|
25
|
-
|
24
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
25
|
+
'public gem pushes.'
|
26
26
|
end
|
27
27
|
|
28
|
-
spec.files
|
28
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
29
29
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
30
30
|
end
|
31
|
-
spec.bindir =
|
31
|
+
spec.bindir = 'exe'
|
32
32
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
-
spec.require_paths = [
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
spec.
|
38
|
-
|
39
|
-
spec.add_development_dependency
|
40
|
-
spec.add_development_dependency
|
41
|
-
spec.add_development_dependency
|
33
|
+
spec.require_paths = ['lib']
|
34
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
35
|
+
|
36
|
+
spec.add_dependency 'dry-struct', '~> 1.6'
|
37
|
+
spec.add_dependency 'dry-validation', '~> 1.10'
|
38
|
+
|
39
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.7.3'
|
40
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.9'
|
41
|
+
spec.add_development_dependency 'rake', '~> 13'
|
42
|
+
spec.add_development_dependency 'rdf-reasoner', '~> 0.9'
|
43
|
+
spec.add_development_dependency 'rdf-turtle', '~> 3.3'
|
44
|
+
spec.add_development_dependency 'rspec', '~> 3.9'
|
45
|
+
spec.add_development_dependency 'rubocop', '~> 1.57'
|
46
|
+
spec.add_development_dependency 'simplecov', '~> 0.21'
|
47
|
+
spec.add_development_dependency 'sparql', '~> 3.3'
|
42
48
|
end
|
data/lib/chronicle/core.rb
CHANGED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
require 'chronicle/schema/types'
|
3
|
+
|
4
|
+
module Chronicle
|
5
|
+
module Models
|
6
|
+
# The base class for all generated models
|
7
|
+
class Base < Dry::Struct
|
8
|
+
transform_keys(&:to_sym)
|
9
|
+
schema schema.strict
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_reader :type_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def properties
|
16
|
+
# FIXME: think about this more. Does dedupe belong in serialization
|
17
|
+
attributes.except(:dedupe_on)
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: remove this from attributes and just do custom getters/setters
|
21
|
+
attribute(:id, Chronicle::Schema::Types::String.optional.default(nil).meta(many: false, required: false))
|
22
|
+
|
23
|
+
# set of properties to dedupe on
|
24
|
+
# each set of properties is an array of symbols representing the properties to dedupe on
|
25
|
+
# example: [[:slug, :source], [:url]]
|
26
|
+
attribute(
|
27
|
+
:dedupe_on,
|
28
|
+
Chronicle::Schema::Types::Array.of(
|
29
|
+
Chronicle::Schema::Types::Array.of(
|
30
|
+
Chronicle::Schema::Types::Symbol
|
31
|
+
)
|
32
|
+
).optional.default([].freeze).meta(
|
33
|
+
many: false, required: false
|
34
|
+
)
|
35
|
+
)
|
36
|
+
|
37
|
+
def self.new(attributes = {})
|
38
|
+
if block_given?
|
39
|
+
attribute_struct = Struct.new(*schema.map(&:name)).new
|
40
|
+
attribute_struct.dedupe_on = []
|
41
|
+
|
42
|
+
yield(attribute_struct)
|
43
|
+
attributes = attribute_struct.to_h
|
44
|
+
end
|
45
|
+
|
46
|
+
super(attributes)
|
47
|
+
rescue Dry::Struct::Error, NoMethodError => e
|
48
|
+
# TODO: be more clear about the attribute that's invalid
|
49
|
+
raise AttributeError, e.message
|
50
|
+
end
|
51
|
+
|
52
|
+
def meta
|
53
|
+
output = {}
|
54
|
+
output[:dedupe_on] = dedupe_on unless dedupe_on.empty?
|
55
|
+
output
|
56
|
+
end
|
57
|
+
|
58
|
+
def type_id
|
59
|
+
self.class.type_id
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_h
|
63
|
+
super.merge({ type: type_id })
|
64
|
+
.except(:dedupe_on)
|
65
|
+
.compact # TODO: don't know about this one
|
66
|
+
end
|
67
|
+
|
68
|
+
# TODO: exclude dedupe_on from serialization
|
69
|
+
def to_h_flattened
|
70
|
+
require 'chronicle/utils/hash_utils'
|
71
|
+
Chronicle::Utils::HashUtils.flatten_hash(to_h)
|
72
|
+
end
|
73
|
+
|
74
|
+
# FIXME: this isn't working
|
75
|
+
def self.many_cardinality_attributes
|
76
|
+
schema.type.select do |type|
|
77
|
+
type.meta[:many]
|
78
|
+
end.map(&:name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.one_cardinality_attributes
|
82
|
+
schema.type.map(&:name) - many_cardinality_attributes
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.schema_type(types)
|
87
|
+
Chronicle::Schema::Types::Instance(Chronicle::Models::Base).constructor do |input|
|
88
|
+
unless input.type_id && [types].flatten.include?(input.type_id)
|
89
|
+
raise Dry::Types::ConstraintError.new(:type?, input)
|
90
|
+
end
|
91
|
+
|
92
|
+
input
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Models
|
3
|
+
# Utility methods to build models from hash input
|
4
|
+
module Builder
|
5
|
+
# Build a nested a model with nested associated from a hash in json-ld format
|
6
|
+
def build(obj)
|
7
|
+
raise ArgumentError, 'Object must be a hash' unless obj.is_a?(Hash)
|
8
|
+
|
9
|
+
type = obj[:@type]
|
10
|
+
raise ArgumentError, 'Object must have a type' unless type
|
11
|
+
|
12
|
+
model_klass = const_get(type.to_sym)
|
13
|
+
raise ArgumentError, "Unknown model type: #{type}" unless model_klass
|
14
|
+
|
15
|
+
# recursively create nested models
|
16
|
+
# TODO: have a better way of detecting chronicle schema objects
|
17
|
+
begin
|
18
|
+
obj.each do |property, value|
|
19
|
+
if value.is_a?(Hash)
|
20
|
+
obj[property] = build(value)
|
21
|
+
elsif value.is_a?(Array)
|
22
|
+
obj[property] = value.map do |v|
|
23
|
+
v.is_a?(Hash) ? build(v) : v
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
model_klass.new(obj.except(:@type))
|
29
|
+
rescue Chronicle::Models::AttributeError => e
|
30
|
+
raise ArgumentError, e.message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'chronicle/schema'
|
2
|
+
require_relative 'model_factory'
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Chronicle
|
6
|
+
module Models
|
7
|
+
module Generation
|
8
|
+
@models_generated = false
|
9
|
+
@benchmark_enabled = false
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :models_generated, :benchmark_enabled
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.extend ClassMethods
|
17
|
+
base.generate_models unless base.models_generated?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Methods for generating models from the schema
|
21
|
+
module ClassMethods
|
22
|
+
def models_generated?
|
23
|
+
Chronicle::Models::Generation.models_generated
|
24
|
+
end
|
25
|
+
|
26
|
+
def extant_models
|
27
|
+
constants.select do |constant_name|
|
28
|
+
constant = const_get(constant_name)
|
29
|
+
constant.is_a?(Class) && constant.superclass == Chronicle::Models::Base
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_models(graph = nil)
|
34
|
+
graph ||= begin
|
35
|
+
require 'json'
|
36
|
+
schema_path = File.join(File.dirname(__FILE__), '..', '..', '..', 'schema', 'chronicle_schema_v1.json')
|
37
|
+
Chronicle::Schema::SchemaGraph.build_from_json(JSON.parse(File.read(schema_path)))
|
38
|
+
end
|
39
|
+
|
40
|
+
start_time = Time.now
|
41
|
+
graph.types.each do |klass|
|
42
|
+
type_id = graph.id_to_identifier(klass.id)
|
43
|
+
|
44
|
+
new_model_klass = Chronicle::Models::ModelFactory.new(
|
45
|
+
type_id: type_id.to_sym,
|
46
|
+
properties: klass.all_properties
|
47
|
+
).generate
|
48
|
+
|
49
|
+
const_set(type_id, new_model_klass)
|
50
|
+
end
|
51
|
+
end_time = Time.now
|
52
|
+
duration_ms = (end_time - start_time) * 1000
|
53
|
+
handle_benchmark_data(graph.types.length, duration_ms) if Chronicle::Models::Generation.benchmark_enabled
|
54
|
+
|
55
|
+
Chronicle::Models::Generation.models_generated = true
|
56
|
+
end
|
57
|
+
|
58
|
+
def unload_models
|
59
|
+
constants.each do |constant_name|
|
60
|
+
constant = const_get(constant_name)
|
61
|
+
if constant.is_a?(Class) && constant.superclass == Chronicle::Models::Base
|
62
|
+
send(:remove_const,
|
63
|
+
constant_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
Chronicle::Models::Generation.models_generated = false
|
67
|
+
end
|
68
|
+
|
69
|
+
def enable_benchmarking
|
70
|
+
Chronicle::Models::Generation.benchmark_enabled = true
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def handle_benchmark_data(number_of_models, duration)
|
76
|
+
puts "Generated #{number_of_models} models in #{duration.round(2)}ms"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.reset
|
81
|
+
Chronicle::Models::Generation.models_generated = false
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.suppress_model_generation
|
85
|
+
Chronicle::Models::Generation.models_generated = true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Models
|
3
|
+
class ModelFactory
|
4
|
+
attr_reader :id
|
5
|
+
|
6
|
+
def initialize(type_id:, properties: [])
|
7
|
+
@type_id = type_id
|
8
|
+
@properties = properties
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate
|
12
|
+
attribute_info = @properties.map do |property|
|
13
|
+
generate_attribute_info(property)
|
14
|
+
end
|
15
|
+
|
16
|
+
type_id = @type_id
|
17
|
+
|
18
|
+
Class.new(Chronicle::Models::Base) do
|
19
|
+
attribute_info.each do |a|
|
20
|
+
attribute(a[:name], a[:type])
|
21
|
+
end
|
22
|
+
|
23
|
+
@type_id = type_id
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def generate_attribute_info(property)
|
30
|
+
range = build_type(property)
|
31
|
+
type = if property.many?
|
32
|
+
Chronicle::Schema::Types::Array.of(range)
|
33
|
+
else
|
34
|
+
range
|
35
|
+
end
|
36
|
+
|
37
|
+
type = type.optional.default(nil) unless property.required?
|
38
|
+
type = type.meta(required: property.required?, many: property.many?)
|
39
|
+
|
40
|
+
{
|
41
|
+
name: property.id_snakecase.to_sym,
|
42
|
+
type:
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_type(property)
|
47
|
+
type_values = []
|
48
|
+
|
49
|
+
full_range_identifiers = property.full_range_identifiers
|
50
|
+
|
51
|
+
type_values << Chronicle::Schema::Types::Params::Time if full_range_identifiers.include? :DateTime
|
52
|
+
type_values << Chronicle::Schema::Types::String if %i[Text URL Distance Duration Energy
|
53
|
+
Mass].intersect?(full_range_identifiers)
|
54
|
+
type_values << Chronicle::Schema::Types::Params::Integer if full_range_identifiers.include? :Integer
|
55
|
+
type_values << Chronicle::Models.schema_type(full_range_identifiers)
|
56
|
+
|
57
|
+
type_values.reduce do |memo, type|
|
58
|
+
memo | type
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'models/generation'
|
2
|
+
require_relative 'models/base'
|
3
|
+
require_relative 'models/builder'
|
4
|
+
|
5
|
+
module Chronicle
|
6
|
+
module Models
|
7
|
+
class Error < StandardError; end
|
8
|
+
class AttributeError < Error; end
|
9
|
+
|
10
|
+
# Automatically generate models from the default schema
|
11
|
+
# We will do this dynamically whenever 'chronicle/models' is required
|
12
|
+
# until the performance becomes an issue (currently takes about ~10ms)
|
13
|
+
include Chronicle::Models::Generation
|
14
|
+
|
15
|
+
extend Chronicle::Models::Builder
|
16
|
+
end
|
17
|
+
end
|