spigot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,7 @@
1
+ user:
2
+ name:
3
+ attribute: 'name'
4
+ options:
5
+ id: true
6
+
7
+ username: 'login'
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Spigot
2
+ VERSION = "0.0.1"
3
+ end
@@ -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')
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