siren_client 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in siren_client.gemspec
4
+ gemspec
@@ -0,0 +1,11 @@
1
+ guard 'rspec', cmd: "bundle exec rspec" do
2
+ # watch /lib/ files
3
+ watch(%r{^lib/siren_client/(.+).rb$}) do |m|
4
+ "spec/unit/#{m[1]}_spec.rb"
5
+ end
6
+
7
+ # watch /spec/ files
8
+ watch(%r{^spec/(.+)/(.+).rb$}) do |m|
9
+ "spec/#{m[1]}/#{m[2]}.rb"
10
+ end
11
+ end
@@ -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.
@@ -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.
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec, :tag) do |t, task_args|
7
+ t.rspec_opts = '--color'
8
+ end
9
+ rescue LoadError
10
+ # no rspec available
11
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'byebug'
4
+ require 'siren_cli'
5
+
6
+ SirenCLI::Shell.new
@@ -0,0 +1,9 @@
1
+ require "siren_cli/version"
2
+
3
+ # Dependencies
4
+ require 'json'
5
+ require 'readline'
6
+ require 'siren_client'
7
+
8
+ # SirenClient files
9
+ require 'siren_cli/shell'
@@ -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
@@ -0,0 +1,3 @@
1
+ module SirenCLI
2
+ VERSION = "0.0.1"
3
+ end
@@ -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