mayak 0.0.2

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.
@@ -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