remote_resource 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +1 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +22 -0
- data/README.md +147 -0
- data/Rakefile +2 -0
- data/lib/remote_resource/concerns/attributes.rb +26 -0
- data/lib/remote_resource/concerns/mappings.rb +16 -0
- data/lib/remote_resource/concerns/party_query.rb +62 -0
- data/lib/remote_resource/concerns/relation.rb +52 -0
- data/lib/remote_resource/concerns.rb +3 -0
- data/lib/remote_resource/connection.rb +28 -0
- data/lib/remote_resource/document_wrapper.rb +48 -0
- data/lib/remote_resource/html/connection.rb +3 -0
- data/lib/remote_resource/html/model.rb +7 -0
- data/lib/remote_resource/html.rb +1 -0
- data/lib/remote_resource/json/connection.rb +7 -0
- data/lib/remote_resource/json/model.rb +7 -0
- data/lib/remote_resource/model/relation.rb +99 -0
- data/lib/remote_resource/model.rb +35 -0
- data/lib/remote_resource/version.rb +3 -0
- data/lib/remote_resource.rb +11 -0
- data/remote_resource.gemspec +22 -0
- data/spec/model_spec.rb +119 -0
- data/spec/spec_helper.rb +6 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 06c0f5f8b3a5ab36d0a2545bc7356d46652a9d09
|
4
|
+
data.tar.gz: e9c83ef1c274f63439dc1c117cb987086e4ba4ec
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 64d28b4b2a7a8e4080c64c475049a6c365bf08dbf3671a7a46c303f2c28fcc68f3ae26dc65ef165387c86aa8783d97324685636b98a47bed493df2bf966a5a0b
|
7
|
+
data.tar.gz: a982c4ad71b391459e9ab8128727017a01c1d66030c7b4a32073d2b605402d458b28a44d4d54913b3f1d7fc569d7f0d4f0e8ff137a7aa85bb9ab3145676e5173
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in remote_resource.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'activesupport', '~> 4'
|
7
|
+
gem 'activemodel', '~> 4'
|
8
|
+
gem 'nokogiri'
|
9
|
+
gem 'httparty'
|
10
|
+
gem 'chronic', require: false
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'rspec', '~> 3.1.0'
|
14
|
+
gem 'webmock'
|
15
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 dizer
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# RemoteResource
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
## Remote::Html::Model
|
6
|
+
Object interface over remote HTML (and JSON) resources.
|
7
|
+
|
8
|
+
### Example
|
9
|
+
|
10
|
+
For example you have ```http://example.com/companies/a``` with this content:
|
11
|
+
|
12
|
+
```html
|
13
|
+
<html>
|
14
|
+
<body>
|
15
|
+
<h1>Company A</h1>
|
16
|
+
<div class="desc">Lovely company</div>
|
17
|
+
</body>
|
18
|
+
</html>
|
19
|
+
```
|
20
|
+
|
21
|
+
First, define Company model:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Company < RemoteResource::Html::Model
|
25
|
+
attr_accessor :name, :description
|
26
|
+
|
27
|
+
mapping do |doc|
|
28
|
+
self.name = doc.c('h1')
|
29
|
+
self.description = doc.c('.desc')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
Now you can make requests:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
company = Company.path('http://example.com/companies/a').find
|
38
|
+
company.name # => "Company A"
|
39
|
+
company.description # => "Lovely company"
|
40
|
+
```
|
41
|
+
|
42
|
+
#### Document maps
|
43
|
+
|
44
|
+
```mapping``` describes how to get attributes from html:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class Company < RemoteResource::Html::Model
|
48
|
+
attr_accessor :name, :description
|
49
|
+
|
50
|
+
mapping do |doc|
|
51
|
+
self.name = doc.c('h1')
|
52
|
+
self.description = doc.c('.desc')
|
53
|
+
end
|
54
|
+
|
55
|
+
mapping :fake_name do |doc|
|
56
|
+
self.name = 'Fake'
|
57
|
+
self.description = doc.c('.desc')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
company = Company.path('http://example.com/companies/a').find(:fake_name)
|
62
|
+
company.name # => "Fake"
|
63
|
+
company.description # => "Lovely company"
|
64
|
+
```
|
65
|
+
|
66
|
+
##### Helpers
|
67
|
+
|
68
|
+
Inside document_map you can use special helpers:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
c(selector) # Get content of html node by css selector
|
72
|
+
a(attribute, selector) # Get tag attribute by css selector
|
73
|
+
parse_date(string) # Get date, uses chronic gem to parse complex dates
|
74
|
+
```
|
75
|
+
|
76
|
+
#### Collections
|
77
|
+
|
78
|
+
.find (and its alias .first) returns only first occurrence, meanwhile .all returns Array of all elements.
|
79
|
+
```http://example.com/companies```:
|
80
|
+
|
81
|
+
```html
|
82
|
+
<html>
|
83
|
+
<body>
|
84
|
+
<div class="company">
|
85
|
+
<h1>Company A</h1>
|
86
|
+
<div class="desc">Lovely company</div>
|
87
|
+
</div>
|
88
|
+
<div class="company">
|
89
|
+
<h1>Company B</h1>
|
90
|
+
<div class="desc">Worst company ever</div>
|
91
|
+
</div>
|
92
|
+
</body>
|
93
|
+
</html>
|
94
|
+
```
|
95
|
+
|
96
|
+
Collection of companies:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
companies = Company.path('http://example.com/companies').separate(css: '.company').all # => [<Company...>, <Company...>]
|
100
|
+
companies.first.name # => "Company A"
|
101
|
+
companies.last.name # => "Company B"
|
102
|
+
```
|
103
|
+
|
104
|
+
##### Pagination example
|
105
|
+
Pagination can be performed with multiple ways, for example:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class Company < RemoteResource::Html::Model
|
109
|
+
# ...
|
110
|
+
module RelationMethods
|
111
|
+
def page(n)
|
112
|
+
query(limit: 100, offset: n.to_i * 100)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
Company.path('http://example.com/companies').page(0).all
|
118
|
+
# will perform request to http://example.com/companies?limit=100&offset=0
|
119
|
+
```
|
120
|
+
|
121
|
+
## Installation
|
122
|
+
|
123
|
+
Add this line to your application's Gemfile:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
gem 'remote_resource'
|
127
|
+
```
|
128
|
+
|
129
|
+
And then execute:
|
130
|
+
|
131
|
+
$ bundle
|
132
|
+
|
133
|
+
Or install it yourself as:
|
134
|
+
|
135
|
+
$ gem install remote_resource
|
136
|
+
|
137
|
+
## Usage
|
138
|
+
|
139
|
+
TODO: Write usage instructions here
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
1. Fork it ( https://github.com/[my-github-username]/remote_resource/fork )
|
144
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
145
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
146
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
147
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module RemoteResource::Concerns::Attributes
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include ActiveModel::Serialization
|
6
|
+
end
|
7
|
+
|
8
|
+
def attributes
|
9
|
+
self.class.attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_localone_hash
|
13
|
+
serializable_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def attr_accessor(*vars)
|
18
|
+
@attributes = (@attributes || []) + vars
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes
|
23
|
+
Hash[(@attributes || []).map{|e| [e, nil]}]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RemoteResource::Concerns::Mappings
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class << self
|
6
|
+
attr_accessor :mappings
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def mapping(name = :default, &block)
|
12
|
+
@mappings ||= {}
|
13
|
+
@mappings[name] = block
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module RemoteResource::PartyQuery
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include HTTParty
|
6
|
+
end
|
7
|
+
|
8
|
+
def query(path, options={})
|
9
|
+
result = nil
|
10
|
+
query_options = default_options.deep_merge(options.compact)
|
11
|
+
time = Benchmark.ms do
|
12
|
+
response = case options.delete(:http_method)
|
13
|
+
when :post
|
14
|
+
post(path, query_options)
|
15
|
+
else
|
16
|
+
get(path, query_options)
|
17
|
+
end
|
18
|
+
raise ResponseException.new(response.code, response) if response.code != 200
|
19
|
+
result = response.parsed_response
|
20
|
+
end
|
21
|
+
result
|
22
|
+
ensure
|
23
|
+
logger.debug "(#{time.try(:round, 1) || 'Failed'} ms) #{path} opts: #{query_options.except(:logger, :log_level, :log_format)}" if logger
|
24
|
+
end
|
25
|
+
|
26
|
+
class ResponseException < StandardError
|
27
|
+
attr_accessor :code
|
28
|
+
def initialize(code, msg, &block)
|
29
|
+
@code = code
|
30
|
+
super(["HTTP code: #{code}", msg].join('. '), &block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def default_options
|
37
|
+
{
|
38
|
+
headers: {
|
39
|
+
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36',
|
40
|
+
'Accept-Language' => 'en-US,en;q=0.8',
|
41
|
+
},
|
42
|
+
logger: logger,
|
43
|
+
log_format: :apache
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def get(path, options={}, &block)
|
48
|
+
self.class.get(path, options, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def post(path, options={}, &block)
|
52
|
+
self.class.post(path, options, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def logger
|
56
|
+
defined?(Rails) ? Rails.logger : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def debug?
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RemoteResource::Concerns::Relation
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
extend SingleForwardable
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def relation
|
10
|
+
@relation ||= RemoteResource::Model::Relation.for_model(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def delegate_to_relation(*methods)
|
14
|
+
@delegated_to_relation ||= []
|
15
|
+
@delegated_to_relation += methods
|
16
|
+
single_delegate methods => :relation
|
17
|
+
end
|
18
|
+
|
19
|
+
def delegated_to_relation
|
20
|
+
collect_from_superclasses(:@delegated_to_relation)
|
21
|
+
end
|
22
|
+
|
23
|
+
def delegate_to_relation_merged(*methods)
|
24
|
+
@delegated_to_relation_merged ||= []
|
25
|
+
@delegated_to_relation_merged += methods
|
26
|
+
delegate_to_relation(*methods)
|
27
|
+
end
|
28
|
+
|
29
|
+
def delegated_to_relation_merged
|
30
|
+
collect_from_superclasses(:@delegated_to_relation_merged)
|
31
|
+
end
|
32
|
+
|
33
|
+
def delegate_from_relation(*methods)
|
34
|
+
@delegated_from_relation ||= []
|
35
|
+
@delegated_from_relation += methods
|
36
|
+
end
|
37
|
+
|
38
|
+
def delegated_from_relation
|
39
|
+
collect_from_superclasses(:@delegated_from_relation)
|
40
|
+
end
|
41
|
+
|
42
|
+
def collect_from_superclasses(variable_name)
|
43
|
+
methods = []
|
44
|
+
klass = self
|
45
|
+
while klass do
|
46
|
+
methods += Array.wrap(klass.instance_variable_get(variable_name))
|
47
|
+
klass = klass.superclass
|
48
|
+
end
|
49
|
+
methods
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'remote_resource/concerns/party_query'
|
2
|
+
|
3
|
+
class RemoteResource::Connection
|
4
|
+
include RemoteResource::PartyQuery
|
5
|
+
|
6
|
+
def request(path, options={})
|
7
|
+
separate = options[:separate]
|
8
|
+
|
9
|
+
if options[:unwrap]
|
10
|
+
original_parser = self.class.parser
|
11
|
+
options[:parser] = Proc.new { |body|
|
12
|
+
body = options[:unwrap].call(body)
|
13
|
+
original_parser.call(body)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
result = query(path, options)
|
18
|
+
Array.wrap(
|
19
|
+
if separate.try(:[], :json)
|
20
|
+
separate[:json].inject(result) { |r, p| r.try(:[], p.to_s) }
|
21
|
+
elsif separate.try(:[], :css)
|
22
|
+
result.css(separate[:css])
|
23
|
+
else
|
24
|
+
result
|
25
|
+
end
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class RemoteResource::DocumentWrapper < SimpleDelegator
|
2
|
+
def css_content(selector)
|
3
|
+
selected = at_css(selector).try(:clone)
|
4
|
+
return unless selected
|
5
|
+
selected.css('br').each{ |br| br.replace "\n" }
|
6
|
+
selected.try(:content).to_s.strip
|
7
|
+
end
|
8
|
+
|
9
|
+
alias :c :css_content
|
10
|
+
|
11
|
+
def tag_attribute(attribute, selector)
|
12
|
+
at_css(selector).try(:[], attribute).to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
alias :a :tag_attribute
|
16
|
+
|
17
|
+
def css_self_content(selector)
|
18
|
+
at_css(selector).try(:xpath, 'text()').to_s.strip
|
19
|
+
end
|
20
|
+
|
21
|
+
alias :c_self :css_self_content
|
22
|
+
|
23
|
+
def parse_date(date, options={})
|
24
|
+
if options.any? && options.delete(:chronic) != false
|
25
|
+
require 'chronic'
|
26
|
+
Chronic.parse(date.to_s, {guess: :begin, context: :past}.merge(options))
|
27
|
+
else
|
28
|
+
begin
|
29
|
+
Time.zone.parse(date.to_s)
|
30
|
+
rescue ArgumentError
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# abs_url('http://example.com', 'path?param=1')
|
37
|
+
# => 'http://example.com/path?param=1'
|
38
|
+
#
|
39
|
+
# abs_url('ftp://sub.domain.dev/other_path?other_param=2', 'path?param=1#anchor')
|
40
|
+
# => 'ftp://sub.domain.dev/path?param=1#anchor'
|
41
|
+
def abs_url(base, path)
|
42
|
+
abs = URI(base)
|
43
|
+
rel = URI(path)
|
44
|
+
rel.scheme = abs.scheme
|
45
|
+
rel.host = abs.host
|
46
|
+
rel.to_s
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
module RemoteResource::Html; end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class RemoteResource::Model::Relation
|
2
|
+
|
3
|
+
attr_reader :model_class
|
4
|
+
attr_accessor :attributes
|
5
|
+
|
6
|
+
def initialize(model_class, attributes={})
|
7
|
+
@model_class = model_class
|
8
|
+
self.attributes = attributes
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(meth, *args, &block)
|
12
|
+
if model_class.delegated_to_relation.try(:include?, meth)
|
13
|
+
if args.count > 0
|
14
|
+
arg = if model_class.delegated_to_relation_merged.try(:include?, meth)
|
15
|
+
(attributes[meth] || {}).merge(args.first)
|
16
|
+
else
|
17
|
+
args.first
|
18
|
+
end
|
19
|
+
self.class.for_model(model_class, attributes.merge(meth => arg))
|
20
|
+
else
|
21
|
+
attributes[meth]
|
22
|
+
end
|
23
|
+
|
24
|
+
elsif model_class.delegated_from_relation.try(:include?, meth)
|
25
|
+
options = args.extract_options!
|
26
|
+
model_class.send(meth, *args, options.merge(attributes), &block)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def relation
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def all(options={})
|
37
|
+
raise '`path` for query not specified' unless attributes[:path]
|
38
|
+
results = model_class.connection.request(
|
39
|
+
attributes[:path],
|
40
|
+
separate: attributes[:separate],
|
41
|
+
http_method: attributes[:via],
|
42
|
+
query: attributes[:query],
|
43
|
+
body: attributes[:body],
|
44
|
+
cookies: attributes[:cookies],
|
45
|
+
headers: attributes[:headers],
|
46
|
+
unwrap: attributes[:unwrap],
|
47
|
+
)
|
48
|
+
results.take(attributes[:limit] || results.count).map do |result_row|
|
49
|
+
build(result_row, options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def first(options={})
|
54
|
+
limit(1).all(options).first
|
55
|
+
end
|
56
|
+
|
57
|
+
alias :find :first
|
58
|
+
|
59
|
+
def build(doc, options={}, &block)
|
60
|
+
model = model_class.new(url: attributes[:path])
|
61
|
+
document = RemoteResource::DocumentWrapper.new(doc)
|
62
|
+
|
63
|
+
mapping_name = options[:mapping] || :default
|
64
|
+
mapping = model_class.mappings[mapping_name]
|
65
|
+
raise "Mapping `#{mapping_name}` not found" unless mapping
|
66
|
+
|
67
|
+
model.instance_exec(document, &mapping)
|
68
|
+
instance_exec(model, &block) if block_given?
|
69
|
+
model
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_all_pages(all_options={}, &block)
|
73
|
+
all_entities = []
|
74
|
+
begin
|
75
|
+
page = (page || -1) + 1
|
76
|
+
remaining_limit = attributes[:limit] ? attributes[:limit] - all_entities.count : nil
|
77
|
+
entities = block_given? ? instance_exec(page, &block) : limit(remaining_limit).page(page).all(all_options)
|
78
|
+
all_entities += entities if entities.any?
|
79
|
+
end while entities.any?
|
80
|
+
all_entities.compact
|
81
|
+
end
|
82
|
+
|
83
|
+
def on_pages(urls, all_options={})
|
84
|
+
on_all_pages do |n|
|
85
|
+
if urls[n]
|
86
|
+
path(urls[n]).all(all_options)
|
87
|
+
else
|
88
|
+
[]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.for_model(model, attributes={})
|
94
|
+
rel = RemoteResource::Model::Relation.new(model, attributes)
|
95
|
+
rel.send(:extend, "#{model.name}::RelationMethods".constantize) if model.const_defined?(:RelationMethods)
|
96
|
+
rel
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'active_model'
|
3
|
+
require 'httparty'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
require 'remote_resource/connection'
|
7
|
+
require 'remote_resource/document_wrapper'
|
8
|
+
|
9
|
+
require 'remote_resource/concerns'
|
10
|
+
require 'remote_resource/concerns/attributes'
|
11
|
+
require 'remote_resource/concerns/mappings'
|
12
|
+
require 'remote_resource/concerns/relation'
|
13
|
+
|
14
|
+
|
15
|
+
class RemoteResource::Model
|
16
|
+
include ActiveModel::Model
|
17
|
+
include RemoteResource::Concerns::Attributes
|
18
|
+
include RemoteResource::Concerns::Relation
|
19
|
+
include RemoteResource::Concerns::Mappings
|
20
|
+
|
21
|
+
attr_accessor :url
|
22
|
+
|
23
|
+
delegate_to_relation :path, :separate, :via, :unwrap, :limit
|
24
|
+
delegate_to_relation_merged :query, :body, :cookies, :headers
|
25
|
+
|
26
|
+
def connection
|
27
|
+
self.class.connection
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def connection
|
32
|
+
@connection ||= RemoteResource::Connection.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'remote_resource/version'
|
2
|
+
require 'remote_resource/model'
|
3
|
+
|
4
|
+
require 'remote_resource/model/relation'
|
5
|
+
|
6
|
+
require 'remote_resource/html'
|
7
|
+
require 'remote_resource/html/connection'
|
8
|
+
require 'remote_resource/html/model'
|
9
|
+
|
10
|
+
module RemoteResource
|
11
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'remote_resource/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "remote_resource"
|
8
|
+
spec.version = RemoteResource::VERSION
|
9
|
+
spec.authors = ["dizer"]
|
10
|
+
spec.email = ["dizeru@gmail.com"]
|
11
|
+
spec.summary = %q{Access remote resources with OOP interface}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module TestCase
|
4
|
+
class Company < RemoteResource::Html::Model
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
mapping do |document|
|
8
|
+
self.name = document.c('h1')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class User < RemoteResource::Html::Model
|
13
|
+
attr_accessor :name, :link
|
14
|
+
|
15
|
+
mapping do |document|
|
16
|
+
self.name = document.c('a')
|
17
|
+
self.link = document.a('href', 'a')
|
18
|
+
end
|
19
|
+
|
20
|
+
mapping :only_name do |document|
|
21
|
+
self.name = document.c('a')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe RemoteResource::Html::Model do
|
27
|
+
|
28
|
+
let(:url) { 'http://example.com/companies/a' }
|
29
|
+
let(:query) { TestCase::User.path(url).separate(css: 'li') }
|
30
|
+
|
31
|
+
before do
|
32
|
+
stub_request(:get, url).
|
33
|
+
to_return(status: 200, body: '
|
34
|
+
<body>
|
35
|
+
<h1>Company A</h1>
|
36
|
+
<ul class="users">
|
37
|
+
<li><a href="users/1">User 1</a></li>
|
38
|
+
<li><a href="users/2">User 2</a></li>
|
39
|
+
</ul>
|
40
|
+
</body>'
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
context '#all' do
|
45
|
+
subject { query.all }
|
46
|
+
|
47
|
+
it do
|
48
|
+
expect(subject).to all(be_instance_of(TestCase::User))
|
49
|
+
end
|
50
|
+
|
51
|
+
it do
|
52
|
+
expect(subject.count).to eq(2)
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'instance' do
|
56
|
+
let(:user) { subject.first }
|
57
|
+
it do
|
58
|
+
expect(user.name).to eq('User 1')
|
59
|
+
expect(user.link).to eq('users/1')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'another mapping' do
|
64
|
+
it do
|
65
|
+
expect(query.all(mapping: :only_name)).to all(be_instance_of(TestCase::User))
|
66
|
+
expect(query.all(mapping: :only_name).count).to eq(2)
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'instance' do
|
70
|
+
let(:user) { query.first(mapping: :only_name) }
|
71
|
+
it do
|
72
|
+
expect(user.name).to eq('User 1')
|
73
|
+
expect(user.link).to eq(nil)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'limit' do
|
79
|
+
it do
|
80
|
+
expect(query.limit(nil).all.count).to eq(2)
|
81
|
+
expect(query.limit(0).all.count).to eq(0)
|
82
|
+
expect(query.limit(1).all.count).to eq(1)
|
83
|
+
expect(query.limit(2).all.count).to eq(2)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context '#first' do
|
89
|
+
subject { query.first }
|
90
|
+
|
91
|
+
it do
|
92
|
+
expect(subject).to be_instance_of(TestCase::User)
|
93
|
+
end
|
94
|
+
|
95
|
+
it do
|
96
|
+
expect(subject.name).to eq('User 1')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'relation' do
|
101
|
+
it do
|
102
|
+
expect(RemoteResource::Html::Model.path('a')).to be_instance_of(RemoteResource::Model::Relation)
|
103
|
+
end
|
104
|
+
|
105
|
+
it do
|
106
|
+
expect(RemoteResource::Html::Model.path('a').attributes).to eq(path: 'a')
|
107
|
+
expect(RemoteResource::Html::Model.path('a').path('b').attributes).to eq(path: 'b')
|
108
|
+
expect(RemoteResource::Html::Model.path('a').path).to eq('a')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'mappings' do
|
113
|
+
it do
|
114
|
+
expect(TestCase::User.mappings).to have_key(:default)
|
115
|
+
expect(TestCase::User.mappings).to have_key(:only_name)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: remote_resource
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- dizer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- dizeru@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- lib/remote_resource.rb
|
55
|
+
- lib/remote_resource/concerns.rb
|
56
|
+
- lib/remote_resource/concerns/attributes.rb
|
57
|
+
- lib/remote_resource/concerns/mappings.rb
|
58
|
+
- lib/remote_resource/concerns/party_query.rb
|
59
|
+
- lib/remote_resource/concerns/relation.rb
|
60
|
+
- lib/remote_resource/connection.rb
|
61
|
+
- lib/remote_resource/document_wrapper.rb
|
62
|
+
- lib/remote_resource/html.rb
|
63
|
+
- lib/remote_resource/html/connection.rb
|
64
|
+
- lib/remote_resource/html/model.rb
|
65
|
+
- lib/remote_resource/json/connection.rb
|
66
|
+
- lib/remote_resource/json/model.rb
|
67
|
+
- lib/remote_resource/model.rb
|
68
|
+
- lib/remote_resource/model/relation.rb
|
69
|
+
- lib/remote_resource/version.rb
|
70
|
+
- remote_resource.gemspec
|
71
|
+
- spec/model_spec.rb
|
72
|
+
- spec/spec_helper.rb
|
73
|
+
homepage: ''
|
74
|
+
licenses:
|
75
|
+
- MIT
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.2.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Access remote resources with OOP interface
|
97
|
+
test_files:
|
98
|
+
- spec/model_spec.rb
|
99
|
+
- spec/spec_helper.rb
|
100
|
+
has_rdoc:
|