michael_taylor_sdk 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '03613032665901e43dc7312cdda7a49d87b9f83b64b7dfcdbdf770f4bfcc738c'
4
+ data.tar.gz: c38a19752db68e459cf04cc60e1ce40a0799f8728e4d2a8d9489544bcdbcb08b
5
+ SHA512:
6
+ metadata.gz: eb0ff8e3afeba6adbda9fe7384dc84f46609d9787e69b7680e486b0dd16c2e44d7b6a124e1aae4f2cf8769275fb06e4a5531a1fcf6d97fed29c7583e338385dc
7
+ data.tar.gz: b36786f16696fe52fbc8497792efd5b4cd0da946f205f6d8b5268ef5448416685042a5e2a089117a6d340bd2de7790881c7668cdda842263a26a52d40f5fc9bb
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+
5
+ Style/StringLiterals:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/StringLiteralsInInterpolation:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/ClassAndModuleChildren:
14
+ Enabled: false
15
+
16
+ Style/TrailingCommaInHashLiteral:
17
+ Enabled: true
18
+ EnforcedStyleForMultiline: comma
19
+
20
+ Style/TrailingCommaInArrayLiteral:
21
+ Enabled: true
22
+ EnforcedStyleForMultiline: comma
23
+
24
+ Layout/LineLength:
25
+ Max: 120
26
+
27
+ Metrics/AbcSize:
28
+ Exclude:
29
+ - spec/**/*.rb
30
+
31
+ Metrics/BlockLength:
32
+ Exclude:
33
+ - spec/**/*.rb
34
+
35
+ Metrics/MethodLength:
36
+ Exclude:
37
+ - spec/**/*.rb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.1
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in michael_taylor_sdk.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.12"
11
+
12
+ gem "rubocop", "~> 1.48"
data/Gemfile.lock ADDED
@@ -0,0 +1,57 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ michael_taylor_sdk (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ diff-lcs (1.5.0)
11
+ json (2.6.3)
12
+ parallel (1.22.1)
13
+ parser (3.2.1.1)
14
+ ast (~> 2.4.1)
15
+ rainbow (3.1.1)
16
+ rake (13.0.6)
17
+ regexp_parser (2.7.0)
18
+ rexml (3.2.5)
19
+ rspec (3.12.0)
20
+ rspec-core (~> 3.12.0)
21
+ rspec-expectations (~> 3.12.0)
22
+ rspec-mocks (~> 3.12.0)
23
+ rspec-core (3.12.1)
24
+ rspec-support (~> 3.12.0)
25
+ rspec-expectations (3.12.2)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.12.0)
28
+ rspec-mocks (3.12.4)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.12.0)
31
+ rspec-support (3.12.0)
32
+ rubocop (1.48.1)
33
+ json (~> 2.3)
34
+ parallel (~> 1.10)
35
+ parser (>= 3.2.0.0)
36
+ rainbow (>= 2.2.2, < 4.0)
37
+ regexp_parser (>= 1.8, < 3.0)
38
+ rexml (>= 3.2.5, < 4.0)
39
+ rubocop-ast (>= 1.26.0, < 2.0)
40
+ ruby-progressbar (~> 1.7)
41
+ unicode-display_width (>= 2.4.0, < 3.0)
42
+ rubocop-ast (1.27.0)
43
+ parser (>= 3.2.1.0)
44
+ ruby-progressbar (1.13.0)
45
+ unicode-display_width (2.4.2)
46
+
47
+ PLATFORMS
48
+ x86_64-linux
49
+
50
+ DEPENDENCIES
51
+ michael_taylor_sdk!
52
+ rake (~> 13.0)
53
+ rspec (~> 3.12)
54
+ rubocop (~> 1.48)
55
+
56
+ BUNDLED WITH
57
+ 2.4.6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Michael Taylor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # MichaelTaylorSdk
2
+
3
+ ## Installation
4
+
5
+ Install the gem and add to the application's Gemfile by executing:
6
+
7
+ $ bundle add michael_taylor_sdk
8
+
9
+ If bundler is not being used to manage dependencies, install the gem by executing:
10
+
11
+ $ gem install michael_taylor_sdk
12
+
13
+ ## Usage
14
+
15
+ An access key is required to use this SDK. Visit [The One API sign up](https://the-one-api.dev/sign-up) to get one.
16
+
17
+ Start using the SDK by passing the access key to get an instance of the SDK.
18
+
19
+ lotr_sdk = MichaelTaylorSdk::LordOfTheRings.new("YOUR_ACCESS_KEY")
20
+
21
+ Access one type of data by calling the appropriate method on the SDK, such as `.movies` to get information about one or more movies.
22
+
23
+ lotr_sdk.movies.list # Get all movies
24
+ lotr_sdk.movies.get("#{movie_id}") # Get one movie by ID
25
+
26
+ The SDK behavior can be changed with modifier methods. These can be chained.
27
+
28
+ # Return at most 10 results
29
+ lotr_sdk.paginated(limit: 10).movies.list
30
+
31
+ # Try to make the call up to 3 times
32
+ exponential_backoff = MichaelTaylorSdk::RetryStrategy::ExponentialBackoff.new(3)
33
+ lotr_sdk.with_retry_strategy(exponential_backoff).movies.list
34
+
35
+ # Both modifiers active
36
+ lotr_sdk.paginated(limit: 10).with_retry_strategy(exponential_backoff).movies.list
37
+
38
+ # Use modifiers more than once
39
+ lotr_sdk.paginated(limit: 10).with_retry_strategy(exponential_backoff) do |modified_sdk|
40
+ modified_sdk.movies.list
41
+ modified_sdk.movies.get("#{movie_id}")
42
+ end
43
+
44
+ Quotes from a movie can be queried by movie ID or name
45
+
46
+ lotr_sdk.movies.quotes_from_movie("#{movie_id}")
47
+ lotr_sdk.movies.quotes_from_movie_name("#{movie_name}")
48
+
49
+ ## Development
50
+
51
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
52
+
53
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
54
+
55
+ ## Contributing
56
+
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mwtaylor/michael_taylor_sdk.
58
+
59
+ ## License
60
+
61
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/api_paths/movies"
4
+
5
+ module MichaelTaylorSdk::ApiPaths
6
+ ##
7
+ # Mixin to add all available API paths to the parent object
8
+ module AllPaths
9
+ ##
10
+ # Provides queries related to movies
11
+ def movies
12
+ MichaelTaylorSdk::ApiPaths::Movies.new(@pipeline)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/pipeline/pipeline_modifiers"
4
+
5
+ module MichaelTaylorSdk::ApiPaths
6
+ ##
7
+ # Provides common methods for all API paths
8
+ class Base
9
+ include MichaelTaylorSdk::Pipeline::PipelineModifiers
10
+
11
+ attr_reader :pipeline
12
+
13
+ def initialize(pipeline)
14
+ @pipeline = pipeline
15
+ @pipeline = replace_existing_stage(
16
+ @pipeline,
17
+ :set_path,
18
+ ->(next_stage) { MichaelTaylorSdk::Pipeline::SetPath.new(next_stage, path) }
19
+ )
20
+ end
21
+
22
+ ##
23
+ # List all items
24
+ def list
25
+ execute_pipeline(@pipeline, {})
26
+ end
27
+
28
+ def get(id)
29
+ with_path(@pipeline, id_path(id)) do |pipeline|
30
+ execute_pipeline(pipeline, {})
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def id_path(id)
37
+ "#{path}/#{id}"
38
+ end
39
+
40
+ def execute_pipeline(pipeline_initializer, input)
41
+ pipeline = pipeline_initializer.call
42
+ stage_initializers = pipeline[:stages].map { |stage_key| pipeline[stage_key] }
43
+ next_stage = initialize_pipeline_stages(stage_initializers)
44
+ next_stage.execute_http_request(input)
45
+ next_stage.result_hash
46
+ end
47
+
48
+ def initialize_pipeline_stages(stage_initializers)
49
+ next_stage = nil
50
+ stage_initializers.reverse.each do |stage_initializer|
51
+ next_stage = if next_stage.nil?
52
+ stage_initializer.call
53
+ else
54
+ stage_initializer.call(next_stage)
55
+ end
56
+ end
57
+ next_stage
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("base")
4
+
5
+ require "michael_taylor_sdk/pipeline/set_path"
6
+ require "michael_taylor_sdk/constants"
7
+
8
+ module MichaelTaylorSdk::ApiPaths
9
+ ##
10
+ # Queries related to /movie
11
+ class Movies < Base
12
+ include MichaelTaylorSdk::Constants
13
+
14
+ ##
15
+ # Find all quotes for a given movie
16
+ def quotes_from_movie(id)
17
+ with_path(@pipeline, quote_path(id)) do |pipeline|
18
+ execute_pipeline(pipeline, {})
19
+ end
20
+ end
21
+
22
+ ##
23
+ # Lookup a movie by name and get all quotes from it
24
+ def quotes_from_movie_name(name)
25
+ movies = with_filters(@pipeline, ["name=#{name}"]) do |filtered_pipeline|
26
+ with_pagination(filtered_pipeline, limit: 1) do |paginated_pipeline|
27
+ execute_pipeline(paginated_pipeline, {})
28
+ end
29
+ end
30
+ matching_movie = movies[:items][0]
31
+ quotes_from_movie(matching_movie[:_id])
32
+ end
33
+
34
+ protected
35
+
36
+ def path
37
+ MOVIE_PATH
38
+ end
39
+
40
+ def quote_path(id)
41
+ "#{MOVIE_PATH}/#{id}/#{QUOTE_PATH}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk
4
+ module Constants
5
+ MOVIE_PATH = "movie"
6
+ QUOTE_PATH = "quote"
7
+
8
+ EXPONENTIAL_BACKOFF_TIME_DIVISOR = 20
9
+ end
10
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::Errors
4
+ ##
5
+ # Errors related to HTTP response codes
6
+ class HttpError < StandardError
7
+ def initialize(message_prefix, response)
8
+ super("(#{message_prefix}) #{response.code}: #{response.message}")
9
+ end
10
+ end
11
+
12
+ ##
13
+ # Errors in the HTTP 5XX range
14
+ class ServerError < HttpError
15
+ def initialize(response)
16
+ super("Server Error", response)
17
+ end
18
+ end
19
+
20
+ ##
21
+ # Errors in the HTTP 4XX range
22
+ class ClientError < HttpError
23
+ def initialize(response)
24
+ super("Client Error", response)
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Errors related to processing the returned content
30
+ class ContentError < StandardError
31
+ def initialize(msg = nil)
32
+ super(msg)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Error caused by a JSON parser error
38
+ class JsonParseError < ContentError
39
+ end
40
+
41
+ ##
42
+ # Error caused by no content being returned by the server
43
+ class NoContentError < ContentError
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/api_paths/all_paths"
4
+ require "michael_taylor_sdk/pipeline/pipeline_modifiers"
5
+
6
+ module MichaelTaylorSdk
7
+ ##
8
+ # All modifiers that can modify the SDK behavior
9
+ module Modifiers
10
+ ##
11
+ # Replaces the default retry strategy
12
+ def with_retry_strategy(retry_strategy)
13
+ new_retry_stage = lambda { |next_stage|
14
+ MichaelTaylorSdk::Pipeline::Retry.new(next_stage, retry_strategy)
15
+ }
16
+ new_pipeline = replace_existing_stage(@pipeline, :retry, new_retry_stage)
17
+ modified_sdk = MichaelTaylorSdk::ModifiedSdk.new(new_pipeline)
18
+ if block_given?
19
+ yield(modified_sdk)
20
+ else
21
+ modified_sdk
22
+ end
23
+ end
24
+
25
+ def paginated(limit: nil, page: nil, offset: nil)
26
+ with_pagination(@pipeline, limit: limit, page: page, offset: offset) do |paginated_pipeline|
27
+ modified_sdk = MichaelTaylorSdk::ModifiedSdk.new(paginated_pipeline)
28
+ if block_given?
29
+ yield(modified_sdk)
30
+ else
31
+ modified_sdk
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ ##
38
+ # An instance of the SDK with modified behavior
39
+ class ModifiedSdk
40
+ include Modifiers
41
+ include MichaelTaylorSdk::ApiPaths::AllPaths
42
+ include MichaelTaylorSdk::Pipeline::PipelineModifiers
43
+
44
+ def initialize(pipeline)
45
+ @pipeline = pipeline
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::Pipeline
4
+ ##
5
+ # Pipeline stage that adds filters to list requests
6
+ class Filters
7
+ def initialize(next_pipeline_stage, filters)
8
+ @next_pipeline_stage = next_pipeline_stage
9
+ @filters = filters
10
+ end
11
+
12
+ def execute_http_request(request_details)
13
+ request_details[:query_parameters] = {} unless request_details.key?(:query_parameters)
14
+ @filters.each do |filter|
15
+ request_details[:query_parameters][filter] = nil
16
+ end
17
+
18
+ @next_pipeline_stage.execute_http_request(request_details)
19
+ end
20
+
21
+ def result_hash
22
+ @next_pipeline_stage.result_hash
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+
6
+ module MichaelTaylorSdk::Pipeline
7
+ ##
8
+ # Final pipeline stage that sends an HTTP request to the server
9
+ class GetRequest
10
+ HEADER_AUTHORIZATION = "Authorization"
11
+ AUTHENTICATION_SCHEME_BEARER = "Bearer"
12
+
13
+ def initialize(base_url, authorization_token)
14
+ @base_url = base_url
15
+ @authorization_token = authorization_token
16
+ end
17
+
18
+ def execute_http_request(request_details)
19
+ request = Net::HTTP::Get.new(uri(request_details))
20
+ add_headers(request)
21
+
22
+ @response = Net::HTTP.start(request.uri.hostname, use_ssl: true) do |http|
23
+ http.request(request)
24
+ end
25
+ end
26
+
27
+ def http_response
28
+ @response
29
+ end
30
+
31
+ private
32
+
33
+ def query_string(request_details)
34
+ request_details[:query_parameters]
35
+ .map { |key, value| value.nil? ? key : "#{key}=#{value}" }
36
+ .join("&")
37
+ end
38
+
39
+ def uri(request_details)
40
+ uri = "#{@base_url}/#{request_details[:path]}"
41
+ if request_details.key?(:query_parameters) && !request_details[:query_parameters].empty?
42
+ uri += "?#{query_string(request_details)}"
43
+ end
44
+ URI(uri)
45
+ end
46
+
47
+ def add_headers(request)
48
+ request[HEADER_AUTHORIZATION] = "#{AUTHENTICATION_SCHEME_BEARER} #{@authorization_token}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module MichaelTaylorSdk::Pipeline
6
+ ##
7
+ # Pipeline stage to transform an HTTP body to JSON
8
+ class Json
9
+ def initialize(next_pipeline_stage)
10
+ @next_pipeline_stage = next_pipeline_stage
11
+ end
12
+
13
+ def execute_http_request(request_details)
14
+ @result = @next_pipeline_stage.execute_http_request(request_details)
15
+ end
16
+
17
+ def result_hash
18
+ JSON.parse(@next_pipeline_stage.http_body, symbolize_names: true)
19
+ rescue JSON::ParserError => e
20
+ raise MichaelTaylorSdk::Errors::JsonParseError, "JSON response could not be parsed: #{e.message}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::Pipeline
4
+ ##
5
+ # Pipeline stage that adds pagination parameters and adds pagination details into response
6
+ class Paginate
7
+ def initialize(next_pipeline_stage, limit: nil, page: nil, offset: nil)
8
+ @next_pipeline_stage = next_pipeline_stage
9
+ @limit = limit
10
+ @page = page
11
+ @offset = offset
12
+ end
13
+
14
+ def execute_http_request(request_details)
15
+ request_details[:query_parameters] = {} unless request_details.key?(:query_parameters)
16
+ request_details[:query_parameters]["limit"] = @limit unless @limit.nil?
17
+ request_details[:query_parameters]["page"] = @page unless @page.nil?
18
+ request_details[:query_parameters]["offset"] = @offset unless @offset.nil?
19
+
20
+ @next_pipeline_stage.execute_http_request(request_details)
21
+ end
22
+
23
+ def result_hash
24
+ next_result = @next_pipeline_stage.result_hash
25
+
26
+ {
27
+ items: next_result[:docs],
28
+ pagination: next_result.slice(:total, :limit, :offset, :page, :pages),
29
+ }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::Pipeline
4
+ ##
5
+ # Common methods to work with pipelines
6
+ module PipelineModifiers
7
+ ##
8
+ # Replace a stage in a pipeline with a modified version with different parameters
9
+ def replace_existing_stage(pipeline, next_stage_key, next_stage_lambda)
10
+ lambda {
11
+ modified_pipeline = pipeline.call
12
+ modified_pipeline[next_stage_key] = next_stage_lambda
13
+ modified_pipeline
14
+ }
15
+ end
16
+
17
+ def with_path(pipeline, path)
18
+ yield replace_existing_stage(
19
+ pipeline,
20
+ :set_path,
21
+ ->(next_stage) { MichaelTaylorSdk::Pipeline::SetPath.new(next_stage, path) }
22
+ )
23
+ end
24
+
25
+ def with_filters(pipeline, filters)
26
+ yield replace_existing_stage(
27
+ pipeline,
28
+ :filter,
29
+ ->(next_stage) { MichaelTaylorSdk::Pipeline::Filters.new(next_stage, filters) }
30
+ )
31
+ end
32
+
33
+ def with_pagination(pipeline, limit: nil, page: nil, offset: nil)
34
+ new_paginate_stage = lambda { |next_stage|
35
+ MichaelTaylorSdk::Pipeline::Paginate.new(next_stage, limit: limit, page: page, offset: offset)
36
+ }
37
+ yield replace_existing_stage(pipeline, :paginate, new_paginate_stage)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/errors"
4
+
5
+ module MichaelTaylorSdk::Pipeline
6
+ ##
7
+ # Pipeline stage that raises errors when the HTTP response is a 4xx or 5xx error
8
+ class RaiseHttpErrors
9
+ def initialize(next_pipeline_stage)
10
+ @next_pipeline_stage = next_pipeline_stage
11
+ end
12
+
13
+ def execute_http_request(request_details)
14
+ response = @next_pipeline_stage.execute_http_request(request_details)
15
+ if response.is_a? Net::HTTPServerError
16
+ raise MichaelTaylorSdk::Errors::ServerError, response
17
+ elsif response.is_a? Net::HTTPClientError
18
+ raise MichaelTaylorSdk::Errors::ClientError, response
19
+ end
20
+ end
21
+
22
+ def http_response
23
+ @next_pipeline_stage.http_response
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::Pipeline
4
+ ##
5
+ # Pipeline stage to get body content from HTTP response
6
+ class ResponseBody
7
+ def initialize(next_pipeline_stage)
8
+ @next_pipeline_stage = next_pipeline_stage
9
+ end
10
+
11
+ def execute_http_request(request_details)
12
+ @next_pipeline_stage.execute_http_request(request_details)
13
+ end
14
+
15
+ def http_body
16
+ body = @next_pipeline_stage.http_response.body
17
+ raise MichaelTaylorSdk::Errors::NoContentError, "Server response was empty" if body.empty?
18
+
19
+ body
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/retry_strategy/one_try"
4
+
5
+ module MichaelTaylorSdk::Pipeline
6
+ ## Pipeline stage to run a given retry strategy
7
+ class Retry
8
+ def initialize(next_pipeline_stage, retry_strategy)
9
+ @next_pipeline_stage = next_pipeline_stage
10
+ @retry_strategy = retry_strategy
11
+ end
12
+
13
+ def execute_http_request(request_details)
14
+ @retry_strategy.run(-> { @next_pipeline_stage.execute_http_request(request_details) })
15
+ end
16
+
17
+ def http_response
18
+ @next_pipeline_stage.http_response
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::Pipeline
4
+ ##
5
+ # Pipeline stage to set the path into the HTTP request
6
+ class SetPath
7
+ def initialize(next_pipeline_stage, path)
8
+ @next_pipeline_stage = next_pipeline_stage
9
+ @path = path
10
+ end
11
+
12
+ def execute_http_request(request_details)
13
+ request_details[:path] = @path
14
+
15
+ @next_pipeline_stage.execute_http_request(request_details)
16
+ end
17
+
18
+ def http_response
19
+ @next_pipeline_stage.http_response
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/constants"
4
+
5
+ module MichaelTaylorSdk::RetryStrategy
6
+ ##
7
+ # Retry strategy for exponential backoffs
8
+ #
9
+ # This will rerun an HTTP call up to max_tries times. Each retry will be delayed.
10
+ # The delay will have an exponential backoff that makes each delay about twice as long as the previous delay.
11
+ # The delay will also have a random jitter added to avoid overloading the server from many clients
12
+ # retrying simultaneously.
13
+ class ExponentialBackoff
14
+ include MichaelTaylorSdk::Constants
15
+
16
+ def initialize(max_tries)
17
+ @max_tries = max_tries
18
+ end
19
+
20
+ def run(do_request)
21
+ tries = 0
22
+ begin
23
+ tries += 1
24
+ do_request.call
25
+ rescue MichaelTaylorSdk::Errors::ServerError
26
+ raise if tries == @max_tries
27
+
28
+ jittered_sleep(tries)
29
+
30
+ retry
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def jittered_sleep(tries)
37
+ max_sleep_seconds = Float(2**tries) / EXPONENTIAL_BACKOFF_TIME_DIVISOR
38
+ sleep(rand((0.5 * max_sleep_seconds)..max_sleep_seconds))
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk::RetryStrategy
4
+ ##
5
+ # The default retry strategy that just runs a request once and passes along any errors
6
+ class OneTry
7
+ def run(do_request)
8
+ do_request.call
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MichaelTaylorSdk
4
+ VERSION = "1.0.1"
5
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "michael_taylor_sdk/version"
4
+
5
+ require "michael_taylor_sdk/pipeline/pipeline_modifiers"
6
+ require "michael_taylor_sdk/pipeline/paginate"
7
+ require "michael_taylor_sdk/pipeline/filters"
8
+ require "michael_taylor_sdk/pipeline/json"
9
+ require "michael_taylor_sdk/pipeline/response_body"
10
+ require "michael_taylor_sdk/pipeline/retry"
11
+ require "michael_taylor_sdk/pipeline/raise_http_errors"
12
+ require "michael_taylor_sdk/pipeline/get_request"
13
+ require "michael_taylor_sdk/api_paths/all_paths"
14
+ require "michael_taylor_sdk/modified_sdk"
15
+ require "michael_taylor_sdk/constants"
16
+
17
+ module MichaelTaylorSdk
18
+ ##
19
+ # Entry point into the SDK
20
+ class LordOfTheRings
21
+ include MichaelTaylorSdk::Pipeline::PipelineModifiers
22
+ include MichaelTaylorSdk::ApiPaths::AllPaths
23
+ include MichaelTaylorSdk::Modifiers
24
+
25
+ DEFAULT_BASE_URL = "https://the-one-api.dev/v2"
26
+
27
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
28
+ def initialize(
29
+ access_token,
30
+ base_url: DEFAULT_BASE_URL,
31
+ default_retry_strategy: MichaelTaylorSdk::RetryStrategy::OneTry.new
32
+ )
33
+
34
+ raise "Access token must be given as a string" unless access_token.is_a? String
35
+
36
+ @default_pipeline = lambda {
37
+ {
38
+ paginate: ->(next_stage) { MichaelTaylorSdk::Pipeline::Paginate.new(next_stage) },
39
+ filter: ->(next_stage) { MichaelTaylorSdk::Pipeline::Filters.new(next_stage, []) },
40
+ json: ->(next_stage) { MichaelTaylorSdk::Pipeline::Json.new(next_stage) },
41
+ response_body: ->(next_stage) { MichaelTaylorSdk::Pipeline::ResponseBody.new(next_stage) },
42
+ set_path: ->(next_stage) {},
43
+ retry: ->(next_stage) { MichaelTaylorSdk::Pipeline::Retry.new(next_stage, default_retry_strategy) },
44
+ raise_http_errors: ->(next_stage) { MichaelTaylorSdk::Pipeline::RaiseHttpErrors.new(next_stage) },
45
+ get_request: -> { MichaelTaylorSdk::Pipeline::GetRequest.new(base_url, access_token) },
46
+ stages: %i[paginate filter json response_body set_path retry raise_http_errors get_request],
47
+ }
48
+ }
49
+
50
+ @pipeline = @default_pipeline
51
+ end
52
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/michael_taylor_sdk/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "michael_taylor_sdk"
7
+ spec.version = MichaelTaylorSdk::VERSION
8
+ spec.authors = ["Michael Taylor"]
9
+ spec.email = ["mwtaylor@users.noreply.github.com"]
10
+
11
+ spec.summary = "SDK for the Lord of the Rings API"
12
+ spec.homepage = "https://github.com/mwtaylor/MichaelTaylor-SDK"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+
16
+ spec.metadata["rubygems_mfa_required"] = "true"
17
+ spec.metadata["github_repo"] = "https://github.com/mwtaylor/MichaelTaylor-SDK"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ # spec.add_dependency "example-gem", "~> 1.0"
32
+
33
+ # For more information and examples about making a new gem, check out our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
@@ -0,0 +1,4 @@
1
+ module MichaelTaylorSdk
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: michael_taylor_sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Michael Taylor
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - mwtaylor@users.noreply.github.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - ".rubocop.yml"
22
+ - ".ruby-version"
23
+ - Gemfile
24
+ - Gemfile.lock
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - lib/michael_taylor_sdk.rb
29
+ - lib/michael_taylor_sdk/api_paths/all_paths.rb
30
+ - lib/michael_taylor_sdk/api_paths/base.rb
31
+ - lib/michael_taylor_sdk/api_paths/movies.rb
32
+ - lib/michael_taylor_sdk/constants.rb
33
+ - lib/michael_taylor_sdk/errors.rb
34
+ - lib/michael_taylor_sdk/modified_sdk.rb
35
+ - lib/michael_taylor_sdk/pipeline/filters.rb
36
+ - lib/michael_taylor_sdk/pipeline/get_request.rb
37
+ - lib/michael_taylor_sdk/pipeline/json.rb
38
+ - lib/michael_taylor_sdk/pipeline/paginate.rb
39
+ - lib/michael_taylor_sdk/pipeline/pipeline_modifiers.rb
40
+ - lib/michael_taylor_sdk/pipeline/raise_http_errors.rb
41
+ - lib/michael_taylor_sdk/pipeline/response_body.rb
42
+ - lib/michael_taylor_sdk/pipeline/retry.rb
43
+ - lib/michael_taylor_sdk/pipeline/set_path.rb
44
+ - lib/michael_taylor_sdk/retry_strategy/exponential_backoff.rb
45
+ - lib/michael_taylor_sdk/retry_strategy/one_try.rb
46
+ - lib/michael_taylor_sdk/version.rb
47
+ - michael_taylor_sdk.gemspec
48
+ - sig/michael_taylor_sdk.rbs
49
+ homepage: https://github.com/mwtaylor/MichaelTaylor-SDK
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ rubygems_mfa_required: 'true'
54
+ github_repo: https://github.com/mwtaylor/MichaelTaylor-SDK
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.0.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.4.6
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: SDK for the Lord of the Rings API
74
+ test_files: []