spigot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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