laforge 0.0.1.alpha
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +20 -0
- data/Readme.md +75 -0
- data/app/models/laforge/data_entry.rb +31 -0
- data/app/models/laforge/data_source.rb +7 -0
- data/lib/laforge.rb +5 -0
- data/lib/laforge/activerecord.rb +104 -0
- data/lib/laforge/version.rb +3 -0
- data/lib/tasks/laforge_tasks.rake +39 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1b541939a70c5422d9a39a36cb396eb371a4d9aa9068117e3ce8d0a23293d3bf
|
4
|
+
data.tar.gz: 4c2158ddb780dce754fb05b4776f409f30140f07b4b9ec4880068a1dd079f82d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ccfb3d1db0a662a1f7c2092f46f67bad2b9726661fbe240be0a0f24b9a1a50f56d7e1d9ac4287099bf4d1bc47fff6d17534b15e7c0364db8dcfaf1fa5041bedd
|
7
|
+
data.tar.gz: 29773c22f931db03b2359b044acd8d31e705fe14d2eec77388527e07f690e0ed815e10d057a4a40c12ec4383f7795596261d8e30f7d74e7a14503f45079aa893
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Nicholas Jakobsen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
APP_RAKEFILE = File.expand_path("../spec/internal/Rakefile", __FILE__)
|
8
|
+
load 'rails/tasks/engine.rake'
|
9
|
+
|
10
|
+
load 'rails/tasks/statistics.rake'
|
11
|
+
|
12
|
+
Bundler::GemHelper.install_tasks
|
13
|
+
|
14
|
+
|
15
|
+
# Add Rspec tasks
|
16
|
+
require 'rspec/core/rake_task'
|
17
|
+
|
18
|
+
RSpec::Core::RakeTask.new(:spec)
|
19
|
+
|
20
|
+
task :default => :spec
|
data/Readme.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# LaForge [](https://badge.fury.io/rb/laforge)
|
2
|
+
|
3
|
+
By [Combinaut](http://www.combinaut.com).
|
4
|
+
|
5
|
+
**LaForge** is a gem that makes it easy to build records using data from several data sources. It aims to facilitate
|
6
|
+
management of which data sources a record is assembled from, and to perform the actual data assembly in order to output
|
7
|
+
a record.
|
8
|
+
|
9
|
+
Key features:
|
10
|
+
|
11
|
+
- Allows published content to be edited without those changes immediately being seen by visitors
|
12
|
+
- Can selectively update content without needing to sync the entire database with production
|
13
|
+
|
14
|
+
## Setup
|
15
|
+
1. Add **Laforge** to your Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'laforge', github: 'combinaut/laforge'
|
19
|
+
```
|
20
|
+
|
21
|
+
2. Add laforge to your model.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# In your model
|
25
|
+
class MyModel < ApplicationModel
|
26
|
+
laforged
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
1. Create some sources and prioritize them, giving higher priority to sources whose attributes should override those
|
33
|
+
same attributes from lower priority sources.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
source1 = LaForge::DataSource.create(name: 'Encyclopedia Britannica', priority: 2)
|
37
|
+
source2 = LaForge::DataSource.create(name: 'Wikipedia', priority: 1)
|
38
|
+
```
|
39
|
+
|
40
|
+
2. Build a new model, or update an existing one with data from any of your sources.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
foo = MyModel.new
|
44
|
+
foo.record_data_entries({ height: 20, width: 5 }, source1)
|
45
|
+
foo.record_data_entries({ height: 19, name: 'El Capitan' }, source2)
|
46
|
+
foo.forge! # => Saves the model with the attributes { height: 20, width: 5, name: 'El Capitan' }
|
47
|
+
```
|
48
|
+
|
49
|
+
### Overriding Priority
|
50
|
+
|
51
|
+
The priority of a data entry can be customized to override the priority inherited from the data source.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
foo = MyModel.new
|
55
|
+
foo.record_data_entries({ height: 20, width: 5 }, source1)
|
56
|
+
foo.record_data_entries({ height: 19 }, source2, priority: 3) # Override the priority inherited from the data source
|
57
|
+
foo.forge! # => Saves the model with the attributes { height: 19, width: 5 }
|
58
|
+
```
|
59
|
+
|
60
|
+
### Querying
|
61
|
+
```ruby
|
62
|
+
MyModel.from_data_source(source1) # => All records from source1
|
63
|
+
MyModel.without_data_source # => All records without any data source
|
64
|
+
MyModel.without_data_source(source1) # => All records not from source1
|
65
|
+
|
66
|
+
MyModel.with_attribute_from_data_source(:height, source1) # => All records with a recorded height attribute from source1
|
67
|
+
MyModel.with_attribute_from_data_source([:height, :width], source1) # => All records with a recorded height or width attribute from source1
|
68
|
+
MyModel.with_attribute_from_data_source(:height, source1).with_attribute_from_data_source(:width, source1) # => All records with a recorded height and width attribute from source1
|
69
|
+
|
70
|
+
MyModel.without_attribute_from_data_source(:height) # => All records without a recorded height attribute from any source
|
71
|
+
MyModel.without_attribute_from_data_source(:height, source1) # => All records without a recorded height attribute from source1
|
72
|
+
MyModel.without_attribute_from_data_source([:height, :width], source1) # => All records missing a recorded height or missing a recorded width from source1
|
73
|
+
MyModel.without_attribute_from_data_source(:height, source1).without_attribute_from_data_source(:width, source1) # => All records without both a recorded height and width attribute from source1
|
74
|
+
|
75
|
+
```
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# create_table :laforge_data_entries do |t|
|
2
|
+
# t.belongs_to :record, polymorphic: true, null: false, index: false
|
3
|
+
# t.string :attribute, null: false
|
4
|
+
# t.belongs_to :source, null: false, index: false
|
5
|
+
# t.text :value
|
6
|
+
# t.integer :priority
|
7
|
+
# t.timestamps null: false
|
8
|
+
# end
|
9
|
+
# add_index :laforge_data_entries, [:record_id, :record_type, :attribute, :source_id]
|
10
|
+
|
11
|
+
|
12
|
+
module LaForge
|
13
|
+
class DataEntry < ActiveRecord::Base
|
14
|
+
belongs_to :record, polymorphic: true
|
15
|
+
belongs_to :source, class_name: 'LaForge::DataSource'
|
16
|
+
|
17
|
+
delegate :priority, to: :source, prefix: true, allow_nil: true
|
18
|
+
|
19
|
+
before_save :normalize_value
|
20
|
+
|
21
|
+
def priority_with_fallback
|
22
|
+
priority || source_priority
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def normalize_value
|
28
|
+
self.value = nil unless value.present?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/laforge.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# TODO: create generator to generate tables
|
2
|
+
module LaForge
|
3
|
+
module ActiveRecord
|
4
|
+
def self.laforged
|
5
|
+
has_many :data_entries, dependent: :delete_all, class_name: 'LaForge::DataEntry'
|
6
|
+
has_many :data_sources, through: :source_data_entries, class_name: 'LaForge::DataSource'
|
7
|
+
|
8
|
+
scope :from_data_source, ->(source) { joins(:data_entries).merge(DataEntry.from_source(source)) }
|
9
|
+
scope :with_attribute_from_data_source, ->(attribute, source) { joins(:data_entries).merge(DataEntry.for_attribute(attribute).from_source(source)) }
|
10
|
+
scope :without_data_source, ->(source = nil) { source.nil? ? left_outer_joins(:data_entries).where(data_entries: { source_id: nil }) : left_outer_joins(:data_entries).where.not(data_entries: { source_id: source }) }
|
11
|
+
scope :without_attribute_from_data_source, ->(attribute, source = nil) { source.nil? ? left_outer_joins(:data_entries).merge(DataEntry.for_attribute(attribute)).where(data_entries: { source_id: nil }) : left_outer_joins(:data_entries).merge(DataEntry.for_attribute(attribute)).where.not(data_entries: { source_id: source }) }
|
12
|
+
|
13
|
+
extend ClassMethods
|
14
|
+
include InstanceMethods
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# Create a record from the given data entries
|
20
|
+
def forge!(data_entries)
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
# Assign attributes and save the record based on the data entries
|
27
|
+
def forge!(**forge_options)
|
28
|
+
forge(**forge_options)
|
29
|
+
save!
|
30
|
+
end
|
31
|
+
|
32
|
+
# Assign attributes based on the data entries
|
33
|
+
def forge(**forge_options)
|
34
|
+
self.attributes = forge_attributes(**forge_options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a hash of the attribute changes between the saved record and the data entries
|
38
|
+
def forge_diff(**forge_options)
|
39
|
+
diff = {}
|
40
|
+
forge_attrs = forge_attributes(**forge_options)
|
41
|
+
record_attrs = slice(*forge_attrs.keys)
|
42
|
+
|
43
|
+
record_attrs.each do |key, value|
|
44
|
+
diff[key] = [value, forge_attrs[key]] if forge_attrs[key] != value
|
45
|
+
end
|
46
|
+
|
47
|
+
return diff
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a hash of the attributes generated from the record's the data entries
|
51
|
+
# Optionally pass `attributes` to limit data entries used in the calculation to only those for the given attributes
|
52
|
+
# Optionally pass `sources` to limit data entries used in the calculation to only those from the given sources
|
53
|
+
def forge_attributes(attributes: nil, sources: nil)
|
54
|
+
forged_attrs = {}
|
55
|
+
filter_loaded_data_entries(attributes: attributes, sources: sources, present: true).sort_by(&:priority_with_fallback).reverse.uniq_by(&:attribute).each do |data_entry|
|
56
|
+
forged_attrs[data_entry.attribute] = data_entry.value
|
57
|
+
end
|
58
|
+
|
59
|
+
return forged_attrs
|
60
|
+
end
|
61
|
+
|
62
|
+
# Record several pieces of information from the same source.
|
63
|
+
def record_data_entries(attributes_hash, source, **data_entry_options)
|
64
|
+
attributes_hash.each do |attribute, value|
|
65
|
+
record_data_entry(attribute, value, source, **data_entry_options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Record a single piece of information from a source.
|
70
|
+
# Optionally pass a custom priority for that attribute and source at the same time.
|
71
|
+
# Optionally pass `replace: false` to leave the existing entry for the attribute and source instead of deleting it
|
72
|
+
def record_data_entry(attribute, value, source, priority: nil, replace: true)
|
73
|
+
data_entries.destroy(*filter_loaded_data_entries(attributes: attribute, sources: source)) if replace
|
74
|
+
data_entries << DataEntry.new(attribute: attribute, value: value, source: source, priority: priority)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set and save the priority of a source for the source of a single attribute
|
78
|
+
def update_attribute_source_priority(attribute, source, priority)
|
79
|
+
filter_loaded_data_entries(attributes: attribute, sources: source).each {|data_entry| data_entry.update(priority: priority) }
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Returns a list of the entries matching the filters
|
85
|
+
def filter_loaded_data_entries(attributes: nil, sources: nil, present: nil)
|
86
|
+
list = data_entries.to_a
|
87
|
+
|
88
|
+
unless attribute.nil?
|
89
|
+
attributes = Array.wrap(attributes).map(&:to_s)
|
90
|
+
list.select! {|data_entry| attributes.include?(data_entry.attribute) }
|
91
|
+
end
|
92
|
+
|
93
|
+
unless sources.nil?
|
94
|
+
sources = Array.wrap(sources).map {|source| source.id if source.is_a?(ActiveRecord::Base) }
|
95
|
+
list.select! {|data_entry| sources.include?(data_entry.source_id) }
|
96
|
+
end
|
97
|
+
|
98
|
+
list.select!(&:present?) if present == true
|
99
|
+
list.reject!(&:present?) if present == false
|
100
|
+
|
101
|
+
return list
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
namespace :laforge do
|
2
|
+
desc "Polls the commit entries table for changes to sync to production"
|
3
|
+
task :auto_sync, [:delay] => :environment do |t, args|
|
4
|
+
delay = args[:delay].present? ? args[:delay].to_i : 5.seconds
|
5
|
+
LaForge::Staging::Synchronizer.auto_sync(delay)
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Syncs records that don't need confirmation to production"
|
9
|
+
task :sync, [:limit] => :environment do |t, args|
|
10
|
+
limit = args[:limit].present? ? args[:limit].to_i : nil
|
11
|
+
LaForge::Staging::Synchronizer.sync(limit)
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Syncs all records to production, including those that require confirmation"
|
15
|
+
task :sync_all => :environment do
|
16
|
+
LaForge::Staging::Synchronizer.sync_all
|
17
|
+
end
|
18
|
+
|
19
|
+
# Enhance the regular tasks to run on both staging and production databases
|
20
|
+
def rake_both_databases(task, laforge_task = task.gsub(':','_'))
|
21
|
+
task(laforge_task => :environment) do
|
22
|
+
LaForge::Database.each do |connection_name|
|
23
|
+
LaForge::Connection.with_production_writes do
|
24
|
+
puts "#{connection_name}"
|
25
|
+
Rake::Task[task].reenable
|
26
|
+
Rake::Task[task].invoke
|
27
|
+
end
|
28
|
+
end
|
29
|
+
Rake::Task[task].clear
|
30
|
+
end
|
31
|
+
|
32
|
+
# Enhance the original task to run the laforge_task as a prerequisite
|
33
|
+
Rake::Task[task].enhance(["laforge:#{laforge_task}"])
|
34
|
+
end
|
35
|
+
|
36
|
+
rake_both_databases('db:migrate')
|
37
|
+
rake_both_databases('db:rollback')
|
38
|
+
rake_both_databases('db:test:load_structure')
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: laforge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.alpha
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicholas Jakobsen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: combustion
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mysql2
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.4.10
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.4.10
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.7'
|
69
|
+
description: LaForge is a gem that makes it easy to build records using data from
|
70
|
+
several data sources. It aims to facilitate management of which data sources a record
|
71
|
+
is assembled from, and to perform the actual data assembly in order to output a
|
72
|
+
record.
|
73
|
+
email:
|
74
|
+
- nicholas@combinaut.ca
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- MIT-LICENSE
|
80
|
+
- Rakefile
|
81
|
+
- Readme.md
|
82
|
+
- app/models/laforge/data_entry.rb
|
83
|
+
- app/models/laforge/data_source.rb
|
84
|
+
- lib/laforge.rb
|
85
|
+
- lib/laforge/activerecord.rb
|
86
|
+
- lib/laforge/version.rb
|
87
|
+
- lib/tasks/laforge_tasks.rake
|
88
|
+
homepage: https://github.com/combinaut/laforge
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">"
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 1.3.1
|
106
|
+
requirements: []
|
107
|
+
rubygems_version: 3.0.8
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: LaForge is a gem that makes it easy to build records using data from several
|
111
|
+
data sources
|
112
|
+
test_files: []
|