alman 0.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +16 -0
  4. data/Gemfile +8 -0
  5. data/README.md +103 -0
  6. data/Rakefile +8 -0
  7. data/VERSION +1 -0
  8. data/alman.gemspec +29 -0
  9. data/bin/alman-console +7 -0
  10. data/gemfiles/default-with-activesupport.gemfile +10 -0
  11. data/gemfiles/json.gemfile +12 -0
  12. data/gemfiles/yajl.gemfile +12 -0
  13. data/lib/alman.rb +68 -0
  14. data/lib/alman/apibits/api_client.rb +28 -0
  15. data/lib/alman/apibits/api_endpoint.rb +11 -0
  16. data/lib/alman/apibits/api_list.rb +88 -0
  17. data/lib/alman/apibits/api_method.rb +95 -0
  18. data/lib/alman/apibits/api_object.rb +52 -0
  19. data/lib/alman/apibits/api_resource.rb +139 -0
  20. data/lib/alman/apibits/headers_builder.rb +47 -0
  21. data/lib/alman/apibits/params_builder.rb +27 -0
  22. data/lib/alman/apibits/path_builder.rb +38 -0
  23. data/lib/alman/apibits/requester.rb +104 -0
  24. data/lib/alman/apibits/util.rb +51 -0
  25. data/lib/alman/clients/default_client.rb +31 -0
  26. data/lib/alman/endpoints/bookings_endpoint.rb +36 -0
  27. data/lib/alman/endpoints/calendar_vacancies_endpoint.rb +35 -0
  28. data/lib/alman/endpoints/calendars_endpoint.rb +48 -0
  29. data/lib/alman/endpoints/vacancies_endpoint.rb +27 -0
  30. data/lib/alman/endpoints/vacancy_bookings_endpoint.rb +11 -0
  31. data/lib/alman/errors/alman_error.rb +13 -0
  32. data/lib/alman/errors/api_connection_error.rb +4 -0
  33. data/lib/alman/errors/api_error.rb +35 -0
  34. data/lib/alman/errors/authentication_error.rb +4 -0
  35. data/lib/alman/resources/booking.rb +62 -0
  36. data/lib/alman/resources/calendar.rb +65 -0
  37. data/lib/alman/resources/vacancy.rb +48 -0
  38. data/lib/alman/version.rb +3 -0
  39. data/test/alman/api_client_test.rb +51 -0
  40. data/test/alman/api_endpoint_test.rb +13 -0
  41. data/test/alman/api_list_test.rb +49 -0
  42. data/test/alman/api_method_test.rb +78 -0
  43. data/test/alman/headers_builder_test.rb +28 -0
  44. data/test/alman/params_builder_test.rb +57 -0
  45. data/test/alman/path_builder_test.rb +50 -0
  46. data/test/alman/requester_test.rb +86 -0
  47. data/test/alman/util_test.rb +51 -0
  48. data/test/test_data.rb +72 -0
  49. data/test/test_helper.rb +41 -0
  50. metadata +208 -0
@@ -0,0 +1,52 @@
1
+ module Alman
2
+ class ApiObject
3
+ include Enumerable
4
+ attr_reader :json
5
+
6
+ def self.construct(json)
7
+ if json.is_a?(Array)
8
+ return json.map{ |a| ApiObject.construct(a) }
9
+ elsif json.is_a?(Hash)
10
+ return ApiObject.new(json)
11
+ else
12
+ return json
13
+ end
14
+ end
15
+
16
+ def initialize(json=nil)
17
+ refresh_from(json)
18
+ end
19
+
20
+ def refresh_from(json={})
21
+ @json = Util.sorta_deep_clone(json)
22
+ @json.each do |k, v|
23
+ @json[k] = ApiObject.construct(v)
24
+ end
25
+ self
26
+ end
27
+
28
+ def inspect
29
+ @json.inspect
30
+ end
31
+
32
+ def to_json(*args)
33
+ JSON.generate(@json)
34
+ end
35
+
36
+ def method_missing(name, *args, &blk)
37
+ if name.to_s.end_with?('=')
38
+ attr = name.to_s[0...-1].to_sym
39
+ @json[attr] = args[0]
40
+ else
41
+ if @json.respond_to?(name)
42
+ @json.send(name, *args, &blk)
43
+ elsif @json.has_key?(name.to_sym)
44
+ return @json[name.to_sym]
45
+ else
46
+ super
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,139 @@
1
+ module Alman
2
+ class ApiResource
3
+ attr_reader :api_method
4
+ attr_reader :json
5
+ attr_reader :client
6
+
7
+ def initialize(json=nil, api_method=nil, client=nil)
8
+ refresh_from(json, api_method, client)
9
+ end
10
+
11
+ def refresh_from(json={}, api_method=nil, client=nil)
12
+ unless json.is_a?(Hash)
13
+ json = { :id => json }
14
+ end
15
+ json = Util.symbolize_keys(json)
16
+
17
+ # Clear or write over any old data
18
+ clear_api_attributes
19
+ @api_method = api_method
20
+ @json = Util.sorta_deep_clone(json)
21
+ @client = client || Alman.default_client
22
+
23
+ # Use json (not the @json, the cloned copy)
24
+ json.each do |k, v|
25
+ unless self.class.api_attribute_names.include?(k.to_sym)
26
+ self.class.add_api_attribute(k.to_sym)
27
+ end
28
+ instance_variable_set("@#{k}", determine_api_attribute_value(k, v))
29
+ end
30
+ self
31
+ end
32
+
33
+ # Returns the default client
34
+ def self.client
35
+ Alman.default_client
36
+ end
37
+
38
+ def inspect
39
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
40
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> Attributes: " + JSON.pretty_generate(inspect_api_attributes)
41
+ end
42
+
43
+ def inspect_nested
44
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
45
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}>"
46
+ end
47
+
48
+ def to_json(*args)
49
+ JSON.generate(api_attributes)
50
+ end
51
+
52
+ def self.api_attribute_names
53
+ @api_attributes.map(&:first)
54
+ end
55
+
56
+ def self.add_api_attribute(name)
57
+ attr_accessor name.to_sym
58
+ @api_attributes[name.to_sym] = {}
59
+ end
60
+
61
+ def api_attributes
62
+ ret = {}
63
+ self.class.api_attribute_names.each do |attribute|
64
+ ret[attribute] = self.send(attribute)
65
+ end
66
+ ret
67
+ end
68
+
69
+ def inspect_api_attributes
70
+ ret = {}
71
+ api_attributes.each do |k, v|
72
+ if v.is_a?(ApiResource)
73
+ ret[k] = v.inspect_nested
74
+ else
75
+ ret[k] = v
76
+ end
77
+ end
78
+ ret
79
+ end
80
+
81
+ # TODO(joncalhoun): Fix this (not currently working as intended)
82
+ def changed_api_attributes
83
+ ret = {}
84
+ self.api_attributes.each do |name, value|
85
+ if @json[name] != value
86
+ ret[name] = value
87
+ end
88
+ end
89
+ ret
90
+ end
91
+
92
+ def clear_api_attributes
93
+ self.class.api_attribute_names.each do |name|
94
+ instance_variable_set("@#{name}", nil)
95
+ end
96
+ end
97
+
98
+ def self.determine_api_attribute_value(name, raw_value)
99
+ if @api_attributes[name] && @api_attributes[name].has_key?(:constructor)
100
+ klass = Util.constantize(@api_attributes[name][:constructor])
101
+ if(klass.respond_to?(:construct))
102
+ klass.construct(raw_value)
103
+ else
104
+ klass.new(raw_value)
105
+ end
106
+ else
107
+ ApiObject.construct(raw_value)
108
+ end
109
+ end
110
+
111
+ def determine_api_attribute_value(name, raw_value)
112
+ self.class.determine_api_attribute_value(name, raw_value)
113
+ end
114
+
115
+
116
+ def self.api_subclasses
117
+ return @api_subclasses ||= Set.new
118
+ end
119
+
120
+ def self.api_subclass_fetch(name)
121
+ @api_subclasses_hash ||= {}
122
+ if @api_subclasses_hash.has_key?(name)
123
+ @api_subclasses_hash[name]
124
+ end
125
+ end
126
+
127
+ def self.register_api_subclass(subclass, name=nil)
128
+ @api_subclasses ||= Set.new
129
+ @api_subclasses << subclass
130
+
131
+ unless name.nil?
132
+ @api_subclasses_hash ||= {}
133
+ @api_subclasses_hash[name] = subclass
134
+ end
135
+ end
136
+
137
+ @api_attributes = {}
138
+ end
139
+ end
@@ -0,0 +1,47 @@
1
+ module Alman
2
+ module HeadersBuilder
3
+
4
+ def self.build(headers)
5
+ headers ||= {}
6
+ default_headers.merge(headers)
7
+ end
8
+
9
+ def self.default_headers
10
+ headers = {
11
+ :user_agent => "Alman/#{Alman.api_version} RubyBindings/#{Alman::VERSION}",
12
+ }
13
+
14
+ begin
15
+ headers.update({
16
+ :x_alman_client_user_agent => JSON.generate(user_agent)
17
+ })
18
+ rescue => e
19
+ headers.update({
20
+ :x_alman_client_raw_user_agent => user_agent.inspect,
21
+ :error => "#{e} (#{e.class})"
22
+ })
23
+ end
24
+ headers
25
+ end
26
+
27
+ def self.user_agent
28
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
29
+
30
+ {
31
+ :bindings_version => Alman::VERSION,
32
+ :lang => 'ruby',
33
+ :lang_version => lang_version,
34
+ :platform => RUBY_PLATFORM,
35
+ :publisher => 'alman',
36
+ :uname => get_uname
37
+ }
38
+ end
39
+
40
+ def self.get_uname
41
+ `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
42
+ rescue Errno::ENOMEM => ex # couldn't create subprocess
43
+ "uname lookup failed"
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ module Alman
2
+ module ParamsBuilder
3
+
4
+ def self.clean(params)
5
+ Util.symbolize_keys(params || {})
6
+ end
7
+
8
+ # Clean the params, and the hash to_merge, and then merge them.
9
+ # This ensures that we dont get something like { "id" => 123, :id => 321 }.
10
+ def self.merge(*args)
11
+ ret = {}
12
+ args.each do |arg|
13
+ ret = ret.merge(clean(arg))
14
+ end
15
+ ret
16
+ end
17
+
18
+ def self.build(params, api_key=nil, auth_key=nil)
19
+ default_params.merge(clean(params))
20
+ end
21
+
22
+ def self.default_params
23
+ params = {}
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ module Alman
2
+ module PathBuilder
3
+
4
+ # Take a path like:
5
+ # ":path/:id/dogs/:dog_id"
6
+ # and convert it to:
7
+ # "#{object.path}/#{object.id}/dogs/#{params[:id]}" => "/objects/1/dogs/2"
8
+ #
9
+ # Path priority is:
10
+ # 1. Object - this will be a class or an instance of a class.
11
+ # 2. Params - this is a hash of key values. All keys *must* be symbolized.
12
+ def self.build(path, object, params)
13
+ ret = path.dup
14
+ if ret.include?(":")
15
+ matches = ret.scan(/:([^\/]*)/).flatten.map(&:to_sym)
16
+ missing = Set.new(matches)
17
+
18
+ matches.each do |match|
19
+ value = determine_value(match, object, params)
20
+ missing.delete(match) unless value.nil?
21
+ ret.sub!(match.inspect, "#{value}")
22
+ end
23
+
24
+ if missing.any?
25
+ raise ArgumentError.new("Could not determine the full URL. The following values of the path are missing: #{missing.to_a.join(', ')}. Try setting them in your params.")
26
+ end
27
+ end
28
+ ret
29
+ end
30
+
31
+ def self.determine_value(match, object, params)
32
+ value = object.send(match) if object && object.respond_to?(match)
33
+ value ||= params[match] if params && params.has_key?(match)
34
+ value
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,104 @@
1
+ module Alman
2
+ module Requester
3
+
4
+ def self.request(method, url, params={}, headers={})
5
+ method = method.to_sym
6
+ url, params = prepare_params(method, url, params, headers)
7
+ request_opts = {
8
+ :method => method,
9
+ :url => url,
10
+ :headers => headers,
11
+ :payload => params,
12
+
13
+ :verify_ssl => false,
14
+ :open_timeout => 30,
15
+ :timeout => 60
16
+ }
17
+
18
+ execute_request(request_opts)
19
+ end
20
+
21
+ def self.execute_request(opts)
22
+ RestClient::Request.execute(opts)
23
+ end
24
+
25
+ def self.get(url, params, headers)
26
+ self.request(:get, url, params, headers)
27
+ end
28
+
29
+ def self.delete(url, params, headers)
30
+ self.request(:delete, url, params, headers)
31
+ end
32
+
33
+ def self.put(url, params, headers)
34
+ self.request(:put, url, params, headers)
35
+ end
36
+
37
+ def self.post(url, params, headers)
38
+ self.request(:post, url, params, headers)
39
+ end
40
+
41
+ def self.prepare_params(method, url, params={}, headers={})
42
+ if [:get, :head, :delete].include?(method)
43
+ unless params.empty?
44
+ url += URI.parse(url).query ? '&' : '?' + query_string(params)
45
+ end
46
+ params = nil
47
+ else
48
+ if headers["Content-Type"] == "application/json" || headers[:"Content-Type"] == "application/json"
49
+ params = JSON.generate(params)
50
+ else
51
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
52
+ if !RestClient::Payload.has_file?(params)
53
+ params = query_string(params)
54
+ end
55
+ end
56
+ end
57
+ [url, params]
58
+ end
59
+
60
+ def self.query_string(params)
61
+ params ||= {}
62
+ if params.any?
63
+ query_array(params).join('&')
64
+ else
65
+ ""
66
+ end
67
+ end
68
+
69
+ # Three major use cases (and nesting of them needs to be supported):
70
+ # { :a => { :b => "bvalue" } } => ["a[b]=bvalue"]
71
+ # { :a => [1, 2] } => ["a[]=1", "a[]=2"]
72
+ # { :a => "value" } => ["a=value"]
73
+ def self.query_array(params, key_prefix=nil)
74
+ ret = []
75
+ params.each do |key, value|
76
+ if params.is_a?(Array)
77
+ value = key
78
+ key = ''
79
+ end
80
+ key_suffix = escape(key)
81
+ full_key = key_prefix ? "#{key_prefix}[#{key_suffix}]" : key_suffix
82
+
83
+ if value.is_a?(Hash) || value.is_a?(Array)
84
+ # Handles the following cases:
85
+ # { :a => { :b => "bvalue" } } => ["a[b]=bvalue"]
86
+ # { :a => [1, 2] } => ["a[]=1", "a[]=2"]
87
+ ret += query_array(value, full_key)
88
+ elsif value.is_a?(ApiObject)
89
+ ret += query_array(value.json, full_key)
90
+ else
91
+ # Handles the base case with just key and value:
92
+ # { :a => "value" } => ["a=value"]
93
+ ret << "#{full_key}=#{escape(value)}"
94
+ end
95
+ end
96
+ ret
97
+ end
98
+
99
+ def self.escape(val)
100
+ URI.escape(val.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,51 @@
1
+ module Alman
2
+ module Util
3
+
4
+ def self.symbolize_keys(obj)
5
+ if obj.is_a?(Hash)
6
+ ret = {}
7
+ obj.each do |key, value|
8
+ ret[(key.to_sym rescue key) || key] = symbolize_keys(value)
9
+ end
10
+ return ret
11
+ elsif obj.is_a?(Array)
12
+ return obj.map{ |value| symbolize_keys(value) }
13
+ else
14
+ return obj
15
+ end
16
+ end
17
+
18
+ def self.sorta_deep_clone(json)
19
+ if json.is_a?(Hash)
20
+ ret = {}
21
+ json.each do |k, v|
22
+ ret[k] = sorta_deep_clone(v)
23
+ end
24
+ ret
25
+ elsif json.is_a?(Array)
26
+ json.map{ |j| sorta_deep_clone(j) }
27
+ else
28
+ begin
29
+ json.dup
30
+ rescue
31
+ json
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.constantize(str, prefix=false)
37
+ str = str.to_s
38
+ begin
39
+ str.split('::').reduce(Alman, :const_get)
40
+ rescue NameError => e
41
+ if prefix
42
+ raise e
43
+ else
44
+ p = "#{self.name}".split("::").first
45
+ constantize("#{p}::#{str}", true)
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end