atum 0.5.0

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 (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