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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a336365a8fdebfc3d1bd45fa11afea3c0a489ba6
4
+ data.tar.gz: 30a5f6d8ce9c7fce3644233db8d35ff57cc45f17
5
+ SHA512:
6
+ metadata.gz: b9df09433c59668681cfe6bd9ddb3c1c591b9e3d31c0aff96596477da1ca6144ecd94f1fcbed2b587a54e47bd6d2596cf9bfd6258020939d5229831c31711d96
7
+ data.tar.gz: e725567b5260a2b0d9e358191cdb5dee627c6a36e844d49582de010d3f1795e8a6d2dcb8e304ece3e24962de3752aa9a78cf1d369a813f55d9faaed5e2b49a11
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rspec
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spigot.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Matthew Werner
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,221 @@
1
+ # Spigot
2
+
3
+ Spigot provides a clean interface translating API data into context relevant objects
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'spigot'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install spigot
18
+
19
+ ## Usage
20
+
21
+ You express the mapping between your model's data and the data received from an API in
22
+ a yaml file. The mappings follow the structure of the data received. Any attribute you wish
23
+ to retain, assign the name of your model's attribute to the name of the key received.
24
+
25
+ Remember, the key is their attribute, the value is yours:
26
+
27
+ user:
28
+ login: 'email'
29
+ full_name: 'name'
30
+
31
+ Reads as: "For Users, their `login` is my `email` and their `full_name` is my `name`"
32
+
33
+ ## Example
34
+
35
+ # Our Model
36
+ class User
37
+ include Spigot::Base
38
+ attr_accessor :name, :email, :auth
39
+ end
40
+
41
+ # Api Data Received
42
+ data = {"full_name":"Dean Martin","login":"dino@amore.io","token":"abc123"}
43
+
44
+ # Spigot yaml file to map this data correctly
45
+ user:
46
+ full_name: name
47
+ login: email
48
+ token: auth
49
+
50
+ # Usage
51
+ User.new_by_api(data).inspect
52
+ #=> #<User:0x007ffa2918c7b8 @name="Dean Martin", @email="dino@amore.io", @auth="abc123">
53
+
54
+ ## Map Format
55
+
56
+ Each Spigot map file represents one service, with as many resources defined as you like.
57
+
58
+ ##### Basic Map
59
+
60
+ user:
61
+ name: 'full_name'
62
+ email: 'login'
63
+ username: 'username'
64
+
65
+ ##### Multiple Resources
66
+
67
+ Spigot will look up the map for the name of the class implementing the method. This let's you map
68
+ several of your resources that you're getting from the same service.
69
+
70
+ # ./config/github.yml
71
+ user:
72
+ name: 'full_name'
73
+ email: 'login'
74
+ username: 'username'
75
+
76
+ post:
77
+ title: 'title'
78
+ body: 'description'
79
+ created_at: 'timestamp'
80
+
81
+ ##### Options
82
+
83
+ Spigot will look for an options key named `spigot` in the defined map.
84
+ Those options let you configure additional options for that resource,
85
+ such as denoting the identification in the data received.
86
+
87
+ user:
88
+ name: 'full_name'
89
+ email: 'login'
90
+ username: 'username'
91
+ spigot:
92
+ primary_key: 'service_id'
93
+ foreign_key: 'id'
94
+
95
+ ## ActiveRecord
96
+
97
+ If you include Spigot on an ActiveRecord class, you get a few more methods.
98
+
99
+ ##### `find_by_api`
100
+
101
+ This uses the primary key defined in that resource's options to query the database
102
+ with the value taken from the api data. (**Note** Does not update any values on the
103
+ record received from the database.)
104
+
105
+ # Spigot yaml file for the github service
106
+ user:
107
+ full_name: name
108
+ login: email
109
+ token: auth
110
+ spigot:
111
+ primary_key: email
112
+
113
+ # API Data
114
+ data = {"full_name":"Dean","login":"dino@amore.io","token":"bcd456"}
115
+
116
+ #=> User.find_by_api(:github, data)
117
+ User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'dino@amore.io' ORDER BY "users"."id" ASC LIMIT 1
118
+ #=> #<User id: 1, name: "Dean Martin", email: "dino@amore.io", token: "abc123">
119
+
120
+ ##### `find_all_by_api`
121
+
122
+ Operates just like `find_by_api`, but returns all matches on the primary key. The method
123
+ returns an `ActveRecord::Relation` allowing you to chain other constraints if you need.
124
+
125
+ # Spigot yaml file for the github service
126
+ user:
127
+ full_name: name
128
+ login: email
129
+ token: auth
130
+ spigot:
131
+ primary_key: full_name
132
+
133
+ # API Data
134
+ data = {"full_name":"Dean","login":"dino@amore.io","token":"bcd456"}
135
+
136
+ #=> User.find_all_by_api(:github, data)
137
+ User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = 'Dean'
138
+ => #<ActiveRecord::Relation [#<User id: 1, name: "Dean", email: "dino@amore.io", token: "abc123">, #<User id: 2, name: "Dean", email: "minerals@notrocks.io", token: '92fnd'>]>
139
+
140
+ ##### `create_by_api`
141
+
142
+ Creates a record in your database using the provided API data, without doing
143
+ any kind of query before, beyond your model's defined validations. Notice the
144
+ creation does not use any of the API that isn't defined in the map.
145
+
146
+ # Spigot yaml file for the github service
147
+ user:
148
+ full_name: name
149
+ login: email
150
+ token: auth
151
+ spigot:
152
+ primary_key: email
153
+
154
+ # API Data
155
+ data = {"full_name":"Frank Sinatra","login":"live@tilidie.io","id":"3"}
156
+
157
+ #=> User.create_by_api(:github, data)
158
+ SQL (0.1ms) INSERT INTO "users" ("name", "email") VALUES (?, ?) [["name", "Frank Sinatra"], ["email", "live@tilidie.io"]]
159
+ => #<User id: 4, name: "Frank Sinatra", email: "live@tilidie.io", token: nil>
160
+
161
+ ##### `update_by_api`
162
+
163
+ Updates a record in your database. If no record matching the primary key is found, nothing happens.
164
+
165
+ # Spigot yaml file for the github service
166
+ user:
167
+ full_name: name
168
+ login: email
169
+ token: auth
170
+ spigot:
171
+ primary_key: email
172
+
173
+ # API Data
174
+ data = {"full_name":"Dino Baby","login":"dean@amore.io","token":"bcd456"}
175
+
176
+ #=> User.update_by_api(:github, data)
177
+ User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'livetilidie' ORDER BY "active_users"."id" ASC LIMIT 1
178
+ SQL (0.1ms) UPDATE "users" SET "name" = ?, token = ? WHERE "users"."id" = 3 [["name", "Dino Baby"], ["token", "bcd456"]]
179
+ => #<User id: 3, name: "Dino Baby", email: "dean@amore.io", token: "bcd456">
180
+
181
+ ##### `find_or_create_by_api`
182
+
183
+ Query the database to find an existing record. If none is found, create one with the provided API data.
184
+
185
+ ##### `create_or_update_by_api`
186
+
187
+ Query the database to find an existing record. If a record is found, update
188
+ the record with the received API data. If no record is found, create one with
189
+ the provided API data.
190
+
191
+ ## Configuration
192
+
193
+ There are a handful of options that let you make spigot work the way you need.
194
+
195
+ logger:
196
+ Specify a logger you would like Spigot to log to.
197
+ type: Object
198
+ default: Logger.new(STDOUT)
199
+
200
+ options_key:
201
+ The key which Spigot uses for configuring a resource map.
202
+ type: String
203
+ default: 'spigot'
204
+
205
+ path:
206
+ The directory which holds all the yaml files for the implemented services
207
+ type: String
208
+ default: 'config/spigot'
209
+
210
+ translations:
211
+ A map that, if present, overrides the resource maps found in the `path` directory
212
+ type: Hash
213
+ default: nil
214
+
215
+ ## Contributing
216
+
217
+ 1. Fork it
218
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
219
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
220
+ 4. Push to the branch (`git push origin my-new-feature`)
221
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
Binary file
@@ -0,0 +1,57 @@
1
+ require 'active_record'
2
+ require 'spigot'
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ ActiveRecord::Base.logger = Spigot.logger
7
+ ActiveRecord::Base.establish_connection({
8
+ :adapter => "sqlite3",
9
+ :database => ':memory:'
10
+ })
11
+
12
+ ActiveRecord::Schema.define do
13
+ self.verbose = false
14
+ create_table :users, :force => true do |t|
15
+ t.integer :github_id
16
+ t.string :username
17
+ t.string :image_url
18
+ t.string :profile_url
19
+ end
20
+ end
21
+
22
+ class User < ActiveRecord::Base
23
+ include Spigot::Base
24
+ end
25
+
26
+ Spigot.configure do |config|
27
+ config.translations = {'user' => {
28
+ 'id' => 'github_id',
29
+ 'login' => 'username',
30
+ 'avatar_url' => 'image_url',
31
+ 'url' => 'profile_url',
32
+ 'spigot' => {
33
+ 'primary_key' => 'username'
34
+ }
35
+ }}
36
+ end
37
+
38
+ puts "Making a request to an external API (Github)"
39
+ response = Net::HTTP.get_response URI.parse("https://api.github.com/users/mwerner")
40
+ puts "Parse the response: `data = JSON.parse(response.body)`"
41
+ data = JSON.parse(response.body)
42
+
43
+ puts "\nReceived a whole bunch of data: "
44
+ puts "#{data.inspect[0..100]}... etc, etc, etc (#{data.keys.length} more keys received)"
45
+
46
+ puts "\nWe don't want to use all of it. We can define a map on Spigot:"
47
+ puts User.spigot(:github).map.inspect
48
+ puts "Each key is an attribute received from the API, and the corresponding value is our column name."
49
+
50
+ puts "\nWe define our primary key in the spigot `User` options, so we know how to check if the record already exists:"
51
+ puts User.spigot(:github).options.inspect
52
+
53
+
54
+ puts "\nWe can create a new user with one nice and easy line: `User.find_or_create_by_api(:github, data)`"
55
+ puts User.find_or_create_by_api(:github, data).inspect
56
+
57
+ puts "\nEnjoy!"
@@ -0,0 +1,34 @@
1
+ require 'spigot'
2
+
3
+ Spigot.configure do |config|
4
+ config.translations = {
5
+ 'user' => {
6
+ 'full_name' => 'name',
7
+ 'login' => 'username'
8
+ }
9
+ }
10
+ end
11
+
12
+ class User
13
+ include Spigot::Base
14
+
15
+ attr_reader :name, :username
16
+
17
+ def initialize(params={})
18
+ params.each_pair do |k, v|
19
+ instance_variable_set("@#{k}".to_sym, v)
20
+ end
21
+ end
22
+
23
+ def self.api_data
24
+ { full_name: 'matthew', login: 'mwerner' }
25
+ end
26
+
27
+ def self.build
28
+ new_by_api(:github, api_data)
29
+ end
30
+ end
31
+
32
+ user = User.build
33
+ puts user.name
34
+ puts user.inspect
Binary file
@@ -0,0 +1,32 @@
1
+ require "spigot/version"
2
+ require "spigot/errors"
3
+
4
+ module Spigot
5
+ autoload :Configuration, 'spigot/configuration'
6
+ autoload :Translator, 'spigot/translator'
7
+ autoload :Record, 'spigot/record'
8
+ autoload :Base, 'spigot/base'
9
+ autoload :ActiveRecord, 'spigot/active_record'
10
+ autoload :Proxy, 'spigot/proxy'
11
+
12
+ def self.config
13
+ Configuration.instance
14
+ end
15
+
16
+ def self.configure
17
+ yield config
18
+ end
19
+
20
+ def self.root
21
+ File.expand_path('../..', __FILE__)
22
+ end
23
+
24
+ def self.logger
25
+ @log ||= Spigot.config.logger || begin
26
+ buffer = Logger.new(STDOUT)
27
+ buffer.level = $0 == 'irb' ? Logger::DEBUG : Logger::INFO
28
+ buffer.formatter = proc{|severity, datetime, progname, msg| "#{msg}\n"}
29
+ buffer
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,106 @@
1
+ module Spigot
2
+ module ActiveRecord
3
+ module ClassMethods
4
+
5
+ ## #find_by_api(service, api_data)
6
+ # Build a query based on the defined map for this resource
7
+ # to find a single matching record in the database
8
+ #
9
+ # @param service [Symbol] Service from which the data was received
10
+ # @param api_data [Hash] The data as received from the remote api, unformatted.
11
+ def find_by_api(service, api_data)
12
+ find_all_by_api(service, api_data).first
13
+ end
14
+
15
+ ## #find_all_by_api(service, api_data)
16
+ # Build a query based on the defined map for this resource
17
+ # to find all matching records in the database
18
+ #
19
+ # @param service [Symbol] Service from which the data was received
20
+ # @param api_data [Hash] The data as received from the remote api, unformatted.
21
+ def find_all_by_api(service, api_data)
22
+ find_by_translator Translator.new(service, self, api_data)
23
+ end
24
+
25
+ ## #create_by_api(service, api_data)
26
+ # Insert mapped data into the calling model's table. Does not
27
+ # do any checks on existing content already present in the database
28
+ #
29
+ # @param service [Symbol] Service from which the data was received
30
+ # @param api_data [Hash] The data as received from the remote api, unformatted.
31
+ def create_by_api(service, api_data)
32
+ create_by_translator Translator.new(service, self, api_data)
33
+ end
34
+
35
+ ## #update_by_api(service, api_data)
36
+ # Queries the database to find an existing record, based on the options
37
+ # provided to spigot. If a record is found, it updates that record
38
+ # with any new data received by the API
39
+ #
40
+ # @param service [Symbol] Service from which the data was received
41
+ # @param api_data [Hash] The data as received from the remote api, unformatted.
42
+ def update_by_api(service, api_data)
43
+ babel = Translator.new(service, self, api_data)
44
+ record = find_by_translator(babel).first
45
+ update_by_translator(babel, record) if record.present?
46
+ end
47
+
48
+ ## #find_or_create_by_api(service, api_data)
49
+ # Queries the database to find an existing record. If that record is found
50
+ # simply return it, otherwise create a new record and return it. This does
51
+ # not update any existing record. If you want that, use `create_or_update_by_api`
52
+ #
53
+ # @param service [Symbol] Service from which the data was received
54
+ # @param api_data [Hash] The data as received from the remote api, unformatted.
55
+ def find_or_create_by_api(service, api_data)
56
+ babel = Translator.new(service, self, api_data)
57
+ find_by_translator(babel).first || create_by_translator(babel)
58
+ end
59
+
60
+ ## #create_or_update_by_api(service, api_data)
61
+ # Queries the database to find an existing record. If that record is found
62
+ # it updates it with passed api_data and returns the record. Otherwise it
63
+ # creates a new record and returns the newly created record.
64
+ #
65
+ # @param service [Symbol] Service from which the data was received
66
+ # @param api_data [Hash] The data as received from the remote api, unformatted.
67
+ def create_or_update_by_api(service, api_data)
68
+ babel = Translator.new(service, self, api_data)
69
+ record = find_by_translator(babel).first
70
+ record.present? ? update_by_translator(babel, record) : create_by_translator(babel)
71
+ end
72
+
73
+ private
74
+
75
+ def find_by_translator(translator)
76
+ if invalid_primary_keys?(translator)
77
+ raise Spigot::InvalidSchemaError, "The #{translator.primary_key} column does not exist on #{self.to_s}"
78
+ end
79
+
80
+ if translator.id.blank?
81
+ Spigot.logger.warn " <Spigot::Warning> No #{translator.service} API data found at :#{translator.foreign_key}"
82
+ end
83
+
84
+ return [] if translator.conditions.blank?
85
+ self.where(translator.conditions)
86
+ end
87
+
88
+ def create_by_translator(translator)
89
+ Record.create(self, translator.format)
90
+ end
91
+
92
+ def update_by_translator(translator, record)
93
+ Record.update(self, record, translator.format)
94
+ record
95
+ end
96
+
97
+ def invalid_primary_keys?(translator)
98
+ [*translator.primary_key].each do |key|
99
+ return true unless self.column_names.include?(key.to_s)
100
+ end
101
+ false
102
+ end
103
+ end
104
+
105
+ end
106
+ end