extended_her 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +2 -0
- data/LICENSE +7 -0
- data/README.md +723 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +32 -0
- data/examples/twitter-oauth/Gemfile +13 -0
- data/examples/twitter-oauth/app.rb +50 -0
- data/examples/twitter-oauth/config.ru +5 -0
- data/examples/twitter-oauth/views/index.haml +9 -0
- data/examples/twitter-search/Gemfile +12 -0
- data/examples/twitter-search/app.rb +55 -0
- data/examples/twitter-search/config.ru +5 -0
- data/examples/twitter-search/views/index.haml +9 -0
- data/extended_her.gemspec +27 -0
- data/lib/her.rb +23 -0
- data/lib/her/api.rb +108 -0
- data/lib/her/base.rb +17 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +5 -0
- data/lib/her/exceptions/exception.rb +4 -0
- data/lib/her/exceptions/record_invalid.rb +8 -0
- data/lib/her/exceptions/record_not_found.rb +13 -0
- data/lib/her/middleware.rb +9 -0
- data/lib/her/middleware/accept_json.rb +15 -0
- data/lib/her/middleware/first_level_parse_json.rb +34 -0
- data/lib/her/middleware/second_level_parse_json.rb +28 -0
- data/lib/her/model.rb +69 -0
- data/lib/her/model/base.rb +7 -0
- data/lib/her/model/hooks.rb +114 -0
- data/lib/her/model/http.rb +284 -0
- data/lib/her/model/introspection.rb +57 -0
- data/lib/her/model/orm.rb +191 -0
- data/lib/her/model/orm/comparison_methods.rb +20 -0
- data/lib/her/model/orm/create_methods.rb +29 -0
- data/lib/her/model/orm/destroy_methods.rb +53 -0
- data/lib/her/model/orm/error_methods.rb +19 -0
- data/lib/her/model/orm/fields_definition.rb +15 -0
- data/lib/her/model/orm/find_methods.rb +46 -0
- data/lib/her/model/orm/persistance_methods.rb +22 -0
- data/lib/her/model/orm/relation_mapper.rb +21 -0
- data/lib/her/model/orm/save_methods.rb +58 -0
- data/lib/her/model/orm/serialization_methods.rb +28 -0
- data/lib/her/model/orm/update_methods.rb +31 -0
- data/lib/her/model/paths.rb +82 -0
- data/lib/her/model/relationships.rb +191 -0
- data/lib/her/paginated_collection.rb +20 -0
- data/lib/her/relation.rb +94 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +42 -0
- data/spec/middleware/second_level_parse_json_spec.rb +25 -0
- data/spec/model/hooks_spec.rb +406 -0
- data/spec/model/http_spec.rb +184 -0
- data/spec/model/introspection_spec.rb +59 -0
- data/spec/model/orm_spec.rb +552 -0
- data/spec/model/paths_spec.rb +286 -0
- data/spec/model/relationships_spec.rb +222 -0
- data/spec/model_spec.rb +31 -0
- data/spec/spec_helper.rb +46 -0
- metadata +222 -0
data/Rakefile
ADDED
data/UPGRADE.md
ADDED
@@ -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,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,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,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
|
data/lib/her.rb
ADDED
@@ -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
|
data/lib/her/api.rb
ADDED
@@ -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
|
data/lib/her/base.rb
ADDED
@@ -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
|
data/lib/her/errors.rb
ADDED
@@ -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
|