siren_client 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +103 -0
- data/Rakefile +11 -0
- data/bin/siren_cli +6 -0
- data/lib/siren_cli.rb +9 -0
- data/lib/siren_cli/shell.rb +85 -0
- data/lib/siren_cli/version.rb +3 -0
- data/lib/siren_client.rb +17 -0
- data/lib/siren_client/action.rb +47 -0
- data/lib/siren_client/base.rb +32 -0
- data/lib/siren_client/entity.rb +112 -0
- data/lib/siren_client/exceptions.rb +6 -0
- data/lib/siren_client/field.rb +17 -0
- data/lib/siren_client/link.rb +22 -0
- data/lib/siren_client/version.rb +3 -0
- data/siren_client.gemspec +32 -0
- data/spec/helper/live_spec_helper.rb +20 -0
- data/spec/helper/spec_helper.rb +112 -0
- data/spec/live/traverse_spec.rb +101 -0
- data/spec/support/endpoints/concepts.rb +85 -0
- data/spec/support/endpoints/root.rb +101 -0
- data/spec/support/test_server.rb +17 -0
- data/spec/unit/action_spec.rb +86 -0
- data/spec/unit/base_spec.rb +52 -0
- data/spec/unit/entity_spec.rb +179 -0
- data/spec/unit/field_spec.rb +44 -0
- data/spec/unit/link_spec.rb +47 -0
- metadata +240 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b8341e11f509b82395f8ae9c653301abeadc3b6a
|
4
|
+
data.tar.gz: fd41f07b2c546db3cb64047d83fc23cc88be7d2c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8122952b651d378955248c0d3bb94df1f6d9965e2aec1e3d0ecfcc91a6104ba209d14bd67734ac944e6e7d302ed174b925c5e45724b75a661a4031c122699a6f
|
7
|
+
data.tar.gz: 0eb6e64977eb00c6710f1e5fbfcc964d9e5ecb341c63ff534b536175e5d46b5ad5d39944f65c6f79e99a55f365c7cc37b63f530e8dba126419cdc7935de6caaa
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Chason Choate
|
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,103 @@
|
|
1
|
+
# SirenClient
|
2
|
+
|
3
|
+
A simple client for traversing Siren APIs. Not sure what Siren is? View the spec here: https://github.com/kevinswiber/siren.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Grabbing the root of the API.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'siren_client'
|
11
|
+
|
12
|
+
# The simplest form
|
13
|
+
root = SirenClient.get('http://siren-api.example.com')
|
14
|
+
|
15
|
+
# Advanced usage
|
16
|
+
root = SirenClient.get('http://siren-api.example.com', {
|
17
|
+
headers: { "Accept": "application/json", ... },
|
18
|
+
basic_auth: { username: 'person', password: '1234' },
|
19
|
+
timeout: 5,
|
20
|
+
... # Refer to https://github.com/jnunemaker/httparty/blob/master/lib/httparty.rb#L45
|
21
|
+
# For more options.
|
22
|
+
})
|
23
|
+
```
|
24
|
+
|
25
|
+
There are four main parts to an [Entity](https://github.com/kevinswiber/siren#entity) in Siren. (properties, entities, links, and actions) To make your life a little easier SirenClient will try to pick one of the four items given a method name. Otherwise you can access the data directly.
|
26
|
+
|
27
|
+
### Properties
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# If color was a property on the root you could do something like this:
|
31
|
+
root.color
|
32
|
+
# or
|
33
|
+
root.properties['color']
|
34
|
+
```
|
35
|
+
|
36
|
+
### Entities
|
37
|
+
|
38
|
+
Since entities are usually the most important, SirenClient provides enumerable support to obtain them.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# Will grab the entity as if root was an array.
|
42
|
+
root[x]
|
43
|
+
# or
|
44
|
+
root.entities[x] # This is an array
|
45
|
+
# Will iterate through all the entities on the root.
|
46
|
+
root.each do |entity|
|
47
|
+
# do something
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
#### Entity sub-links
|
52
|
+
|
53
|
+
If the root contains an entity that is an [embedded link](https://github.com/kevinswiber/siren#embedded-link) you can call it based on it's class name. This will also execute the link's href and return you the entity.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
root.embedded_link_class_name
|
57
|
+
root.messages # For example
|
58
|
+
```
|
59
|
+
|
60
|
+
### Links
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# If you know the link's name you could do something like this:
|
64
|
+
root.concepts
|
65
|
+
# or
|
66
|
+
root.links['concepts'].go
|
67
|
+
```
|
68
|
+
|
69
|
+
### Actions
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# Again, if you know the action's name you can do this:
|
73
|
+
root.filter_concepts.where(name: 'github', status: 'active')
|
74
|
+
# or
|
75
|
+
root.actions['filter_concepts'].where(...)
|
76
|
+
```
|
77
|
+
|
78
|
+
#### Fields
|
79
|
+
|
80
|
+
Actions have fields that are used to make the request when you use `.where`. To see those fields you can do this:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
action = root.actions[0]
|
84
|
+
action.fields.each do |field|
|
85
|
+
puts "#Field: {field.name}, #{field.type}, #{field.value}, #{field.title}"
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## Development
|
90
|
+
|
91
|
+
Run the following commands to start development:
|
92
|
+
|
93
|
+
```bash
|
94
|
+
bundle install
|
95
|
+
bundle exec guard
|
96
|
+
# This will open a CLI that watches files for changes.
|
97
|
+
```
|
98
|
+
|
99
|
+
I've included `byebug` as a development dependency and you may use it as well.
|
100
|
+
|
101
|
+
## Thanks To
|
102
|
+
|
103
|
+
Kevin Swiber - For creating the Siren spec and giving this project meaning.
|
data/Rakefile
ADDED
data/bin/siren_cli
ADDED
data/lib/siren_cli.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module SirenCLI
|
2
|
+
class Shell
|
3
|
+
def initialize
|
4
|
+
root = get_root
|
5
|
+
@ent_stack = []
|
6
|
+
@ent = root
|
7
|
+
display_entity
|
8
|
+
loop_input
|
9
|
+
end
|
10
|
+
def loop_input
|
11
|
+
while input = Readline.readline('(ent)> ', true).chomp
|
12
|
+
exit if ['exit', 'quit'].include?(input.downcase)
|
13
|
+
case input.downcase
|
14
|
+
when 'b', 'back'
|
15
|
+
if @ent_stack.length == 0
|
16
|
+
puts "Already at the root entity."
|
17
|
+
next
|
18
|
+
end
|
19
|
+
@ent = @ent_stack.pop
|
20
|
+
display_entity
|
21
|
+
next
|
22
|
+
when 's', 'summary'
|
23
|
+
display_entity
|
24
|
+
next
|
25
|
+
end
|
26
|
+
begin
|
27
|
+
ent = @ent
|
28
|
+
val = eval input
|
29
|
+
if val.is_a? SirenClient::Entity
|
30
|
+
@ent_stack << @ent
|
31
|
+
@ent = val
|
32
|
+
display_entity
|
33
|
+
else
|
34
|
+
puts val unless input.empty?
|
35
|
+
end
|
36
|
+
rescue SyntaxError => e
|
37
|
+
puts "#{e.class} - #{e.message}"
|
38
|
+
rescue StandardError => e
|
39
|
+
puts "#{e.class} - #{e.message}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_root
|
45
|
+
@root_url = Readline.readline('Root URL> ', true).chomp
|
46
|
+
SirenClient.get(@root_url)
|
47
|
+
end
|
48
|
+
|
49
|
+
def display_entity
|
50
|
+
puts "----------------------------------"
|
51
|
+
puts " Entity: #{get_href(@ent.links['self'] && @ent.links['self'].href)}"
|
52
|
+
puts "----------------------------------"
|
53
|
+
puts " Properties: #{@ent.properties.keys.length}"
|
54
|
+
@ent.properties.each do |k, v|
|
55
|
+
val = v.to_s
|
56
|
+
ind = @ent.properties.keys[-1] == k ? '└──' : '├──'
|
57
|
+
puts " #{ind} #{k}: #{val.length > 80 ? val[0..80] + '...' : val}"
|
58
|
+
end
|
59
|
+
puts " Entities: #{@ent.entities.length}"
|
60
|
+
@ent.entities.each_with_index do |e, i|
|
61
|
+
ind = @ent.entities[-1] == e ? '└──' : '├──'
|
62
|
+
is_collection = e.classes.include?('collection')
|
63
|
+
puts " #{ind} [#{i}] #{e.properties.to_s.length > 80 ? e.properties.to_s[0..80] + '...' : e.properties}" unless is_collection
|
64
|
+
puts " #{ind} [#{i}] #{e.rels[0]}: #{get_href(e.href)}" if is_collection
|
65
|
+
end
|
66
|
+
puts " Links: #{@ent.links.length}"
|
67
|
+
@ent.links.each do |k, link|
|
68
|
+
ind = @ent.links.keys[-1] == k ? '└──' : '├──'
|
69
|
+
puts " #{ind} #{link.rels[0]}: #{get_href(link.href)}"
|
70
|
+
end
|
71
|
+
puts " Actions: #{@ent.actions.length}"
|
72
|
+
@ent.actions.each do |k, action|
|
73
|
+
ind = @ent.actions.keys[-1] == k ? '└──' : '├──'
|
74
|
+
puts " #{ind} #{action.name}: #{action.method.upcase} #{action.fields.map { |f| f.name + '[' + f.type + ']' }.join(', ')}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_href(href)
|
79
|
+
return 'Unknown' unless href
|
80
|
+
return href unless @root_url
|
81
|
+
trimmed = href.gsub(@root_url, '')
|
82
|
+
trimmed.empty? ? '/' : trimmed
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/siren_client.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
dir = 'siren_client'
|
2
|
+
require "#{dir}/version"
|
3
|
+
|
4
|
+
# Dependencies
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
7
|
+
require 'httparty'
|
8
|
+
require 'active_support/inflector'
|
9
|
+
require 'active_support/core_ext/hash'
|
10
|
+
|
11
|
+
# SirenClient files
|
12
|
+
require "#{dir}/exceptions"
|
13
|
+
require "#{dir}/link"
|
14
|
+
require "#{dir}/field"
|
15
|
+
require "#{dir}/action"
|
16
|
+
require "#{dir}/entity"
|
17
|
+
require "#{dir}/base"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SirenClient
|
2
|
+
class Action
|
3
|
+
attr_reader :payload, :name, :classes, :method, :href, :title, :type, :fields, :config
|
4
|
+
|
5
|
+
def initialize(data, config={})
|
6
|
+
if data.class != Hash
|
7
|
+
raise ArgumentError, "You must pass in a Hash to SirenClient::Action.new"
|
8
|
+
end
|
9
|
+
@payload = data
|
10
|
+
|
11
|
+
@config = { format: :json }.merge config
|
12
|
+
@name = @payload['name'] || ''
|
13
|
+
@classes = @payload['class'] || []
|
14
|
+
@method = (@payload['method'] || 'GET').downcase
|
15
|
+
@href = @payload['href'] || ''
|
16
|
+
@title = @payload['title'] || ''
|
17
|
+
@type = @payload['type'] || 'application/x-www-form-urlencoded'
|
18
|
+
@fields = @payload['fields'] || []
|
19
|
+
@fields.map! do |data|
|
20
|
+
SirenClient::Field.new(data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def where(params = {})
|
25
|
+
options = { headers: {}, query: {}, body: {} }.merge @config
|
26
|
+
if @method == 'get'
|
27
|
+
options[:query] = params
|
28
|
+
else
|
29
|
+
options[:body] = params
|
30
|
+
end
|
31
|
+
options[:headers]['Content-Type'] = @type
|
32
|
+
begin
|
33
|
+
query = options[:query].empty? ? '' : ('?' + options[:query].to_query)
|
34
|
+
SirenClient.logger.debug "#{@method.upcase} #{@href}#{query}"
|
35
|
+
options[:headers].each do |k, v|
|
36
|
+
SirenClient.logger.debug " #{k}: #{v}"
|
37
|
+
end
|
38
|
+
SirenClient.logger.debug ' ' + options[:body].to_query unless options[:body].empty?
|
39
|
+
Entity.new(HTTParty.send(@method.to_sym, @href, options).parsed_response, @config)
|
40
|
+
rescue URI::InvalidURIError => e
|
41
|
+
raise InvalidURIError, e.message
|
42
|
+
rescue JSON::ParserError => e
|
43
|
+
raise InvalidResponseError, e.message
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SirenClient
|
2
|
+
# Add a logger that gets passed in from an outside source
|
3
|
+
# or default to a standard logger. This will allow different
|
4
|
+
# setups i.e. java logging to log wherever/however they wish.
|
5
|
+
@@logger = Logger.new(STDOUT)
|
6
|
+
@@logger.level = Logger::WARN
|
7
|
+
@@logger.progname = 'SirenClient.' + SirenClient::VERSION
|
8
|
+
def self.logger; @@logger; end
|
9
|
+
def self.logger=(log)
|
10
|
+
unless log.respond_to?(:debug) &&
|
11
|
+
log.respond_to?(:info) &&
|
12
|
+
log.respond_to?(:warn) &&
|
13
|
+
log.respond_to?(:error) &&
|
14
|
+
log.respond_to?(:fatal)
|
15
|
+
raise InvalidLogger, "The logger object does not respond to [:debug, :info, :warn, :error, :fatal]."
|
16
|
+
end
|
17
|
+
@@logger = log
|
18
|
+
@@logger.progname = 'SirenClient.' + SirenClient::VERSION
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.get(options)
|
22
|
+
if options.is_a? String
|
23
|
+
Entity.new(options)
|
24
|
+
elsif options.is_a? Hash
|
25
|
+
url = options[:url] || options['url']
|
26
|
+
raise ArgumentError, "You must supply a valid url to SirenClient.get" unless url
|
27
|
+
Entity.new(url, options)
|
28
|
+
else
|
29
|
+
raise ArgumentError, 'You must supply either a string or hash to SirenClient.get'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module SirenClient
|
2
|
+
class Entity
|
3
|
+
attr_reader :payload, :classes, :properties, :entities, :rels,
|
4
|
+
:links, :actions, :title, :type, :config, :href
|
5
|
+
|
6
|
+
|
7
|
+
def initialize(data, config={})
|
8
|
+
@config = { format: :json }.merge config
|
9
|
+
if data.class == String
|
10
|
+
unless data.class == String && data.length > 0
|
11
|
+
raise InvalidURIError, 'An invalid url was passed to SirenClient::Entity.new.'
|
12
|
+
end
|
13
|
+
begin
|
14
|
+
SirenClient.logger.debug "GET #{data}"
|
15
|
+
@payload = HTTParty.get(data, @config).parsed_response
|
16
|
+
rescue URI::InvalidURIError => e
|
17
|
+
raise InvalidURIError, e.message
|
18
|
+
rescue JSON::ParserError => e
|
19
|
+
raise InvalidResponseError, e.message
|
20
|
+
end
|
21
|
+
elsif data.class == Hash
|
22
|
+
@payload = data
|
23
|
+
else
|
24
|
+
raise ArgumentError, "You must pass in either a url(String) or an entity(Hash) to SirenClient::Entity.new"
|
25
|
+
end
|
26
|
+
parse_data
|
27
|
+
end
|
28
|
+
|
29
|
+
# Execute an entity sub-link if called directly
|
30
|
+
# otherwise just return the entity.
|
31
|
+
def [](i)
|
32
|
+
@entities[i].href.empty? ? @entities[i] : @entities[i].go rescue nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def each(&block)
|
36
|
+
@entities.each(&block) rescue nil
|
37
|
+
end
|
38
|
+
|
39
|
+
### Entity sub-links only
|
40
|
+
def go
|
41
|
+
return if self.href.empty?
|
42
|
+
self.class.new(self.href, @config)
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(method, *args)
|
46
|
+
method_str = method.to_s
|
47
|
+
return @entities.length if method_str == 'length'
|
48
|
+
# Does it match a property, if so return the property value.
|
49
|
+
@properties.each do |key, prop|
|
50
|
+
return prop if method_str == key
|
51
|
+
end
|
52
|
+
# Does it match an entity sub-link's class?
|
53
|
+
@entities.each do |ent|
|
54
|
+
return ent.go if ent.href && ent.classes.include?(method_str)
|
55
|
+
end
|
56
|
+
# Does it match a link, if so traverse it and return the entity.
|
57
|
+
@links.each do |key, link|
|
58
|
+
return link.go if method_str == key
|
59
|
+
end
|
60
|
+
# Does it match an action, if so return the action.
|
61
|
+
@actions.each do |key, action|
|
62
|
+
return action if method_str == key
|
63
|
+
end
|
64
|
+
raise NoMethodError, 'The method does not match a property, action, or link on SirenClient::Entity.'
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def parse_data
|
70
|
+
return if @payload.nil?
|
71
|
+
@classes = @payload['class'] || []
|
72
|
+
@properties = @payload['properties'] || { }
|
73
|
+
@entities = @payload['entities'] || []
|
74
|
+
@entities.map! do |data|
|
75
|
+
self.class.new(data, @config)
|
76
|
+
end
|
77
|
+
@rels = @payload['rel'] || []
|
78
|
+
@links = @payload['links'] || []
|
79
|
+
@links.map! do |data|
|
80
|
+
Link.new(data, @config)
|
81
|
+
end
|
82
|
+
# Convert links into a hash
|
83
|
+
@links = @links.inject({}) do |hash, link|
|
84
|
+
next unless link.rels.length > 0
|
85
|
+
# Don't use a rel name if it's generic like 'collection'
|
86
|
+
hash_rel = nil
|
87
|
+
generic_rels = ['collection']
|
88
|
+
link.rels.each do |rel|
|
89
|
+
next if generic_rels.include?(rel)
|
90
|
+
hash_rel = rel and break
|
91
|
+
end
|
92
|
+
# Ensure the rel name is a valid hash key
|
93
|
+
hash[hash_rel.underscore] = link
|
94
|
+
hash
|
95
|
+
end
|
96
|
+
@actions = @payload['actions'] || []
|
97
|
+
@actions.map! do |data|
|
98
|
+
Action.new(data, @config)
|
99
|
+
end
|
100
|
+
# Convert actions into a hash
|
101
|
+
@actions = @actions.inject({}) do |hash, action|
|
102
|
+
next unless action.name
|
103
|
+
hash[action.name.underscore] = action
|
104
|
+
hash
|
105
|
+
end
|
106
|
+
@title = @payload['title'] || ''
|
107
|
+
@type = @payload['type'] || ''
|
108
|
+
# Should only be present for entity sub-links.
|
109
|
+
@href = @payload['href'] || ''
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|