spigot 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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +221 -0
- data/Rakefile +1 -0
- data/examples/.DS_Store +0 -0
- data/examples/active_record.rb +57 -0
- data/examples/model.rb +34 -0
- data/lib/.DS_Store +0 -0
- data/lib/spigot.rb +32 -0
- data/lib/spigot/active_record.rb +106 -0
- data/lib/spigot/base.rb +43 -0
- data/lib/spigot/config/.DS_Store +0 -0
- data/lib/spigot/config/spigot/github.yml +7 -0
- data/lib/spigot/configuration.rb +28 -0
- data/lib/spigot/errors.rb +8 -0
- data/lib/spigot/proxy.rb +40 -0
- data/lib/spigot/record.rb +71 -0
- data/lib/spigot/translator.rb +128 -0
- data/lib/spigot/version.rb +3 -0
- data/script/console.rb +31 -0
- data/spec/.DS_Store +0 -0
- data/spec/fixtures/.DS_Store +0 -0
- data/spec/fixtures/api_data.rb +25 -0
- data/spec/fixtures/mappings/active_user_map.rb +42 -0
- data/spec/fixtures/mappings/mappings.rb +7 -0
- data/spec/fixtures/mappings/post_map.rb +22 -0
- data/spec/fixtures/mappings/user_map.rb +33 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/spigot/active_record_spec.rb +95 -0
- data/spec/spigot/base_spec.rb +7 -0
- data/spec/spigot/configuration_spec.rb +60 -0
- data/spec/spigot/factory_spec.rb +5 -0
- data/spec/spigot/translator_spec.rb +165 -0
- data/spec/support/active_record.rb +15 -0
- data/spigot.gemspec +28 -0
- metadata +193 -0
data/lib/spigot/base.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
module Spigot
|
3
|
+
module Base
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:extend, self::ClassMethods)
|
6
|
+
base.send(:extend, Spigot::ActiveRecord::ClassMethods) if active_record?(base)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# #self.new_by_api(service, api_data)
|
11
|
+
# Instantiate a new object mapping the api data to the calling object's attributes
|
12
|
+
#
|
13
|
+
# @param service [Symbol] Service which will be doing the translating. Must have a corresponding yaml file
|
14
|
+
# @param api_data [Hash] The data as received from the remote api, unformatted.
|
15
|
+
def new_by_api(service, api_data)
|
16
|
+
Record.instantiate(self, formatted_api_data(service, api_data))
|
17
|
+
end
|
18
|
+
|
19
|
+
# #self.formatted_api_data(service, api_data)
|
20
|
+
# Create a Spigot::Translator for the given service and return the formatted data.
|
21
|
+
#
|
22
|
+
# @param service [Symbol] Service which will be doing the translating. Must have a corresponding yaml file
|
23
|
+
# @param api_data [Hash] The data as received from the remote api, unformatted.
|
24
|
+
def formatted_api_data(service, api_data)
|
25
|
+
Translator.new(service, self, api_data).format
|
26
|
+
end
|
27
|
+
|
28
|
+
# #self.spigot
|
29
|
+
# Return a Spigot::Proxy that provides accessor methods to the spigot library
|
30
|
+
#
|
31
|
+
# @param service [Symbol] Service which pertains to the data being processed on the implementation
|
32
|
+
def spigot(service)
|
33
|
+
Spigot::Proxy.new(service, self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.active_record?(klass)
|
40
|
+
defined?(ActiveRecord) && klass < ::ActiveRecord::Base
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
Binary file
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Spigot
|
4
|
+
class Configuration
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
attr_accessor :path, :translations, :options_key, :logger
|
8
|
+
|
9
|
+
@@defaults = {
|
10
|
+
path: 'config/spigot',
|
11
|
+
translations: nil,
|
12
|
+
options_key: 'spigot',
|
13
|
+
logger: nil
|
14
|
+
}
|
15
|
+
|
16
|
+
def self.defaults
|
17
|
+
@@defaults
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
reset
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset
|
25
|
+
@@defaults.each_pair{|k,v| self.send("#{k}=",v)}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Spigot
|
2
|
+
class MissingServiceError < StandardError; end
|
3
|
+
class InvalidServiceError < StandardError; end
|
4
|
+
class MissingResourceError < StandardError; end
|
5
|
+
class InvalidResourceError < StandardError; end
|
6
|
+
class InvalidDataError < StandardError; end
|
7
|
+
class InvalidSchemaError < StandardError; end
|
8
|
+
end
|
data/lib/spigot/proxy.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Spigot
|
2
|
+
class Proxy
|
3
|
+
|
4
|
+
## Proxy
|
5
|
+
#
|
6
|
+
# Spigot::Proxy provides accessor methods used by the implementation
|
7
|
+
# that could be useful for development or custom behavior
|
8
|
+
|
9
|
+
attr_reader :resource, :service
|
10
|
+
|
11
|
+
## #initialize(resource)
|
12
|
+
# Method to initialize a proxy.
|
13
|
+
#
|
14
|
+
# @param service [String] This is the service that dictates the proxy.
|
15
|
+
# @param resource [Object] This is the class implementing the proxy.
|
16
|
+
def initialize(service, resource)
|
17
|
+
@service = service
|
18
|
+
@resource = resource
|
19
|
+
end
|
20
|
+
|
21
|
+
## #translator
|
22
|
+
# Instantiate a Spigot::Translator object with the contextual service and resource
|
23
|
+
def translator
|
24
|
+
Translator.new(service, resource)
|
25
|
+
end
|
26
|
+
|
27
|
+
## #map
|
28
|
+
# Return a hash of the data map the current translator is using
|
29
|
+
def map
|
30
|
+
translator.mapping.reject{|k,v| k == 'spigot'}
|
31
|
+
end
|
32
|
+
|
33
|
+
## #options
|
34
|
+
# Return a hash of any service specific options for this translator. `Spigot.config` not included
|
35
|
+
def options
|
36
|
+
translator.mapping['spigot'] || {}
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Spigot
|
2
|
+
class Record
|
3
|
+
|
4
|
+
## Record
|
5
|
+
#
|
6
|
+
# Spigot::Record is responsible for the instantiation and creation
|
7
|
+
# of objects with the formatted data received from Spigot::Translator.
|
8
|
+
|
9
|
+
attr_reader :resource, :record, :data
|
10
|
+
|
11
|
+
# #initialize(resource, data)
|
12
|
+
# Method to initialize a record.
|
13
|
+
#
|
14
|
+
# @param resource [Object] This is the class implementing the record.
|
15
|
+
# @param data [Hash] The already formatted data used to produce the object.
|
16
|
+
# @param record [Object] Optional record of `resource` type already in database.
|
17
|
+
def initialize(resource, data, record=nil)
|
18
|
+
@resource = resource
|
19
|
+
@data = data
|
20
|
+
@record = record
|
21
|
+
end
|
22
|
+
|
23
|
+
## #instantiate(resource, data)
|
24
|
+
# Executes the initialize method on the implementing resource with formatted data.
|
25
|
+
#
|
26
|
+
# @param resource [Object] This is the class implementing the record.
|
27
|
+
# @param data [Hash] The already formatted data used to produce the object.
|
28
|
+
def self.instantiate(resource, data)
|
29
|
+
new(resource, data).instantiate
|
30
|
+
end
|
31
|
+
|
32
|
+
## #create(resource, data)
|
33
|
+
# Executes the create method on the implementing resource with formatted data.
|
34
|
+
#
|
35
|
+
# @param resource [Object] This is the class implementing the record.
|
36
|
+
# @param data [Hash] The already formatted data used to produce the object.
|
37
|
+
def self.create(resource, data)
|
38
|
+
new(resource, data).create
|
39
|
+
end
|
40
|
+
|
41
|
+
## #update(resource, data)
|
42
|
+
# Assigns the formatted data to the resource and saves.
|
43
|
+
#
|
44
|
+
# @param resource [Object] This is the class implementing the record.
|
45
|
+
# @param record [Object] Optional record of `resource` type already in database.
|
46
|
+
# @param data [Hash] The already formatted data used to produce the object.
|
47
|
+
def self.update(resource, record, data)
|
48
|
+
new(resource, data, record).update
|
49
|
+
end
|
50
|
+
|
51
|
+
## #instantiate
|
52
|
+
# Executes the initialize method on the implementing resource with formatted data.
|
53
|
+
def instantiate
|
54
|
+
resource.new(data)
|
55
|
+
end
|
56
|
+
|
57
|
+
## #create
|
58
|
+
# Executes the create method on the implementing resource with formatted data.
|
59
|
+
def create
|
60
|
+
resource.create(data)
|
61
|
+
end
|
62
|
+
|
63
|
+
## #update
|
64
|
+
# Assigns the formatted data to the resource and saves.
|
65
|
+
def update
|
66
|
+
record.assign_attributes(data)
|
67
|
+
record.save! if record.changed?
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Spigot
|
4
|
+
class Translator
|
5
|
+
|
6
|
+
## Translator
|
7
|
+
#
|
8
|
+
# Translator reads the yaml file in the spigot config directory for
|
9
|
+
# a given service. It looks up the key for the resource class name
|
10
|
+
# passed in, then translates the data received into the format described
|
11
|
+
# in the yaml file for that resource.
|
12
|
+
#
|
13
|
+
# Relevant Configuration:
|
14
|
+
# config.options_key => The key which the Translator uses to configure a resource mapping.
|
15
|
+
# config.path => The path which the Translator will look in to find the mappings.
|
16
|
+
# config.translations => A hash that overrides any mappings found in the `path` directory.
|
17
|
+
|
18
|
+
attr_reader :service, :resource
|
19
|
+
attr_accessor :data
|
20
|
+
|
21
|
+
OPTIONS = %w(primary_key foreign_key conditions).freeze
|
22
|
+
|
23
|
+
## #initialize(service, resource, data)
|
24
|
+
# Method to initialize a translator.
|
25
|
+
#
|
26
|
+
# @param service [Symbol] Service doing the translating. Must have a corresponding yaml file.
|
27
|
+
# @param resource [Object] This is the class using the translator.
|
28
|
+
# @param data [Hash] Data in the format received by the api (optional).
|
29
|
+
def initialize(service, resource, data={})
|
30
|
+
@service = service
|
31
|
+
raise InvalidServiceError, 'You must provide a service name' if service.nil? || service == ''
|
32
|
+
@resource = resource.is_a?(Class) ? resource : resource.class
|
33
|
+
raise InvalidResourceError, 'You must provide a calling resource' if resource.nil?
|
34
|
+
@data = data || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
## #format(custom_map)
|
38
|
+
# Formats the hash of data passed in to the format specified in the yaml file.
|
39
|
+
#
|
40
|
+
# @param custom_map [Hash] Optional hash that you can prefer to use over the correlated translation.
|
41
|
+
def format(custom_map=nil)
|
42
|
+
translations = custom_map || mapping
|
43
|
+
formatted = {}
|
44
|
+
data.each_pair do |key, val|
|
45
|
+
next if key == Spigot.config.options_key
|
46
|
+
attribute = translations[key.to_s]
|
47
|
+
formatted.merge!(attribute.to_s => data[key]) unless attribute.nil?
|
48
|
+
end
|
49
|
+
formatted
|
50
|
+
end
|
51
|
+
|
52
|
+
## #id
|
53
|
+
# The value at the foreign_key attribute specified in the resource options, defaults to 'id'.
|
54
|
+
def id
|
55
|
+
@id ||= lookup(foreign_key)
|
56
|
+
end
|
57
|
+
|
58
|
+
## #lookup(attribute)
|
59
|
+
# Find the value in the unformatted api data that matches the passed in key.
|
60
|
+
#
|
61
|
+
# @param attribute [String] The key pointing to the value you wish to lookup.
|
62
|
+
def lookup(attribute)
|
63
|
+
data.detect{|k, v| k.to_s == attribute.to_s }.try(:last)
|
64
|
+
end
|
65
|
+
|
66
|
+
## #options
|
67
|
+
# Available options per resource.
|
68
|
+
#
|
69
|
+
# @primary_key:
|
70
|
+
# Default: "#{service}_id"
|
71
|
+
# Name of the column in your local database that serves as id for an external resource.
|
72
|
+
# @foreign_key:
|
73
|
+
# Default: "id"
|
74
|
+
# Name of the key representing the resource's ID in the data received from the API.
|
75
|
+
# @conditions:
|
76
|
+
# Default: nil
|
77
|
+
# Array of attributes included in the database query, these are names of columns in your database.
|
78
|
+
def options
|
79
|
+
@options ||= mapping[Spigot.config.options_key] || {}
|
80
|
+
end
|
81
|
+
|
82
|
+
def primary_key
|
83
|
+
options['primary_key'] || "#{service}_id"
|
84
|
+
end
|
85
|
+
|
86
|
+
def foreign_key
|
87
|
+
options['foreign_key'] || mapping.invert[primary_key] || 'id'
|
88
|
+
end
|
89
|
+
|
90
|
+
def conditions
|
91
|
+
p_keys = [*(condition_keys.blank? ? primary_key : condition_keys)].map(&:to_s)
|
92
|
+
keys = mapping.select{|k, v| p_keys.include?(v.to_s) }
|
93
|
+
format(keys)
|
94
|
+
end
|
95
|
+
|
96
|
+
## #mapping
|
97
|
+
# Return a hash of the data map currently being used by this translator, including options.
|
98
|
+
def mapping
|
99
|
+
return @mapping if defined?(@mapping)
|
100
|
+
@mapping = translations[resource_key.to_s]
|
101
|
+
raise MissingResourceError, "There is no #{resource_key} mapping for #{service}" if @mapping.nil?
|
102
|
+
@mapping
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def condition_keys
|
108
|
+
options['conditions'].to_s.split(',').map(&:strip)
|
109
|
+
end
|
110
|
+
|
111
|
+
def resource_key
|
112
|
+
resource.to_s.downcase.gsub('::', '/')
|
113
|
+
end
|
114
|
+
|
115
|
+
def translations
|
116
|
+
@translations ||= Spigot.config.translations || YAML.load(translation_file)
|
117
|
+
end
|
118
|
+
|
119
|
+
def translation_file
|
120
|
+
begin
|
121
|
+
@translation_file ||= File.read(File.join(Spigot.config.path, "#{service.to_s}.yml"))
|
122
|
+
rescue Errno::ENOENT => e
|
123
|
+
raise MissingServiceError, "There is no service map for #{service} defined"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
data/script/console.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'spigot'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Spigot.logger
|
5
|
+
require File.join(Spigot.root, 'spec', 'support', 'active_record')
|
6
|
+
|
7
|
+
class ActiveUser < ActiveRecord::Base
|
8
|
+
include Spigot::Base
|
9
|
+
end
|
10
|
+
|
11
|
+
map = {'activeuser' => {
|
12
|
+
'name' => 'name',
|
13
|
+
'username' => 'login',
|
14
|
+
'spigot' => {
|
15
|
+
'primary_key' => 'username'
|
16
|
+
}
|
17
|
+
}}
|
18
|
+
|
19
|
+
conditions = {'activeuser' => {
|
20
|
+
'name' => 'name',
|
21
|
+
'login' => 'username',
|
22
|
+
'spigot' => {
|
23
|
+
'primary_key' => 'username'
|
24
|
+
}
|
25
|
+
}}
|
26
|
+
|
27
|
+
Spigot.configure do |config|
|
28
|
+
config.translations = conditions
|
29
|
+
end
|
30
|
+
|
31
|
+
user = ActiveUser.create(name: 'Matt', username: 'mttwrnr', token: 'abc123')
|
data/spec/.DS_Store
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Spigot
|
2
|
+
class ApiData
|
3
|
+
|
4
|
+
def self.basic_user
|
5
|
+
{'full_name' => 'Dean Martin', 'login' => 'classyasfuck'}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.user
|
9
|
+
{'full_name' => 'Dean Martin', 'login' => 'classyasfuck', 'auth_token' => '123abc'}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.updated_user
|
13
|
+
{'full_name' => 'Frank Sinatra', 'login' => 'livetilidie', 'auth_token' => '456bcd'}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.basic_post
|
17
|
+
{'title' => 'Brief Article', 'body' => 'lorem ipsum'}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.post
|
21
|
+
{'title' => 'Regular Article', 'body' => 'dolor sit amet', 'author' => 'Dean Martin'}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Spigot
|
2
|
+
module Mapping
|
3
|
+
|
4
|
+
class ActiveUser
|
5
|
+
|
6
|
+
def self.basic
|
7
|
+
{'activeuser' => base}
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.with_options
|
11
|
+
{'activeuser' => base.merge('spigot' => options)}
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.non_unique_key
|
15
|
+
{'activeuser' => base.merge('auth_token' => 'token', 'spigot' => non_unique)}
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.with_invalid_options
|
19
|
+
{'activeuser' => base.merge('spigot' => invalid_options)}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.base
|
25
|
+
{'full_name' => 'name', 'login' => 'username'}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.options
|
29
|
+
{'primary_key' => 'username', 'foreign_key' => 'login'}
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.non_unique
|
33
|
+
{'primary_key' => 'token', 'foreign_key' => 'auth_token'}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.invalid_options
|
37
|
+
{'primary_key' => 'nosuchcolumn', 'foreign_key' => 'nosuchkey'}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|