extended_her 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CONTRIBUTING.md +26 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +7 -0
  7. data/README.md +723 -0
  8. data/Rakefile +11 -0
  9. data/UPGRADE.md +32 -0
  10. data/examples/twitter-oauth/Gemfile +13 -0
  11. data/examples/twitter-oauth/app.rb +50 -0
  12. data/examples/twitter-oauth/config.ru +5 -0
  13. data/examples/twitter-oauth/views/index.haml +9 -0
  14. data/examples/twitter-search/Gemfile +12 -0
  15. data/examples/twitter-search/app.rb +55 -0
  16. data/examples/twitter-search/config.ru +5 -0
  17. data/examples/twitter-search/views/index.haml +9 -0
  18. data/extended_her.gemspec +27 -0
  19. data/lib/her.rb +23 -0
  20. data/lib/her/api.rb +108 -0
  21. data/lib/her/base.rb +17 -0
  22. data/lib/her/collection.rb +12 -0
  23. data/lib/her/errors.rb +5 -0
  24. data/lib/her/exceptions/exception.rb +4 -0
  25. data/lib/her/exceptions/record_invalid.rb +8 -0
  26. data/lib/her/exceptions/record_not_found.rb +13 -0
  27. data/lib/her/middleware.rb +9 -0
  28. data/lib/her/middleware/accept_json.rb +15 -0
  29. data/lib/her/middleware/first_level_parse_json.rb +34 -0
  30. data/lib/her/middleware/second_level_parse_json.rb +28 -0
  31. data/lib/her/model.rb +69 -0
  32. data/lib/her/model/base.rb +7 -0
  33. data/lib/her/model/hooks.rb +114 -0
  34. data/lib/her/model/http.rb +284 -0
  35. data/lib/her/model/introspection.rb +57 -0
  36. data/lib/her/model/orm.rb +191 -0
  37. data/lib/her/model/orm/comparison_methods.rb +20 -0
  38. data/lib/her/model/orm/create_methods.rb +29 -0
  39. data/lib/her/model/orm/destroy_methods.rb +53 -0
  40. data/lib/her/model/orm/error_methods.rb +19 -0
  41. data/lib/her/model/orm/fields_definition.rb +15 -0
  42. data/lib/her/model/orm/find_methods.rb +46 -0
  43. data/lib/her/model/orm/persistance_methods.rb +22 -0
  44. data/lib/her/model/orm/relation_mapper.rb +21 -0
  45. data/lib/her/model/orm/save_methods.rb +58 -0
  46. data/lib/her/model/orm/serialization_methods.rb +28 -0
  47. data/lib/her/model/orm/update_methods.rb +31 -0
  48. data/lib/her/model/paths.rb +82 -0
  49. data/lib/her/model/relationships.rb +191 -0
  50. data/lib/her/paginated_collection.rb +20 -0
  51. data/lib/her/relation.rb +94 -0
  52. data/lib/her/version.rb +3 -0
  53. data/spec/api_spec.rb +131 -0
  54. data/spec/collection_spec.rb +26 -0
  55. data/spec/middleware/accept_json_spec.rb +10 -0
  56. data/spec/middleware/first_level_parse_json_spec.rb +42 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +25 -0
  58. data/spec/model/hooks_spec.rb +406 -0
  59. data/spec/model/http_spec.rb +184 -0
  60. data/spec/model/introspection_spec.rb +59 -0
  61. data/spec/model/orm_spec.rb +552 -0
  62. data/spec/model/paths_spec.rb +286 -0
  63. data/spec/model/relationships_spec.rb +222 -0
  64. data/spec/model_spec.rb +31 -0
  65. data/spec/spec_helper.rb +46 -0
  66. metadata +222 -0
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ require "rake"
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new(:spec) do |task|
10
+ task.pattern = "spec/**/*_spec.rb"
11
+ end
@@ -0,0 +1,32 @@
1
+ # Upgrade Her
2
+
3
+ Here is a list of backward-incompatible changes that were introduced while Her is pre-1.0. After reaching 1.0, it will follow the [Semantic Versioning](http://semver.org/) system.
4
+
5
+ ## 0.2.4
6
+
7
+ * Her no longer includes default middleware when making HTTP requests. The user has now to define all the needed middleware. Before:
8
+
9
+ Her::API.setup :url => "https://api.example.com" do |connection|
10
+ connection.insert(0, FaradayMiddle::OAuth)
11
+ end
12
+
13
+ Now:
14
+
15
+ Her::API.setup :url => "https://api.example.com" do |connection|
16
+ connection.use FaradayMiddle::OAuth
17
+ connection.use Her::Middleware::FirstLevelParseJSON
18
+ connection.use Faraday::Request::UrlEncoded
19
+ connection.use Faraday::Adapter::NetHttp
20
+ end
21
+
22
+ ## 0.2
23
+
24
+ * The default parser middleware has been replaced to treat first-level JSON data as the resource or collection data. Before it expected this:
25
+
26
+ { "data": { "id": 1, "name": "Foo" }, "errors": [] }
27
+
28
+ Now it expects this (the `errors` key is not treated as resource data):
29
+
30
+ { "id": 1, "name": "Foo", "errors": [] }
31
+
32
+ If you still want to get the old behavior, you can use `Her::Middleware::SecondLevelParseJSON` instead of `Her::Middleware::FirstLevelParseJSON` in your middleware stack.
@@ -0,0 +1,13 @@
1
+ source :rubygems
2
+
3
+ gem "sinatra"
4
+ gem "haml"
5
+ gem "thin", :require => false
6
+ gem "faraday_middleware"
7
+ gem "simple_oauth"
8
+
9
+ gem "her", :path => File.join(File.dirname(__FILE__),"../..")
10
+
11
+ group :development do
12
+ gem "shotgun"
13
+ end
@@ -0,0 +1,50 @@
1
+ # Create custom parser
2
+ class TwitterParser < Faraday::Response::Middleware
3
+ def on_complete(env)
4
+ json = MultiJson.load(env[:body], :symbolize_keys => true)
5
+ errors = [json.delete(:error)]
6
+ env[:body] = {
7
+ :data => json,
8
+ :errors => errors,
9
+ :metadata => {},
10
+ }
11
+ end
12
+ end
13
+
14
+ # See https://dev.twitter.com/apps
15
+ TWITTER_CREDENTIALS = {
16
+ :consumer_key => "",
17
+ :consumer_secret => "",
18
+ :token => "",
19
+ :token_secret => ""
20
+ }
21
+
22
+ # Initialize API
23
+ Her::API.setup :url => "https://api.twitter.com/1/" do |builder|
24
+ builder.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
25
+ builder.use Faraday::Request::UrlEncoded
26
+ builder.use TwitterParser
27
+ builder.use Faraday::Adapter::NetHttp
28
+ end
29
+
30
+ # Define classes
31
+ class Tweet
32
+ include Her::Model
33
+
34
+ def self.timeline
35
+ get "/statuses/home_timeline.json"
36
+ end
37
+
38
+ def self.mentions
39
+ get "/statuses/mentions.json"
40
+ end
41
+
42
+ def username
43
+ user[:screen_name]
44
+ end
45
+ end
46
+
47
+ get "/" do
48
+ @tweets = Tweet.mentions
49
+ haml :index
50
+ end
@@ -0,0 +1,5 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "./app"
5
+ run Sinatra::Application
@@ -0,0 +1,9 @@
1
+ !!!
2
+ %html
3
+ %body
4
+ %ul
5
+ - @tweets.each do |tweet|
6
+ %li{ :style => "margin: 0 0 10px" }
7
+ %span= tweet.text
8
+ %br
9
+ %strong= tweet.username
@@ -0,0 +1,12 @@
1
+ source :rubygems
2
+
3
+ gem "sinatra"
4
+ gem "haml"
5
+ gem "thin", :require => false
6
+ gem "faraday_middleware"
7
+
8
+ gem "her", :path => File.join(File.dirname(__FILE__),"../..")
9
+
10
+ group :development do
11
+ gem "shotgun"
12
+ end
@@ -0,0 +1,55 @@
1
+ # Create custom parser
2
+ class TwitterSearchParser < Faraday::Response::Middleware
3
+ METADATA_KEYS = [:completed_in, :max_id, :max_id_str, :next_page, :page, :query, :refresh_url, :results_per_page, :since_id, :since_id_str]
4
+
5
+ def on_complete(env)
6
+ json = MultiJson.load(env[:body], :symbolize_keys => true)
7
+ data = json.delete(:results)
8
+ errors = [json.delete(:error)].compact
9
+ env[:body] = {
10
+ :data => data,
11
+ :errors => errors,
12
+ :metadata => json
13
+ }
14
+ end
15
+ end
16
+
17
+ class MyCache < Hash
18
+ def read(key)
19
+ if cached = self[key]
20
+ Marshal.load(cached)
21
+ end
22
+ end
23
+
24
+ def write(key, data)
25
+ self[key] = Marshal.dump(data)
26
+ end
27
+
28
+ def fetch(key)
29
+ read(key) || yield.tap { |data| write(key, data) }
30
+ end
31
+ end
32
+
33
+ $cache = MyCache.new
34
+
35
+ # Initialize API
36
+ Her::API.setup :url => "http://search.twitter.com" do |connection|
37
+ connection.use Faraday::Request::UrlEncoded
38
+ connection.use FaradayMiddleware::Caching, $cache
39
+ connection.use TwitterSearchParser
40
+ connection.use Faraday::Adapter::NetHttp
41
+ end
42
+
43
+ # Define classes
44
+ class Tweet
45
+ include Her::Model
46
+
47
+ def self.search(query, attrs={})
48
+ get("/search.json", attrs.merge(:q => query))
49
+ end
50
+ end
51
+
52
+ get "/" do
53
+ @tweets = Tweet.search("justin bieber", :rpp => 30)
54
+ haml :index
55
+ end
@@ -0,0 +1,5 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "./app"
5
+ run Sinatra::Application
@@ -0,0 +1,9 @@
1
+ !!!
2
+ %html
3
+ %body
4
+ %ul
5
+ - @tweets.each do |tweet|
6
+ %li{ :style => "margin: 0 0 10px" }
7
+ %span= tweet.text
8
+ %br
9
+ %strong= tweet.from_user
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'her/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'extended_her'
7
+ s.version = Her::VERSION
8
+ s.authors = ['Rémi Prévost', 'Gregory Eremin']
9
+ s.email = ['remi@exomel.com']
10
+ s.homepage = 'http://remiprev.github.com/her'
11
+ s.license = 'MIT'
12
+ s.summary = 'A simple Representational State Transfer-based Hypertext Transfer Protocol-powered Object Relational Mapper. Her?'
13
+ s.description = 'Her is an ORM that maps REST resources and collections to Ruby objects'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_development_dependency 'rake', '~> 10.0'
21
+ s.add_development_dependency 'rspec', '~> 2.12'
22
+ s.add_development_dependency 'mocha', '~> 0.13'
23
+
24
+ s.add_runtime_dependency 'activesupport', '>= 3.0.0'
25
+ s.add_runtime_dependency 'faraday', '~> 0.8'
26
+ s.add_runtime_dependency 'multi_json', '~> 1.5'
27
+ end
@@ -0,0 +1,23 @@
1
+ require 'her/version'
2
+
3
+ require 'multi_json'
4
+ require 'faraday'
5
+ require 'active_support'
6
+ require 'active_support/inflector'
7
+ require 'active_support/core_ext/hash'
8
+
9
+ require 'her/exceptions/exception'
10
+ require 'her/exceptions/record_invalid'
11
+ require 'her/exceptions/record_not_found'
12
+
13
+ require 'her/model'
14
+ require 'her/relation'
15
+ require 'her/api'
16
+ require 'her/middleware'
17
+ require 'her/errors'
18
+ require 'her/collection'
19
+ require 'her/paginated_collection'
20
+ require 'her/base'
21
+
22
+ module Her
23
+ end
@@ -0,0 +1,108 @@
1
+ module Her
2
+ # This class is where all HTTP requests are made. Before using Her, you must configure it
3
+ # so it knows where to make those requests. In Rails, this is usually done in `config/initializers/her.rb`:
4
+ class API
5
+ # @private
6
+ attr_reader :base_uri, :connection, :options
7
+
8
+ # Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
9
+ def self.setup(attrs={}, &block)
10
+ @@default_api = new
11
+ @@default_api.setup(attrs, &block)
12
+ end
13
+
14
+ # Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
15
+ # If your application uses only one API, you should use Her::API.setup to configure the default API
16
+ #
17
+ # @example Setting up a new API
18
+ # api = Her::API.new :url => "https://api.example" do |connection|
19
+ # connection.use Faraday::Request::UrlEncoded
20
+ # connection.use Her::Middleware::DefaultParseJSON
21
+ # end
22
+ #
23
+ # class User
24
+ # uses_api api
25
+ # end
26
+ def initialize(*args, &blk)
27
+ self.setup(*args, &blk)
28
+ end
29
+
30
+ # Setup the API connection.
31
+ #
32
+ # @param [Hash] attrs the Faraday options
33
+ # @option attrs [String] :url The main HTTP API root (eg. `https://api.example.com`)
34
+ # @option attrs [String] :ssl A hash containing [SSL options](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates)
35
+ #
36
+ # @return Faraday::Connection
37
+ #
38
+ # @example Setting up the default API connection
39
+ # Her::API.setup :url => "https://api.example"
40
+ #
41
+ # @example A custom middleware added to the default list
42
+ # class MyAuthentication < Faraday::Middleware
43
+ # def call(env)
44
+ # env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
45
+ # @all.call(env)
46
+ # end
47
+ # end
48
+ # Her::API.setup :url => "https://api.example.com" do |connection|
49
+ # connection.use Faraday::Request::UrlEncoded
50
+ # connection.use Her::Middleware::DefaultParseJSON
51
+ # connection.use Faraday::Adapter::NetHttp
52
+ # end
53
+ #
54
+ # @example A custom parse middleware
55
+ # class MyCustomParser < Faraday::Response::Middleware
56
+ # def on_complete(env)
57
+ # json = JSON.parse(env[:body], :symbolize_names => true)
58
+ # errors = json.delete(:errors) || {}
59
+ # metadata = json.delete(:metadata) || []
60
+ # env[:body] = { :data => json, :errors => errors, :metadata => metadata }
61
+ # end
62
+ # end
63
+ # Her::API.setup :url => "https://api.example.com" do |connection|
64
+ # connection.use Faraday::Request::UrlEncoded
65
+ # connection.use MyCustomParser
66
+ # connection.use Faraday::Adapter::NetHttp
67
+ # end
68
+ def setup(attrs={}, &blk)
69
+ attrs[:url] = attrs.delete(:base_uri) if attrs.include?(:base_uri) # Support legacy :base_uri option
70
+ @base_uri = attrs[:url]
71
+ @options = attrs
72
+ @connection = Faraday.new(@options) do |connection|
73
+ yield connection if block_given?
74
+ end
75
+ self
76
+ end
77
+
78
+ # Define a custom parsing procedure. The procedure is passed the response object and is
79
+ # expected to return a hash with three keys: a main data Hash, an errors Hash
80
+ # and a metadata Hash.
81
+ #
82
+ # @private
83
+ def request(attrs={})
84
+ method = attrs.delete(:_method)
85
+ path = attrs.delete(:_path)
86
+ headers = attrs.delete(:_headers)
87
+ attrs.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
88
+ response = @connection.send method do |request|
89
+ request.headers.merge!(headers) if headers
90
+ if method == :get
91
+ # For GET requests, treat additional parameters as querystring data
92
+ request.url path, attrs
93
+ else
94
+ # For POST, PUT and DELETE requests, treat additional parameters as request body
95
+ request.url path
96
+ request.body = attrs
97
+ end
98
+ end
99
+ response.env[:body]
100
+ end
101
+
102
+ private
103
+ # @private
104
+ def self.default_api(attrs={})
105
+ defined?(@@default_api) ? @@default_api : nil
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,17 @@
1
+ module Her
2
+ class Base # In case you prefer inheritance over mixins
3
+ include Model
4
+ include ActiveModel::Conversion if defined?(ActiveModel)
5
+ include ActiveModel::AttributeMethods if defined?(ActiveModel)
6
+ extend ActiveModel::Naming if defined?(ActiveModel)
7
+
8
+ class << self
9
+ def inherited(klass)
10
+ klass.root_element(klass.name.demodulize.underscore)
11
+ klass.collection_path(klass.root_element.pluralize)
12
+ klass.resource_path([klass.collection_path, '/:id'].join)
13
+ klass.uses_api(Her::API.default_api)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Her
2
+ class Collection < ::Array
3
+ attr_reader :metadata, :errors
4
+
5
+ # @private
6
+ def initialize(items=[], metadata={}, errors={})
7
+ super(items)
8
+ @metadata = metadata
9
+ @errors = errors
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module Her
2
+ module Errors
3
+ class PathError < StandardError; end;
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Her
2
+ class Exception < ::Exception
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ module Her
2
+ class RecordInvalid < Exception
3
+ def initialize(errors)
4
+ message = errors.map{ |field, message| [field, message].join(' ') }.join(', ')
5
+ super(message)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module Her
2
+ class RecordNotFound < Exception
3
+ class << self
4
+ def one(model, id)
5
+ new("Couldn't find #{model.name} with id=#{id}")
6
+ end
7
+
8
+ def some(model, ids, found, looking_for)
9
+ super("Couldn't find all #{model.name}s with IDs (#{ids.join(', ')}) (found #{found} results, but was looking for #{looking_for})")
10
+ end
11
+ end
12
+ end
13
+ end