plumber 0.0.1
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.
- data/.gitignore +5 -0
- data/Gemfile +8 -0
- data/README.markdown +3 -0
- data/Rakefile +1 -0
- data/lib/plumber.rb +49 -0
- data/lib/plumber/context.rb +75 -0
- data/lib/plumber/version.rb +3 -0
- data/plumber.gemspec +24 -0
- metadata +65 -0
data/Gemfile
ADDED
data/README.markdown
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/plumber.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require "plumber/version"
|
2
|
+
require "plumber/context"
|
3
|
+
|
4
|
+
require "active_record"
|
5
|
+
|
6
|
+
# Provides functionality for creating and updating ActiveRecord model objects. Requires Rails 3+.
|
7
|
+
class Plumber
|
8
|
+
# Create a new instance which will act on the given model and log to the given logger.
|
9
|
+
def initialize(model, logger)
|
10
|
+
@model = model
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def import
|
15
|
+
context = Plumber::Context.new(@model.scoped)
|
16
|
+
|
17
|
+
yield context
|
18
|
+
|
19
|
+
operation = if context.record.new_record?
|
20
|
+
"Create"
|
21
|
+
else
|
22
|
+
"Update"
|
23
|
+
end
|
24
|
+
|
25
|
+
changed = context.record.changed
|
26
|
+
|
27
|
+
begin
|
28
|
+
ActiveRecord::Base.transaction do
|
29
|
+
if context.record.save!
|
30
|
+
if changed
|
31
|
+
@logger.info("#{operation} #{context.record.class} #{context.record.id}.")
|
32
|
+
else
|
33
|
+
@logger.debug("#{context.record.class} #{context.record.id} not changed.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
rescue ActiveRecord::RecordInvalid => e
|
38
|
+
prefix = "#{e.record.class} #{e.record.id}".strip
|
39
|
+
|
40
|
+
@logger.warn("#{prefix}: #{operation} failed - #{e.record.errors.size} validation errors.")
|
41
|
+
|
42
|
+
context.record.errors.full_messages.each do |message|
|
43
|
+
@logger.warn("#{prefix}: #{message}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context.record
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class Plumber
|
2
|
+
class Context
|
3
|
+
MAX_ASSOCIATION_COUNT = 50
|
4
|
+
|
5
|
+
attr_reader :scope, :conditions, :mapping, :associations
|
6
|
+
|
7
|
+
def initialize(scope, parent = nil)
|
8
|
+
@scope = scope
|
9
|
+
@parent = parent
|
10
|
+
@conditions = {}
|
11
|
+
@mapping = {}
|
12
|
+
@associations = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def conditions(hash)
|
16
|
+
@conditions = hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def mapping(hash)
|
20
|
+
@mapping = hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def association(name)
|
24
|
+
scope = record.send(name)
|
25
|
+
context = self.class.new(scope, self)
|
26
|
+
|
27
|
+
yield context
|
28
|
+
|
29
|
+
@associations << context
|
30
|
+
|
31
|
+
# Return the record defined in the yielded context:
|
32
|
+
context.record
|
33
|
+
end
|
34
|
+
|
35
|
+
def record
|
36
|
+
if @record.nil?
|
37
|
+
@record = if @parent.nil?
|
38
|
+
# If this is a top-level context, use a database query to look up an
|
39
|
+
# existing record that matches the conditions supplied.
|
40
|
+
@scope.where(@conditions).first
|
41
|
+
else
|
42
|
+
# Because ActiveRecord (currently) doesn't have an "Identity Map" feature,
|
43
|
+
# unfortunately for associated records we can't run a DB query to find
|
44
|
+
# existing records.
|
45
|
+
#
|
46
|
+
# Instead we load all records in the scope and search for one matching
|
47
|
+
# the conditions supplied. This is why we check the scope size and raise
|
48
|
+
# an exception if it's too large - for situations where there are
|
49
|
+
# potentially large numbers of associated records, a separate Importer
|
50
|
+
# instance should be defined for handling those records.
|
51
|
+
if @scope.count > MAX_ASSOCIATION_COUNT
|
52
|
+
message = "%s scope contains too many records (%s)" % [
|
53
|
+
@scope.new.class, @scope.count
|
54
|
+
]
|
55
|
+
|
56
|
+
raise RuntimeError, message
|
57
|
+
end
|
58
|
+
|
59
|
+
@scope.detect do |record|
|
60
|
+
@conditions.all? { |k, v| record.send(k) == v }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# If an existing record hasn't been found in the scope, build a new one:
|
65
|
+
@record ||= @scope.build
|
66
|
+
|
67
|
+
@conditions.merge(@mapping).each do |key, value|
|
68
|
+
@record.send("#{key}=", value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@record
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/plumber.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "plumber/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "plumber"
|
7
|
+
s.version = Plumber::VERSION
|
8
|
+
s.authors = ["Nick Dainty"]
|
9
|
+
s.email = ["nick@npad.co.uk"]
|
10
|
+
s.homepage = "https://github.com/nickpad/plumber"
|
11
|
+
s.summary = %q{Connect anything to an ActiveRecord model}
|
12
|
+
s.description = %q{Plumber is a library for creating and updating ActiveRecord models from external data}
|
13
|
+
|
14
|
+
s.rubyforge_project = "plumber"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
s.add_runtime_dependency "activerecord", ">= 3.0.0"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: plumber
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nick Dainty
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: &70177976806580 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70177976806580
|
25
|
+
description: Plumber is a library for creating and updating ActiveRecord models from
|
26
|
+
external data
|
27
|
+
email:
|
28
|
+
- nick@npad.co.uk
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- .gitignore
|
34
|
+
- Gemfile
|
35
|
+
- README.markdown
|
36
|
+
- Rakefile
|
37
|
+
- lib/plumber.rb
|
38
|
+
- lib/plumber/context.rb
|
39
|
+
- lib/plumber/version.rb
|
40
|
+
- plumber.gemspec
|
41
|
+
homepage: https://github.com/nickpad/plumber
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project: plumber
|
61
|
+
rubygems_version: 1.8.11
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Connect anything to an ActiveRecord model
|
65
|
+
test_files: []
|