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.
@@ -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