chronicle-core 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f8a25629577c83f71a66b2451f2858cad759e9ace5547e8f9add4eb11be8771
4
- data.tar.gz: 978d36a75ebdb775b86698afd5f8f7659dc64b9d993b2046f45aaf979ca52549
3
+ metadata.gz: 3820f9aac588a93691f2c5d82b7f84ee4f5786dc5652baf301ce0dd5df7810aa
4
+ data.tar.gz: 7f57cb66da0f22510ba66c2ea5783428a6c8c2135ea9c311ddb9e8a1ee896f2f
5
5
  SHA512:
6
- metadata.gz: 6aff817c68c5d0795389456eafe32aa791f0dd45a3998bb8101a443dc7681b5aaaa719a0de4694c4af001eec98b66b81fffc24f4ed874d089e1951a1ae3d42b6
7
- data.tar.gz: 362d898e479fe85e629abf911e034d4c56236ef95e21b1000f2c839a12fc8d87b028f765bba661856c65bd77020e8f557ee29780df071a644829e26ea2e3d080
6
+ metadata.gz: 8bdf2808fd3a81d4a911825c4b8823bf9bff506a1203d9e282851bcb88058a9440821e762225c322aae3decf2c8490ecd566ffe7d6bcef21e7981da828339dc0
7
+ data.tar.gz: c22405deb6ceb94be72df6f40b32eb2f7dd950887412461f77f86506486dd7886a037f3ae3baba733db69be1bf104d7aec3a526592ae3ec5ad7955b7e129885b
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  strategy:
15
15
  matrix:
16
- ruby_version: ['2.7', '3.0']
16
+ ruby_version: ['3.1']
17
17
  fail-fast: false
18
18
 
19
19
  steps:
data/.gitignore CHANGED
@@ -8,4 +8,6 @@
8
8
  /tmp/
9
9
 
10
10
  Gemfile.lock
11
- .DS_Store
11
+ .DS_Store
12
+
13
+ .ruby-version
@@ -0,0 +1,4 @@
1
+ inherit_from: ./.rubocop.yml
2
+
3
+ Style/Documentation:
4
+ Enabled: false
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
- StringLiterals:
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
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in chronicle-core.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.0"
8
+ gem 'rake', '~> 13.0'
data/Guardfile CHANGED
@@ -1,7 +1,7 @@
1
- guard :rspec, cmd: "bundle exec rspec" do
2
- require "guard/rspec/dsl"
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') { "spec" }
6
+ watch('spec/spec_helper.rb') { 'spec' }
7
7
  end
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2023 Andrew Louis
3
+ Copyright (c) 2024 Andrew Louis
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,3 +1,88 @@
1
- # Chronicle::Core
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 "bundler/gem_tasks"
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 "bundler/setup"
5
- require "chronicle/core"
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
- # require "pry"
12
- # Pry.start
11
+ require 'pry'
12
+ Pry.start
13
13
 
14
- require "irb"
15
- IRB.start(__FILE__)
14
+ # require "irb"
15
+ # IRB.start(__FILE__)
@@ -1,42 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/chronicle/core/version"
3
+ require_relative 'lib/chronicle/core/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "chronicle-core"
6
+ spec.name = 'chronicle-core'
7
7
  spec.version = Chronicle::Core::VERSION
8
- spec.authors = ["Andrew Louis"]
9
- spec.email = ["andrew@hyfen.net"]
8
+ spec.authors = ['Andrew Louis']
9
+ spec.email = ['andrew@hyfen.net']
10
10
 
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 = ">= 2.7"
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'] = "https://rubygems.org"
18
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
19
19
 
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"
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 "RubyGems 2.0 or newer is required to protect against " \
25
- "public gem pushes."
24
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
25
+ 'public gem pushes.'
26
26
  end
27
27
 
28
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
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 = "exe"
31
+ spec.bindir = 'exe'
32
32
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
- spec.require_paths = ["lib"]
34
-
35
- spec.add_dependency "dry-schema", "~> 1.13"
36
-
37
- spec.add_development_dependency "guard-rspec", "~> 4.7.3"
38
- spec.add_development_dependency "pry-byebug", "~> 3.9"
39
- spec.add_development_dependency "rubocop", "~> 1.25.1"
40
- spec.add_development_dependency "rspec", "~> 3.9"
41
- spec.add_development_dependency "simplecov", "~> 0.21"
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
@@ -1,7 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Chronicle
4
2
  module Core
5
- VERSION = "0.2.1"
3
+ VERSION = '0.3.0'.freeze
6
4
  end
7
5
  end
@@ -1,6 +1,4 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "core/version"
1
+ require_relative 'core/version'
4
2
 
5
3
  begin
6
4
  require 'pry'
@@ -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