siren_client 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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