food_info 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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in food_info.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Deviantech, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,111 @@
1
+ # FoodInfo
2
+
3
+ FoodInfo is a ruby gem that retrieves food nutrition information from various online data sources.
4
+
5
+
6
+
7
+ ## Installation
8
+
9
+ FoodInfo is available as a gem, so installation is as simple as:
10
+
11
+ gem install food_info
12
+
13
+
14
+
15
+ ## Supported Data Sources
16
+
17
+ There's currently only one adapter implemented, which pulls data from [FatSecret's REST API](http://platform.fatsecret.com/api/Default.aspx?screen=rapih). The code's modular and adding additional data sources should be fairly straightforward, but since DailyBurn discontinued their API access I don't know of any other solid sources (if you do, though, please let me know and/or add an adapter!).
18
+
19
+
20
+
21
+ ## Usage
22
+
23
+ ### Housekeeping
24
+
25
+ To use the FatSecret API (currently your only option), you'll first need to [sign up for a free developer account](http://platform.fatsecret.com/api/Default.aspx?screen=r) and retrieve the "REST API Consumer Key" and "REST API Consumer Secret" from your "My Account" tab.
26
+
27
+ Once that's done, the first step is to tell FoodInfo which adapter you want to use and what authorization to send.
28
+
29
+ FoodInfo.establish_connection(:fat_secret, :key => 'YOUR-KEY', :secret => 'YOUR-KEY')
30
+
31
+
32
+ ### Searching
33
+
34
+ Now we can search for foods.
35
+
36
+ cheese = FoodInfo.search('cheese')
37
+ cheese.total_results # => 2469
38
+ cheese.per_page # => 20
39
+ cheese.page # => 1
40
+ cheese.results # => ... big array ...
41
+ cheese.results.first # =>
42
+ # {
43
+ # "description" => "Per 100g - Calories: 403kcal | Fat: 33.14g | Carbs: 1.28g | Protein: 24.90g",
44
+ # "id" => "33689",
45
+ # "kind" => "Generic",
46
+ # "name" => "Cheddar Cheese",
47
+ # "url" => "http://www.fatsecret.com/calories-nutrition/usda/cheddar-cheese"
48
+ # }
49
+
50
+ (As an aside, I get that pretty, nicely-lined-up console formatting from the remarkably awesome [AwesomePrint Gem](https://github.com/michaeldv/awesome_print)).
51
+
52
+ Also, note that search supports pagination via the <tt>page</tt> and <tt>per_page</tt> (max 50) parameters:
53
+
54
+ FoodInfo.search('cheese', :page => 2, :per_page => 50)
55
+
56
+
57
+ ### Nutritional Details
58
+
59
+ Once you have a specific food item in mind from the search results, you can retrieve a whole lot of additional information.
60
+
61
+ cheddar = FoodInfo.search('cheese').results.first
62
+ info = FoodInfo.details( cheddar.id ) # => ... a whole lotta data ...
63
+
64
+ General metadata about the cheese includes id, name, kind, and url, which are identical to what you'd get from the <tt>search</tt> method. It also has one or more servings, however, and this is where we finally get our nutrition info.
65
+
66
+ serving = info.servings.first # =>
67
+ # {
68
+ # "calcium" => 95,
69
+ # "calories" => 532.0,
70
+ # "carbohydrate" => 1.69,
71
+ # "cholesterol" => 139.0,
72
+ # "fat" => 43.74,
73
+ # "fiber" => 0.0,
74
+ # "id" => "29131",
75
+ # "iron" => 5,
76
+ # "measurement_description" => "cup, diced",
77
+ # "metric_serving_amount" => 132.0,
78
+ # "metric_serving_unit" => "g",
79
+ # "monounsaturated_fat" => 12.396,
80
+ # "number_of_units" => 1.0,
81
+ # "polyunsaturated_fat" => 1.243,
82
+ # "potassium" => 129.0,
83
+ # "protein" => 32.87,
84
+ # "saturated_fat" => 27.841,
85
+ # "serving_description" => "1 cup diced",
86
+ # "sodium" => 820.0,
87
+ # "sugar" => 0.69,
88
+ # "trans_fat" => 0.0,
89
+ # "url" => "http://www.fatsecret.com/calories-nutrition/usda/cheddar-cheese?portionid=29131&portionamount=1.000",
90
+ # "vitamin_a" => 26,
91
+ # "vitamin_c" => 0
92
+ # }
93
+
94
+ For full details on what each of those fields contains, check [the FatSecret documentation](http://platform.fatsecret.com/api/Default.aspx?screen=rapiref&method=food.get#methodResponse).
95
+
96
+
97
+ ## Legal Note
98
+
99
+ The FatSecret TOS requires you not to store, well, [pretty much anything](http://platform.fatsecret.com/api/Default.aspx?screen=rapisd) aside from food or serving IDs for more than 24 hours. This is annoying, but I figured I'd give you a heads up.
100
+
101
+
102
+ ## Note on Patches/Pull Requests
103
+
104
+ Contributions are welcome, particularly adding adapters for additional data sources.
105
+
106
+ As always, the process is to fork this project on Github, make your changes (preferably in a topic branch, and without changing the gem version), send a pull request, and then receive much appreciation!
107
+
108
+ ## License
109
+
110
+ Copyright &copy; 2011 [Deviantech, Inc.](http://www.deviantech.com) and released under the MIT license.
111
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/TODO.txt ADDED
@@ -0,0 +1,4 @@
1
+ * Error handling (currently returns empty results for invalid key)
2
+ * Better search API (search('cheese'), not search('cheese').results)
3
+ * Add DB caching
4
+ * It'd be great to have a connection pool, so we could process multiple requests concurrently. I've added a skeleton for it, but actually using it would require a non-blocking HTTP library in HTTParty.
data/food_info.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/food_info/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Kali Donovan"]
6
+ gem.email = ["kali@deviantech.com"]
7
+ gem.description = %q{Generic Ruby interface to look up nutritional information on food. Design is modular so other adapters can be plugged in, but only data source currently implemented is FatSecret.}
8
+ gem.summary = %q{API for researching nutritional information of various foods}
9
+ gem.homepage = "https://github.com/deviantech/food_info"
10
+
11
+ gem.add_dependency('httparty', '>= 0.7.7')
12
+ gem.add_dependency('hashie', '>= 1.1.0')
13
+
14
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ gem.name = "food_info"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = FoodInfo::VERSION
20
+ end
@@ -0,0 +1,23 @@
1
+ module FoodInfo
2
+ module Adapters
3
+ class FatSecret
4
+ module Data
5
+
6
+ class FoodItem < Hashie::Trash
7
+ property :servings
8
+ property :id, :from => :food_id
9
+ property :name, :from => :food_name
10
+ property :kind, :from => :food_type
11
+ property :url, :from => :food_url
12
+ property :brand, :from => :brand_name
13
+
14
+ def initialize(*args)
15
+ super(*args)
16
+ self[:servings] = self[:servings]['serving'].collect{|s| FoodServing.new(s) }
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ module FoodInfo
2
+ module Adapters
3
+ class FatSecret
4
+ module Data
5
+
6
+ class FoodServing < Hashie::Trash
7
+ property :id, :from => :serving_id
8
+ property :url, :from => :serving_url
9
+
10
+ # These are strange from FatSecret, we'll set them as properties to get it by Hashie::Trash, then clean up after initialized
11
+ property :metric_serving_amount # => "132.000"
12
+ property :metric_serving_unit # => "g"
13
+ property :serving_description # => "1 cup, diced"
14
+ property :measurement_description # => "cup, diced"
15
+ property :number_of_units # => "1.000"
16
+
17
+ # For all attributes expected, see http://platform.fatsecret.com/api/Default.aspx?screen=rapiref&method=food.get
18
+ DECIMALS = %w(calories carbohydrate protein fat saturated_fat polyunsaturated_fat monounsaturated_fat trans_fat cholesterol sodium potassium fiber sugar)
19
+ INTEGERS = %w(vitamin_a vitamin_c calcium iron)
20
+ (DECIMALS + INTEGERS).each {|n| property(n) }
21
+
22
+ def initialize(*args)
23
+ super(*args)
24
+ normalize_data
25
+ end
26
+
27
+ def normalize_data
28
+ self[:metric_serving_amount] = self[:metric_serving_amount].to_f
29
+ self[:number_of_units] = self[:number_of_units].to_f
30
+
31
+ INTEGERS.each do |n|
32
+ self[n.to_sym] = self[n.to_sym].to_i
33
+ end
34
+
35
+ DECIMALS.each do |n|
36
+ self[n.to_sym] = self[n.to_sym].to_f
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ module FoodInfo
2
+ module Adapters
3
+ class FatSecret
4
+ module Data
5
+
6
+ class SearchResult < Hashie::Trash
7
+ property :id, :from => :food_id
8
+ property :name, :from => :food_name
9
+ property :kind, :from => :food_type
10
+ property :url, :from => :food_url
11
+ property :brand, :from => :brand_name
12
+ property :description, :from => :food_description
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ module FoodInfo
2
+ module Adapters
3
+ class FatSecret
4
+ module Data
5
+
6
+ class SearchResults < Hashie::Trash
7
+ property :results, :from => :food
8
+ property :page, :from => :page_number
9
+ property :per_page, :from => :max_results
10
+ property :total_results
11
+
12
+ def initialize(*args)
13
+ super(*args)
14
+ normalize_data
15
+ end
16
+
17
+ def normalize_data
18
+ [:page, :per_page, :total_results].each do |n|
19
+ self[n] = self[n].to_i
20
+ end
21
+
22
+ self[:page] += 1 # FatSecret indexes their pages from 0
23
+ self[:results] = [self[:results]] unless self[:results].is_a?(Array)
24
+ self[:results] = (self[:results] || []).collect {|result| SearchResult.new(result) }
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ require 'base64'
2
+ require 'hmac-sha1'
3
+
4
+ module FoodInfo
5
+ module Adapters
6
+ class FatSecret
7
+
8
+ class Request
9
+ HOST = 'http://platform.fatsecret.com/rest/server.api'
10
+
11
+ # Returns the query string necessary to run the specified +method+
12
+ # against the FatSecret API, using the +auth+ (+key+ and +secret+)
13
+ # to sign it.
14
+ #
15
+ # If this class were accessed externally, I'd refactor it a bit
16
+ # so it didn't require auth info to be passed on every request.
17
+ def initialize(method, auth, optional_params = {})
18
+ @auth = auth
19
+ @request_nonce = (0...10).map{65.+(rand(25)).chr}.join
20
+ @request_time = Time.now.to_i.to_s
21
+ @http_method = optional_params.delete(:http_method) || 'GET'
22
+
23
+ @params = {
24
+ :method => method,
25
+ :format => 'json'
26
+ }.merge(optional_params || {})
27
+ end
28
+
29
+ def signed_request
30
+ "#{HOST}?#{make_query_string(query_params)}&oauth_signature=#{request_signature}"
31
+ end
32
+
33
+
34
+ protected
35
+
36
+ def request_signature(token=nil)
37
+ signing_key = [@auth[:secret], token].join('&')
38
+
39
+ sha = HMAC::SHA1.digest(signing_key, signature_base_string)
40
+ Base64.encode64(sha).strip.oauth_escape
41
+ end
42
+
43
+ def signature_base_string
44
+ [@http_method, HOST, make_query_string(query_params)].map(&:oauth_escape).join('&')
45
+ end
46
+
47
+ def make_query_string(pairs)
48
+ sorted = pairs.sort{|a,b| a[0].to_s <=> b[0].to_s}
49
+ sorted.collect{|p| p.join('=')}.join('&')
50
+ end
51
+
52
+ def query_params
53
+ oauth_components.merge(@params)
54
+ end
55
+
56
+ def oauth_components
57
+ {
58
+ :oauth_consumer_key => @auth[:key],
59
+ :oauth_signature_method => 'HMAC-SHA1',
60
+ :oauth_timestamp => @request_time,
61
+ :oauth_nonce => @request_nonce,
62
+ :oauth_version => '1.0'
63
+ }
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,45 @@
1
+ require 'httparty'
2
+ require 'food_info/adapters/fat_secret/request'
3
+ require 'food_info/adapters/fat_secret/data/search_result'
4
+ require 'food_info/adapters/fat_secret/data/search_results'
5
+ require 'food_info/adapters/fat_secret/data/food_item'
6
+ require 'food_info/adapters/fat_secret/data/food_serving'
7
+
8
+ module FoodInfo
9
+ module Adapters
10
+ class FatSecret
11
+ include HTTParty
12
+
13
+ def initialize(opts = {})
14
+ raise AuthorizationError.new("Missing required argument :key") unless @key = opts[:key]
15
+ raise AuthorizationError.new("Missing required argument :secret") unless @secret = opts[:secret]
16
+ end
17
+
18
+ def search(q, opts = {})
19
+ params = {
20
+ :search_expression => q,
21
+ :page_number => opts[:page] || 1,
22
+ :max_results => opts[:per_page] || 20
23
+ }
24
+ params[:page_number] = [params[:page_number].to_i - 1, 0].max # FatSecret's pagination starts at 0
25
+ params[:max_results] = [params[:max_results].to_i, 50].min # FatSecret's max allowed results per page
26
+
27
+ data = query('foods.search', params)
28
+ Data::SearchResults.new( data['foods'] )
29
+ end
30
+
31
+ def details(food_id)
32
+ data = query('food.get', :food_id => food_id)
33
+ Data::FoodItem.new( data['food'] )
34
+ end
35
+
36
+ protected
37
+
38
+ def query(method, opts = {})
39
+ query_url = Request.new(method, {:key => @key, :secret => @secret}, opts).signed_request
40
+ self.class.get( query_url )
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ require "food_info/adapters/fat_secret"
2
+
3
+ module FoodInfo
4
+ # All FoodInfo adapters must expose two public methods, +search+ and +details+, and will need to
5
+ # define their own classes to return data in a unified manner consistent with that laid out by
6
+ # the existing FatSecret adapter's <tt>search_results</tt>, <tt>search_result</tt>, and <tt>details</tt> classes.
7
+ module Adapters
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module FoodInfo
2
+ class NoAdapterSpecified < StandardError; end
3
+ class UnsupportedAdapter < StandardError; end
4
+ class AuthorizationError < StandardError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def oauth_escape
3
+ CGI.escape(self).gsub("%7E", "~").gsub("+", "%20")
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module FoodInfo
2
+ VERSION = "0.0.1"
3
+ end
data/lib/food_info.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'hashie'
2
+
3
+ require "food_info/utils"
4
+ require "food_info/errors"
5
+ require "food_info/adapters"
6
+ require "food_info/version"
7
+
8
+
9
+ module FoodInfo
10
+
11
+ # Allow extending to additional data sources in the future
12
+ # Each adapter should implement +search+ and +details+ methods.
13
+ ADAPTERS = {:fat_secret => FoodInfo::Adapters::FatSecret}
14
+
15
+ class << self
16
+
17
+ # Sets the adapter we'll be pulling data from.
18
+ def establish_connection(adapter_name, opts = {})
19
+ klass = ADAPTERS[adapter_name.to_sym]
20
+ raise UnsupportedAdapter.new("Requested adapter ('#{adapter_name}') is unknown") unless klass
21
+ @@pool = []
22
+ @@cursor = 0
23
+ (opts.delete(:pool) || 1).to_i.times do
24
+ @@pool << klass.new(opts)
25
+ end
26
+
27
+ true
28
+ end
29
+
30
+ def search(q, opts = {})
31
+ next_adapter.search(q, opts)
32
+ end
33
+
34
+ def details(id)
35
+ next_adapter.details(id)
36
+ end
37
+
38
+
39
+ # FUTURE: This connection pool code won't do much good until HTTParty is non-blocking
40
+ def next_adapter
41
+ raise NoAdapterSpecified.new("You must run FoodInfo.establish_connection first") unless @@pool
42
+ @@cursor = (@@cursor + 1) % @@pool.length
43
+ @@pool[@@cursor]
44
+ end
45
+
46
+ end
47
+ end
48
+
49
+
50
+ __END__
51
+
52
+ FoodInfo.establish_connection(:fat_secret, :key => ENV['KEY'], :secret => ENV['SECRET'])
53
+ a=FoodInfo.search('cheese')
54
+ a=FoodInfo.search('cheese', :page => 1, :per_page => 1)
55
+
56
+ FoodInfo.establish_connection(:fat_secret, :key => ENV['KEY'], :secret => ENV['SECRET'])
57
+ a=FoodInfo.details("33689")
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: food_info
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Kali Donovan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-09-10 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: httparty
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 13
29
+ segments:
30
+ - 0
31
+ - 7
32
+ - 7
33
+ version: 0.7.7
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: hashie
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 19
45
+ segments:
46
+ - 1
47
+ - 1
48
+ - 0
49
+ version: 1.1.0
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ description: Generic Ruby interface to look up nutritional information on food. Design is modular so other adapters can be plugged in, but only data source currently implemented is FatSecret.
53
+ email:
54
+ - kali@deviantech.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - .gitignore
63
+ - Gemfile
64
+ - LICENSE
65
+ - README.markdown
66
+ - Rakefile
67
+ - TODO.txt
68
+ - food_info.gemspec
69
+ - lib/food_info.rb
70
+ - lib/food_info/adapters.rb
71
+ - lib/food_info/adapters/fat_secret.rb
72
+ - lib/food_info/adapters/fat_secret/data/food_item.rb
73
+ - lib/food_info/adapters/fat_secret/data/food_serving.rb
74
+ - lib/food_info/adapters/fat_secret/data/search_result.rb
75
+ - lib/food_info/adapters/fat_secret/data/search_results.rb
76
+ - lib/food_info/adapters/fat_secret/request.rb
77
+ - lib/food_info/errors.rb
78
+ - lib/food_info/utils.rb
79
+ - lib/food_info/version.rb
80
+ homepage: https://github.com/deviantech/food_info
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ requirements: []
107
+
108
+ rubyforge_project:
109
+ rubygems_version: 1.8.10
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: API for researching nutritional information of various foods
113
+ test_files: []
114
+