mayak 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,105 @@
1
+ # Http
2
+
3
+ This module contains abstraction for HTTP interactions. It provides data classes that models HTTP requests and response as well interfaces for http client and codecs.
4
+
5
+ #### Verb
6
+
7
+ Enumaration that encodes an HTTP verb.
8
+
9
+ ```ruby
10
+ get_verb = Mayak::Http::Verb::Get
11
+ post_verb = Mayak::Http::Verb::Post
12
+ head_verb = Mayak::Http::Verb::Head
13
+ put_verb = Mayak::Http::Verb::Put
14
+ patch_verb = Mayak::Http::Verb::Patch
15
+ delete_verb = Mayak::Http::Verb::Delete
16
+ connect_verb = Mayak::Http::Verb::Connect
17
+ options_verb = Mayak::Http::Verb::Options
18
+ trace_verb = Mayak::Http::Verb::Trace
19
+ ```
20
+
21
+ #### Request
22
+
23
+ `Mayak::Http::Request` is a datastructure that encodes an HTTP request.
24
+ ```ruby
25
+ # to build an HTTP request verb and URI should be provided, as well as optional headers hash and body
26
+ request = Mayak::Http::Request.new(
27
+ verb: Mayak::Http::Verb::Put,
28
+ url: URI.parse("https://www.foobar.com/users/update"),
29
+ headers: { "content-type" => "application/json" },
30
+ body: """{ id: 100, name: "Daniil" })"""
31
+ )
32
+
33
+ # to build request helper constructor methods can be used
34
+ get_request = Mayak::Http::Request.get(url: URI.parse("https://www.foobar.com"))
35
+ ```
36
+
37
+ #### Response
38
+
39
+ `Mayak::Http::Response` is a datastructure that encodes an HTTP request. It contains status, and optional headers and body:
40
+
41
+ ```ruby
42
+ response = Mayak::Http::Response.new(
43
+ status: 200
44
+ headers: { "content-type" => "application/json" },
45
+ body: """{ id: 100, name: "Daniil" }"""
46
+ )
47
+ ```
48
+
49
+ #### Client
50
+
51
+ Interface that encodes an HTTP client. The interface is very simple and consists from one method that receives a request and returns a `Try` monad containg response:
52
+ ```ruby
53
+ module Mayak
54
+ module Http
55
+ module Client
56
+ extend T::Sig
57
+ extend T::Generic
58
+
59
+ interface!
60
+
61
+ sig { abstract.params(request: Http::Request).returns(Mayak::Monads::Try[Mayak::Http::Response]) }
62
+ def send_request(request)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ ```
68
+
69
+ You can implement this interface using a specific library:
70
+
71
+ ```ruby
72
+ class FaradayClient
73
+ extend T::Sig
74
+
75
+ include Mayak::Http::Client
76
+
77
+ include Mayak::Monads::Try::Mixin
78
+
79
+ sig { params(config_blk: T.nilable(T.proc.params(connection: Faraday::Connection).void)).void }
80
+ def initialize(&config_blk)
81
+ @faraday_instance = T.let(
82
+ config_blk.nil? ? Faraday.new : Faraday.new(&@config_blk),
83
+ Faraday
84
+ )
85
+ end
86
+
87
+ sig { override.params(request: Mayak::Http::Request).returns(Mayak::Monads::Try[Mayak::Http::Response]) }
88
+ def send_request(request)
89
+ Try do
90
+ faraday_response = @faraday_instance.run_request(
91
+ verb.serialize.downcase.to_sym,
92
+ request.url,
93
+ request.body,
94
+ request.headers
95
+ )
96
+
97
+ Mayak::Http::Response.new(
98
+ status: T.must(faraday_response.status.to_i),
99
+ headers: faraday_response.headers || {},
100
+ body: faraday_response.body || ""
101
+ )
102
+ end
103
+ end
104
+ end
105
+ ```
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module Mayak
5
+ module Http
6
+ module Client
7
+ extend T::Sig
8
+ extend T::Generic
9
+
10
+ interface!
11
+
12
+ sig { abstract.params(request: Http::Request).returns(Mayak::Monads::Try[Mayak::Http::Response]) }
13
+ def send_request(request)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'encoder'
5
+ require_relative 'decoder'
6
+
7
+ module Mayak
8
+ module Http
9
+ module Codec
10
+ extend T::Sig
11
+ extend T::Generic
12
+ extend T::Helpers
13
+
14
+ interface!
15
+
16
+ include Mayak::Http::Encoder
17
+ include Mayak::Http::Decoder
18
+
19
+ RequestEntity = type_member
20
+ ResponseEntity = type_member
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module Mayak
5
+ module Http
6
+ module Decoder
7
+ extend T::Sig
8
+ extend T::Generic
9
+ extend T::Helpers
10
+
11
+ interface!
12
+
13
+ ResponseEntity = type_member
14
+
15
+ sig {
16
+ abstract
17
+ .params(response: Mayak::Http::Response)
18
+ .returns(Mayak::Monads::Try[ResponseEntity])
19
+ }
20
+ def decode(response)
21
+ end
22
+
23
+ class IdentityDecoder
24
+ extend T::Sig
25
+ extend T::Generic
26
+ extend T::Helpers
27
+
28
+ include ::Mayak::Http::Decoder
29
+
30
+ ResponseEntity = type_member { { fixed: ::Mayak::Http::Response } }
31
+
32
+ sig {
33
+ override
34
+ .params(response: Mayak::Http::Response)
35
+ .returns(Mayak::Monads::Try[ResponseEntity])
36
+ }
37
+ def decode(response)
38
+ Mayak::Monads::Try::Success[ResponseEntity].new(response)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module Mayak
5
+ module Http
6
+ module Encoder
7
+ extend T::Sig
8
+ extend T::Generic
9
+ extend T::Helpers
10
+
11
+ interface!
12
+
13
+ RequestEntity = type_member
14
+
15
+ sig { abstract.params(entity: RequestEntity).returns(Mayak::Http::Request) }
16
+ def encode(entity)
17
+ end
18
+
19
+ class IdentityEncoder
20
+ extend T::Sig
21
+ extend T::Generic
22
+ extend T::Helpers
23
+
24
+ include ::Mayak::Http::Encoder
25
+
26
+ RequestEntity = type_member { { fixed: ::Mayak::Http::Request } }
27
+
28
+ sig { override.params(entity: RequestEntity).returns(Mayak::Http::Request) }
29
+ def encode(entity)
30
+ entity
31
+ end
32
+ end
33
+
34
+ class FromFunction
35
+ extend T::Sig
36
+ extend T::Generic
37
+ extend T::Helpers
38
+
39
+ include ::Mayak::Http::Encoder
40
+
41
+ RequestEntity = type_member
42
+
43
+ sig { params(function: Mayak::Function[RequestEntity, Mayak::Http::Request]).void }
44
+ def initialize(function)
45
+ @function = T.let(function, Mayak::Function[RequestEntity, Mayak::Http::Request])
46
+ end
47
+
48
+ sig { override.params(entity: RequestEntity).returns(Mayak::Http::Request) }
49
+ def encode(entity)
50
+ @function.call(entity)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require 'uri'
5
+ require_relative 'verb'
6
+
7
+ module Mayak
8
+ module Http
9
+ class Request < T::Struct
10
+ extend T::Sig
11
+
12
+ const :verb, Mayak::Http::Verb
13
+ const :url, URI
14
+ const :headers, T::Hash[String, String], default: {}
15
+ const :body, T.nilable(String)
16
+
17
+ CONTENT_TYPE_HEADER = T.let("Content-Type", String)
18
+
19
+ sig(:final) { params(other: Mayak::Http::Request).returns(T::Boolean) }
20
+ def ==(other)
21
+ verb == other.verb && url.to_s == other.url.to_s && headers && other.headers && body == other.body
22
+ end
23
+
24
+ sig(:final) { params(other: Mayak::Http::Request).returns(T::Boolean) }
25
+ def eql?(other)
26
+ self == other
27
+ end
28
+
29
+ sig { returns(Integer) }
30
+ def hash
31
+ [verb, url.to_s, headers, body].hash
32
+ end
33
+
34
+ sig { returns(T.nilable(String)) }
35
+ def content_type
36
+ headers[CONTENT_TYPE_HEADER]
37
+ end
38
+
39
+ sig { params(type: String).returns(Mayak::Http::Request) }
40
+ def with_content_type(type)
41
+ Mayak::Http::Request.new(
42
+ verb: verb,
43
+ url: url,
44
+ headers: headers.merge(CONTENT_TYPE_HEADER => type),
45
+ body: body
46
+ )
47
+ end
48
+
49
+ sig { params(url: URI, headers: T::Hash[String, String], body: T.nilable(String)).returns(Mayak::Http::Request) }
50
+ def self.get(url:, headers: {}, body: nil)
51
+ Mayak::Http::Request.new(
52
+ verb: Mayak::Http::Verb::Get,
53
+ url: url,
54
+ headers: headers,
55
+ body: body
56
+ )
57
+ end
58
+
59
+ sig { params(url: URI, headers: T::Hash[String, String], body: T.nilable(String)).returns(Mayak::Http::Request) }
60
+ def self.head(url:, headers: {}, body: nil)
61
+ Mayak::Http::Request.new(
62
+ verb: Mayak::Http::Verb::Head,
63
+ url: url,
64
+ headers: headers,
65
+ body: body
66
+ )
67
+ end
68
+
69
+ sig { params(url: URI, headers: T::Hash[String, String], body: T.nilable(String)).returns(Mayak::Http::Request) }
70
+ def self.post(url:, headers: {}, body: nil)
71
+ Mayak::Http::Request.new(
72
+ verb: Mayak::Http::Verb::Post,
73
+ url: url,
74
+ headers: headers,
75
+ body: body
76
+ )
77
+ end
78
+
79
+ sig { params(url: URI, headers: T::Hash[String, String], body: T.nilable(String)).returns(Mayak::Http::Request) }
80
+ def self.put(url:, headers: {}, body: nil)
81
+ Mayak::Http::Request.new(
82
+ verb: Mayak::Http::Verb::Put,
83
+ url: url,
84
+ headers: headers,
85
+ body: body
86
+ )
87
+ end
88
+
89
+ sig { params(url: URI, headers: T::Hash[String, String], body: T.nilable(String)).returns(Mayak::Http::Request) }
90
+ def self.patch(url:, headers: {}, body: nil)
91
+ Mayak::Http::Request.new(
92
+ verb: Mayak::Http::Verb::Patch,
93
+ url: url,
94
+ headers: headers,
95
+ body: body
96
+ )
97
+ end
98
+
99
+ sig { params(url: URI, headers: T::Hash[String, String], body: T.nilable(String)).returns(Mayak::Http::Request) }
100
+ def self.delete(url:, headers: {}, body: nil)
101
+ Mayak::Http::Request.new(
102
+ verb: Mayak::Http::Verb::Delete,
103
+ url: url,
104
+ headers: headers,
105
+ body: body
106
+ )
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require "set"
5
+
6
+ module Mayak
7
+ module Http
8
+ class Response < T::Struct
9
+ extend T::Sig
10
+
11
+ SuccessfulStatuses = T.let(::Set.new(200..299).freeze, T::Set[Integer])
12
+ StatusesWithoutBody = T.let(::Set.new([204, 304]).freeze, T::Set[Integer])
13
+
14
+ const :status, Integer
15
+ const :headers, T::Hash[String, String], default: {}
16
+ const :body, String, default: ""
17
+
18
+ sig { returns(T::Boolean) }
19
+ def success?
20
+ SuccessfulStatuses.include?(status)
21
+ end
22
+
23
+ sig { returns(T::Boolean) }
24
+ def failure?
25
+ !success?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module Mayak
5
+ module Http
6
+ class Verb < T::Enum
7
+ enums do
8
+ Get = new("GET")
9
+ Post = new("POST")
10
+ Head = new("HEAD")
11
+ Put = new("PUT")
12
+ Patch = new("PATCH")
13
+ Delete = new("DELETE")
14
+ Connect = new("CONNECT")
15
+ Options = new("OPTIONS")
16
+ Trace = new("TRACE")
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/mayak/json.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module Mayak
5
+ module Json
6
+ extend T::Sig
7
+
8
+ JsonType = T.type_alias {
9
+ T.any(
10
+ T::Array[T.untyped],
11
+ T::Hash[T.untyped, T.untyped],
12
+ String,
13
+ Integer,
14
+ Float
15
+ )
16
+ }
17
+
18
+ class ParsingError < StandardError
19
+ end
20
+
21
+ sig { params(string: String).returns(Mayak::Monads::Try[JsonType]) }
22
+ def self.parse(string)
23
+ Mayak::Monads::Try::Success.new(JSON.parse(string))
24
+ rescue JSON::ParserError => e
25
+ Mayak::Monads::Try::Failure.new(ParsingError.new(e.message))
26
+ end
27
+
28
+ sig { params(string: String).returns(JsonType) }
29
+ def self.parse_unsafe!(string)
30
+ JSON.parse(string)
31
+ end
32
+ end
33
+ end