atum 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +9 -0
  5. data/.rubocop_todo.yml +25 -0
  6. data/.travis.yml +9 -0
  7. data/Appraisals +9 -0
  8. data/CHANGELOG.md +5 -0
  9. data/CONTRIBUTING.md +9 -0
  10. data/CONTRIBUTORS.md +15 -0
  11. data/Gemfile +4 -0
  12. data/Guardfile +23 -0
  13. data/LICENSE.txt +22 -0
  14. data/README.md +95 -0
  15. data/Rakefile +13 -0
  16. data/TODO +3 -0
  17. data/atum.gemspec +37 -0
  18. data/bin/atum +41 -0
  19. data/circle.yml +7 -0
  20. data/gemfiles/faraday_0.8.9.gemfile +8 -0
  21. data/gemfiles/faraday_0.9.gemfile +8 -0
  22. data/lib/atum.rb +15 -0
  23. data/lib/atum/core.rb +13 -0
  24. data/lib/atum/core/client.rb +45 -0
  25. data/lib/atum/core/errors.rb +20 -0
  26. data/lib/atum/core/link.rb +77 -0
  27. data/lib/atum/core/paginator.rb +32 -0
  28. data/lib/atum/core/request.rb +55 -0
  29. data/lib/atum/core/resource.rb +15 -0
  30. data/lib/atum/core/response.rb +53 -0
  31. data/lib/atum/core/schema.rb +12 -0
  32. data/lib/atum/core/schema/api_schema.rb +62 -0
  33. data/lib/atum/core/schema/link_schema.rb +121 -0
  34. data/lib/atum/core/schema/parameter.rb +27 -0
  35. data/lib/atum/core/schema/parameter_choice.rb +28 -0
  36. data/lib/atum/core/schema/resource_schema.rb +51 -0
  37. data/lib/atum/generation.rb +15 -0
  38. data/lib/atum/generation/erb_context.rb +17 -0
  39. data/lib/atum/generation/errors.rb +6 -0
  40. data/lib/atum/generation/generator_link.rb +39 -0
  41. data/lib/atum/generation/generator_resource.rb +31 -0
  42. data/lib/atum/generation/generator_service.rb +73 -0
  43. data/lib/atum/generation/generators/base_generator.rb +57 -0
  44. data/lib/atum/generation/generators/client_generator.rb +16 -0
  45. data/lib/atum/generation/generators/module_generator.rb +17 -0
  46. data/lib/atum/generation/generators/resource_generator.rb +23 -0
  47. data/lib/atum/generation/generators/views/client.erb +26 -0
  48. data/lib/atum/generation/generators/views/module.erb +104 -0
  49. data/lib/atum/generation/generators/views/resource.erb +33 -0
  50. data/lib/atum/generation/options_parameter.rb +12 -0
  51. data/lib/atum/version.rb +3 -0
  52. data/spec/atum/core/client_spec.rb +26 -0
  53. data/spec/atum/core/errors_spec.rb +19 -0
  54. data/spec/atum/core/link_spec.rb +80 -0
  55. data/spec/atum/core/paginator_spec.rb +72 -0
  56. data/spec/atum/core/request_spec.rb +110 -0
  57. data/spec/atum/core/resource_spec.rb +66 -0
  58. data/spec/atum/core/response_spec.rb +127 -0
  59. data/spec/atum/core/schema/api_schema_spec.rb +49 -0
  60. data/spec/atum/core/schema/link_schema_spec.rb +91 -0
  61. data/spec/atum/core/schema/parameter_choice_spec.rb +40 -0
  62. data/spec/atum/core/schema/parameter_spec.rb +24 -0
  63. data/spec/atum/core/schema/resource_schema_spec.rb +24 -0
  64. data/spec/atum/generation/generator_link_spec.rb +62 -0
  65. data/spec/atum/generation/generator_resource_spec.rb +44 -0
  66. data/spec/atum/generation/generator_service_spec.rb +41 -0
  67. data/spec/atum/generation/generators/base_generator_spec.rb +75 -0
  68. data/spec/atum/generation/generators/client_generator_spec.rb +30 -0
  69. data/spec/atum/generation/generators/module_generator_spec.rb +37 -0
  70. data/spec/atum/generation/generators/resource_generator_spec.rb +46 -0
  71. data/spec/atum/generation/options_parameter_spec.rb +27 -0
  72. data/spec/fixtures/fruity_schema.json +161 -0
  73. data/spec/fixtures/sample_schema.json +139 -0
  74. data/spec/integration/client_integration_spec.rb +91 -0
  75. data/spec/spec_helper.rb +11 -0
  76. metadata +303 -0
@@ -0,0 +1,20 @@
1
+ module Atum
2
+ module Core
3
+ class SchemaError < StandardError; end
4
+
5
+ class ResponseError < StandardError; end
6
+
7
+ class ApiError < StandardError
8
+ attr_reader :error
9
+
10
+ def initialize(error)
11
+ @error = error
12
+ if @error.key?('documentation_url')
13
+ super("#{@error['message']}, see #{@error['documentation_url']}")
14
+ else
15
+ super("#{@error['message']}")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,77 @@
1
+ require 'time'
2
+ require 'active_support/inflector'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module Atum
6
+ module Core
7
+ # A link invokes requests with an HTTP server.
8
+ class Link
9
+ # The amount limit is increased on each successive fetch in pagination
10
+ LIMIT_INCREMENT = 50
11
+
12
+ # Instantiate a link.
13
+ #
14
+ # @param url [String] The URL to use when making requests. Include the
15
+ # username and password to use with HTTP basic auth.
16
+ # @param link_schema [LinkSchema] The schema for this link.
17
+ # @param options [Hash] Configuration for the link. Possible keys
18
+ # include:
19
+ # - default_headers: Optionally, a set of headers to include in every
20
+ # request made by the client. Default is no custom headers.
21
+ def initialize(url, link_schema, options = {})
22
+ root_url, @path_prefix = unpack_url(url)
23
+ @connection = Faraday.new(url: root_url)
24
+ @link_schema = link_schema
25
+ @headers = options[:default_headers] || {}
26
+ end
27
+
28
+ # Make a request to the server.
29
+ #
30
+ # @param parameters [Array] The list of parameters to inject into the
31
+ # path. A request body can be passed as the final parameter and will
32
+ # always be converted to JSON before being transmitted.
33
+ # @raise [ArgumentError] Raised if either too many or too few parameters
34
+ # were provided.
35
+ # @return [String,Object,Enumerator] A string for text responses, an
36
+ # object for JSON responses, or an enumerator for list responses.
37
+ def run(*parameters)
38
+ options = parameters.pop
39
+ raise ArgumentError, 'options must be a hash' unless options.is_a?(Hash)
40
+
41
+ options = default_options.deep_merge(options)
42
+ path = build_path(*parameters)
43
+ Request.new(@connection, @link_schema.method, path, options).request
44
+ end
45
+
46
+ private
47
+
48
+ def default_options
49
+ {
50
+ headers: @headers,
51
+ envelope_name: @link_schema.resource_schema.name.pluralize
52
+ }
53
+ end
54
+
55
+ def build_path(*parameters)
56
+ @path_prefix + @link_schema.construct_path(*parameters)
57
+ end
58
+
59
+ def apply_link_schema(hash)
60
+ definitions = @link_schema.resource_schema.definitions
61
+ hash.each do |k, v|
62
+ next unless definitions.fetch(k, {}).fetch('format', nil)
63
+ case definitions[k]['format']
64
+ when 'date-time'
65
+ hash[k] = Time.parse(v) unless v.nil?
66
+ end
67
+ end
68
+ hash
69
+ end
70
+
71
+ def unpack_url(url)
72
+ path = URI.parse(url).path
73
+ [URI.join(url).to_s, path == '/' ? '' : path]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,32 @@
1
+ module Atum
2
+ module Core
3
+ class Paginator
4
+ LIMIT_INCREMENT = 50
5
+
6
+ def initialize(request, initial_response, options)
7
+ @request = request
8
+ @options = options
9
+ @initial_response = initial_response
10
+ end
11
+
12
+ def enumerator
13
+ response = @initial_response
14
+ Enumerator.new do |yielder|
15
+ loop do
16
+ items = @request.unenvelope(response.body)
17
+ items.each { |item| yielder << item }
18
+
19
+ break if items.count < response.limit
20
+
21
+ new_options = @options.dup
22
+ new_options[:query] = @options.fetch(:query, {})
23
+ new_options[:query].merge!(after: response.meta['cursors']['after'],
24
+ limit: response.limit + LIMIT_INCREMENT)
25
+
26
+ response = @request.make_request(new_options)
27
+ end
28
+ end.lazy
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ module Atum
2
+ module Core
3
+ class Request
4
+ def initialize(connection, method, path, options)
5
+ @connection = connection
6
+ @method = method
7
+ @path = path
8
+ @headers = options[:headers] || {}
9
+ @body = options[:body] || nil
10
+ @query = options[:query] || {}
11
+ @envelope_name = options[:envelope_name] || 'data'
12
+ end
13
+
14
+ def request
15
+ response = Response.new(make_request)
16
+ return response.body unless response.json?
17
+
18
+ if response.limit.nil?
19
+ unenvelope(response.body)
20
+ else
21
+ Paginator.new(self, response, options).enumerator
22
+ end
23
+ end
24
+
25
+ def make_request(opts = options)
26
+ body = opts[:body]
27
+ headers = opts.fetch(:headers, {})
28
+ query = opts[:query]
29
+
30
+ # TODO: Not sure this is required in faraday 0.9.0
31
+ if body.is_a?(Hash)
32
+ body = body.to_json
33
+ headers['Content-Type'] ||= 'application/json'
34
+ end
35
+
36
+ @connection.send(@method) do |request|
37
+ request.url @path
38
+ request.body = body
39
+ request.params = query
40
+ request.headers.merge!(headers)
41
+ end
42
+ end
43
+
44
+ def unenvelope(body)
45
+ body[@envelope_name] || body['data']
46
+ end
47
+
48
+ private
49
+
50
+ def options
51
+ { headers: @headers, body: @body, query: @query }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ module Atum
2
+ module Core
3
+ # A resource with methods mapped to API links.
4
+ class Resource
5
+ # Instantiate a resource.
6
+ #
7
+ # @param links [Hash<String,Link>] A hash that maps method names to links.
8
+ def initialize(links)
9
+ links.each do |name, link|
10
+ define_singleton_method(name) { |*args| link.run(*args) }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Atum
2
+ module Core
3
+ class Response
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+
8
+ def body
9
+ json? ? handle_json : handle_raw
10
+ end
11
+
12
+ def json?
13
+ content_type = @response.headers['Content-Type'] ||
14
+ @response.headers['content-type'] || ''
15
+ content_type.include?('application/json')
16
+ end
17
+
18
+ def error?
19
+ @response.status >= 400
20
+ end
21
+
22
+ def meta
23
+ unless json?
24
+ raise ResponseError, 'Cannot fetch meta for non JSON response'
25
+ end
26
+
27
+ json_body.fetch('meta', {})
28
+ end
29
+
30
+ def limit
31
+ meta.fetch('limit', nil)
32
+ end
33
+
34
+ private
35
+
36
+ def json_body
37
+ @json_body ||= JSON.parse(@response.body).with_indifferent_access
38
+ end
39
+
40
+ def raw_body
41
+ @response.body
42
+ end
43
+
44
+ def handle_json
45
+ error? ? raise(ApiError, json_body['error']) : json_body
46
+ end
47
+
48
+ def handle_raw
49
+ error? ? raise(ApiError) : raw_body
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,12 @@
1
+ module Atum
2
+ module Core
3
+ module Schema
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'atum/core/schema/api_schema'
9
+ require 'atum/core/schema/link_schema'
10
+ require 'atum/core/schema/resource_schema'
11
+ require 'atum/core/schema/parameter'
12
+ require 'atum/core/schema/parameter_choice'
@@ -0,0 +1,62 @@
1
+ module Atum
2
+ module Core
3
+ module Schema
4
+ class ApiSchema
5
+ attr_reader :schema
6
+
7
+ def initialize(schema)
8
+ @schema = schema
9
+ end
10
+
11
+ # Description of the API
12
+ def description
13
+ @schema['description']
14
+ end
15
+
16
+ # Get the schema for a resource.
17
+ #
18
+ # @param name [String] The name of the resource.
19
+ # @raise [SchemaError] Raised if an unknown resource name is provided.
20
+ # @return ResourceSchema The resource schema for resource called name
21
+ def resource_schema_for(name)
22
+ unless resource_schema_hash.key?(name)
23
+ raise SchemaError, "Unknown resource '#{name}'."
24
+ end
25
+
26
+ resource_schema_hash[name]
27
+ end
28
+
29
+ # @return [Array<ResourceSchema>] The resource schemata in this API.
30
+ def resource_schemas
31
+ resource_schema_hash.values
32
+ end
33
+
34
+ # Get a simple human-readable representation of this client instance.
35
+ def inspect
36
+ "#<Atum::ApiSchema description=\"#{description}\">"
37
+ end
38
+
39
+ alias_method :to_s, :inspect
40
+
41
+ # Lookup a path in this schema.
42
+ #
43
+ # @param path [Array<String>] Array of keys, one for each to look up in
44
+ # the schema
45
+ # @return [Object] Value at the specifed path in this schema.
46
+ def lookup_path(*path)
47
+ path.reduce(@schema) { |a, e| a[e] }
48
+ end
49
+
50
+ private
51
+
52
+ def resource_schema_hash
53
+ @resource_schema_hash ||= Hash[
54
+ @schema['definitions'].map do |key, value|
55
+ [key, ResourceSchema.new(self, value, key)]
56
+ end
57
+ ]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,121 @@
1
+ module Atum
2
+ module Core
3
+ module Schema
4
+ class LinkSchema
5
+ attr_reader :resource_schema
6
+
7
+ # @param api_schema [ApiSchema] The schema for the whole API
8
+ # @param resource_schema [ResourceSchema] The schema of the resource this
9
+ # link belongs to.
10
+ # @param link_schema [Hash] The link's schema
11
+ def initialize(api_schema, resource_schema, link_schema_hash)
12
+ @api_schema = api_schema
13
+ @resource_schema = resource_schema
14
+ @link_schema_hash = link_schema_hash
15
+ end
16
+
17
+ def name
18
+ @link_schema_hash['title'].downcase.gsub(' ', '_')
19
+ end
20
+
21
+ def description
22
+ @link_schema_hash['description']
23
+ end
24
+
25
+ def method
26
+ @link_schema_hash['method'].downcase.to_sym
27
+ end
28
+
29
+ def needs_request_body?
30
+ @link_schema_hash.key?('schema')
31
+ end
32
+
33
+ # Get the parameters this link expects.
34
+ #
35
+ # @param parameters [Array] The names of the parameter definitions to
36
+ # convert to parameter names.
37
+ # @return [Array<Parameter|ParameterChoice>] A list of parameter instances
38
+ # that represent parameters to be injected into the link URL.
39
+ def parameters
40
+ expected_params.map do |parameter|
41
+ # URI decode parameters and strip the leading '{(' and trailing ')}'.
42
+ parameter = URI.unescape(parameter[2..-3])
43
+ generate_parameter(parameter)
44
+ end
45
+ end
46
+
47
+ # Construct the URL and body for a call to this link
48
+ #
49
+ # @param params [Array] The list of parameters to inject into the
50
+ # path.
51
+ # @raise [ArgumentError] Raised if either too many or too few parameters
52
+ # were provided.
53
+ # @return [String,Object] A path and request body pair. The body value is
54
+ # nil if a payload wasn't included in the list of parameters.
55
+ def construct_path(*params)
56
+ if expected_params.count != params.count
57
+ raise ArgumentError, "Wrong number of arguments: #{params.count} " \
58
+ "for #{expected_params.count}"
59
+ end
60
+
61
+ href.gsub(PARAMETER_REGEX) { |_match| format_parameter(params.shift) }
62
+ end
63
+
64
+ private
65
+
66
+ # Match parameters in definition strings.
67
+ PARAMETER_REGEX = /\{\([%\/a-zA-Z0-9_-]*\)\}/
68
+
69
+ def href
70
+ @link_schema_hash['href']
71
+ end
72
+
73
+ def expected_params
74
+ href.scan(PARAMETER_REGEX)
75
+ end
76
+
77
+ # Unpack an 'anyOf' or 'oneOf' multi-parameter blob.
78
+ #
79
+ # @param parameters [Array<Hash>] An array of hashes containing '$ref'
80
+ # keys and definition values.
81
+ # @return [Array<Parameter>] An array of parameters extracted from the
82
+ # blob.
83
+ def unpack_multi_params(parameters)
84
+ parameters.map { |info| generate_parameter(info['$ref']) }
85
+ end
86
+
87
+ def generate_parameter(param)
88
+ path = param.split('/')[1..-1]
89
+ name = path[-1]
90
+ resource_name = path.size > 2 ? path[1] : nil
91
+ info = @api_schema.lookup_path(*path)
92
+ description = info['description']
93
+
94
+ if info.key?('anyOf')
95
+ ParameterChoice.new(resource_name, unpack_multi_params(info['anyOf']))
96
+ elsif info.key?('oneOf')
97
+ ParameterChoice.new(resource_name, unpack_multi_params(info['oneOf']))
98
+ else
99
+ Parameter.new(resource_name, name, description)
100
+ end
101
+ end
102
+
103
+ # Convert a path parameter to a format suitable for use in a path.
104
+ #
105
+ # @param [Fixnum,String,TrueClass,FalseClass,Time] The parameter to format.
106
+ # @return [String] The formatted parameter.
107
+ def format_parameter(parameter)
108
+ parameter.instance_of?(Time) ? iso_format(parameter) : parameter.to_s
109
+ end
110
+
111
+ # Convert a time to an ISO 8601 combined data and time format.
112
+ #
113
+ # @param time [Time] The time to convert to ISO 8601 format.
114
+ # @return [String] An ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format.
115
+ def iso_format(time)
116
+ time.getutc.iso8601
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end