jahuty 2.1.0 → 3.2.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.
data/lib/jahuty.rb CHANGED
@@ -1,23 +1,33 @@
1
- require "jahuty/version"
1
+ # frozen_string_literal: true
2
2
 
3
- require "jahuty/snippet"
3
+ require 'jahuty/version'
4
4
 
5
- require "jahuty/data/problem"
6
- require "jahuty/data/render"
5
+ require 'jahuty/action/base'
6
+ require 'jahuty/action/index'
7
+ require 'jahuty/action/show'
7
8
 
8
- require "jahuty/exception/not_ok"
9
+ require 'jahuty/api/client'
9
10
 
10
- require "jahuty/service/connect"
11
- require "jahuty/service/render"
11
+ require 'jahuty/cache/facade'
12
12
 
13
- module Jahuty
14
- @key
13
+ require 'jahuty/exception/error'
14
+
15
+ require 'jahuty/request/base'
16
+ require 'jahuty/request/factory'
17
+
18
+ require 'jahuty/resource/problem'
19
+ require 'jahuty/resource/render'
20
+ require 'jahuty/resource/factory'
21
+
22
+ require 'jahuty/response/handler'
15
23
 
16
- class << self
17
- attr_accessor :key
24
+ require 'jahuty/service/base'
25
+ require 'jahuty/service/snippet'
18
26
 
19
- def key?
20
- !(@key.nil? || @key.empty?)
21
- end
22
- end
27
+ require 'jahuty/client'
28
+
29
+ require 'jahuty/util'
30
+
31
+ module Jahuty
32
+ BASE_URI = 'https://api.jahuty.com'
23
33
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Action
5
+ # Provides common logic for service actions.
6
+ class Base
7
+ attr_accessor :resource, :params
8
+
9
+ def initialize(resource:, params: {})
10
+ @resource = resource
11
+ @params = params
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Action
5
+ # Displays a collection of resources.
6
+ class Index < Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Action
5
+ # Displays a specific resource.
6
+ class Show < Base
7
+ attr_accessor :id
8
+
9
+ def initialize(resource:, id:, params: {})
10
+ @id = id
11
+
12
+ super(resource: resource, params: params)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Jahuty
6
+ module Api
7
+ # Handles HTTP requests and responses.
8
+ class Client
9
+ HEADERS = {
10
+ 'Accept' => 'application/json;q=0.9,*/*;q=0.8',
11
+ 'Accept-Encoding' => 'gzip, deflate',
12
+ 'Content-Type' => 'application/json; charset=utf-8',
13
+ 'User-Agent' => "Jahuty Ruby SDK v#{::Jahuty::VERSION}"
14
+ }.freeze
15
+
16
+ def initialize(api_key:)
17
+ @api_key = api_key
18
+ end
19
+
20
+ def send(request)
21
+ @client ||= Faraday.new(url: ::Jahuty::BASE_URI, headers: headers)
22
+
23
+ # Cnvert the action's string method to Faraday's verb-based methods.
24
+ @client.send(
25
+ request.method.to_sym,
26
+ request.path,
27
+ request.params
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def headers
34
+ { 'Authorization' => "Bearer #{@api_key}" }.merge(HEADERS)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Cache
5
+ # Abstracts away the differences in cache implementation methods and
6
+ # argument lists.
7
+ class Facade
8
+ def initialize(cache)
9
+ @cache = cache
10
+ end
11
+
12
+ def delete(key)
13
+ if @cache.respond_to? :delete
14
+ @cache.delete key
15
+ elsif @cache.respond_to? :unset
16
+ @cache.unset key
17
+ else
18
+ raise NoMethodError, 'Cache must respond to :delete or :unset'
19
+ end
20
+ end
21
+
22
+ def read(key)
23
+ if @cache.respond_to? :read
24
+ @cache.read key
25
+ elsif @cache.respond_to? :get
26
+ @cache.get key
27
+ else
28
+ raise NoMethodError, 'Cache must respond to :read or :get'
29
+ end
30
+ end
31
+
32
+ def write(key, value, expires_in: nil)
33
+ if Object.const_defined?('::ActiveSupport::Cache::Store') &&
34
+ @cache.is_a?(::ActiveSupport::Cache::Store)
35
+ @cache.write key, value, expires_in: expires_in, race_condition_ttl: 10
36
+ elsif @cache.respond_to? :write
37
+ @cache.write key, value, expires_in: expires_in
38
+ elsif @cache.respond_to? :set
39
+ @cache.set key, value, expires_in: expires_in
40
+ else
41
+ raise NoMethodError, 'Cache must respond to :write or :set'
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mini_cache'
4
+
5
+ module Jahuty
6
+ # Executes requests against Jahuty's API and returns resources.
7
+ class Client
8
+ def initialize(api_key:, cache: nil, expires_in: nil)
9
+ @api_key = api_key
10
+ @cache = Cache::Facade.new(cache || ::MiniCache::Store.new)
11
+ @expires_in = expires_in
12
+ @services = {}
13
+ end
14
+
15
+ # Allows services to be accessed as properties (e.g., jahuty.snippets).
16
+ def method_missing(name, *args, &block)
17
+ if args.empty? && name == :snippets
18
+ unless @services.key?(name)
19
+ @services[name] = Service::Snippet.new(
20
+ client: self, cache: @cache, expires_in: @expires_in
21
+ )
22
+ end
23
+ @services[name]
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def request(action)
30
+ @requests ||= Request::Factory.new
31
+
32
+ request = @requests.call(action)
33
+
34
+ @client ||= Api::Client.new(api_key: @api_key)
35
+
36
+ response = @client.send(request)
37
+
38
+ @responses ||= Response::Handler.new
39
+
40
+ result = @responses.call(action, response)
41
+
42
+ raise Exception::Error.new(result), 'API problem' if result.is_a?(Resource::Problem)
43
+
44
+ result
45
+ end
46
+
47
+ def respond_to_missing?(name, include_private = false)
48
+ name == :snippets || super
49
+ end
50
+ end
51
+ end
@@ -1,10 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Jahuty
2
4
  module Exception
3
- class NotOk < ::StandardError
5
+ # Thrown when a client- or server-error occurs.
6
+ class Error < ::StandardError
4
7
  attr_reader :problem
5
8
 
6
9
  def initialize(problem)
7
10
  @problem = problem
11
+
12
+ super
8
13
  end
9
14
 
10
15
  def message
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Request
5
+ # Provides common logic for all requests.
6
+ class Base
7
+ attr_accessor :method, :path, :params
8
+
9
+ def initialize(method:, path:, params: {})
10
+ @method = method
11
+ @path = path
12
+ @params = params
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Request
5
+ # Instantiates a request from an action.
6
+ class Factory
7
+ def call(action)
8
+ Base.new(
9
+ method: 'get',
10
+ path: path(action),
11
+ params: action.params
12
+ )
13
+ end
14
+
15
+ private
16
+
17
+ def path(action)
18
+ case action
19
+ when ::Jahuty::Action::Show
20
+ "snippets/#{action.id}/render"
21
+ when ::Jahuty::Action::Index
22
+ 'snippets/render'
23
+ else
24
+ raise ArgumentError, 'Action is not supported'
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Resource
5
+ # Instantiates and returns a resource.
6
+ class Factory
7
+ CLASSES = {
8
+ problem: Problem.name,
9
+ render: Render.name
10
+ }.freeze
11
+
12
+ def call(resource_name, payload)
13
+ klass = class_name(resource_name.to_sym)
14
+
15
+ raise ArgumentError, "#{resource_name} missing" if klass.nil?
16
+
17
+ Object.const_get(klass).send(:from, **payload)
18
+ end
19
+
20
+ private
21
+
22
+ def class_name(resource_name)
23
+ CLASSES[resource_name.to_sym]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Resource
5
+ # An application/problem+json response. The API should respond with a
6
+ # problem whenever a client- or server-error occurs.
7
+ class Problem
8
+ attr_accessor :status, :type, :detail
9
+
10
+ def initialize(status:, type:, detail:)
11
+ @status = status
12
+ @type = type
13
+ @detail = detail
14
+ end
15
+
16
+ def self.from(data)
17
+ raise ArgumentError.new, 'Key :status missing' unless data.key?(:status)
18
+ raise ArgumentError.new, 'Key :type missing' unless data.key?(:type)
19
+ raise ArgumentError.new, 'Key :detail missing' unless data.key?(:detail)
20
+
21
+ Problem.new(data.slice(:status, :type, :detail))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Resource
5
+ # A snippet's rendered content.
6
+ class Render
7
+ attr_accessor :content, :snippet_id
8
+
9
+ def initialize(content:, snippet_id:)
10
+ @content = content
11
+ @snippet_id = snippet_id
12
+ end
13
+
14
+ def self.from(data)
15
+ raise ArgumentError.new, 'Key :content missing' unless data.key?(:content)
16
+ raise ArgumentError.new, 'Key :snippet_id missing' unless data.key?(:snippet_id)
17
+
18
+ Render.new(data.slice(:content, :snippet_id))
19
+ end
20
+
21
+ def to_s
22
+ @content
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Jahuty
6
+ module Response
7
+ # Inspects the response and returns the appropriate resource or collection.
8
+ class Handler
9
+ def call(action, response)
10
+ resource_name = name_resource action, response
11
+
12
+ payload = parse response
13
+
14
+ @resources ||= ::Jahuty::Resource::Factory.new
15
+
16
+ if collection?(action, payload)
17
+ payload.map { |data| @resources.call resource_name, data }
18
+ elsif resource?(action, payload)
19
+ @resources.call resource_name, payload
20
+ else
21
+ raise ArgumentError, 'Action and payload mismatch'
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def collection?(action, payload)
28
+ action.is_a?(Action::Index) && payload.is_a?(::Array)
29
+ end
30
+
31
+ def name_resource(action, response)
32
+ if success? response
33
+ action.resource
34
+ elsif problem? response
35
+ 'problem'
36
+ else
37
+ raise ArgumentError, 'Unexpected response'
38
+ end
39
+ end
40
+
41
+ def parse(response)
42
+ JSON.parse(response.body, symbolize_names: true)
43
+ end
44
+
45
+ def problem?(response)
46
+ response.headers['Content-Type'].include?('application/problem+json') &&
47
+ (response.status < 200 || response.status >= 300)
48
+ end
49
+
50
+ def resource?(action, payload)
51
+ !action.is_a?(Action::Index) && payload.is_a?(::Object)
52
+ end
53
+
54
+ def success?(response)
55
+ response.headers['Content-Type'].include?('application/json') &&
56
+ response.status.between?(200, 299)
57
+ end
58
+ end
59
+ end
60
+ end