vigiles 0.1.0.pre.beta4 → 0.1.0.pre.beta6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ebe4f039d4bb2b69d55ecc09714ad8ca498da080c1d29837b9c6ad4f4f455df
4
- data.tar.gz: ca4c6d6221b06437e3a099282a6d70ab1ddacde85446ecfd78940a3b94de4ea6
3
+ metadata.gz: 6ad47542ac3fa029cef8be2b99b104eb7995abf67765c8e0582426249f2d5a5a
4
+ data.tar.gz: a468738d5ef0ad815be016420198ef939b254997509b25228f5b3779fa51976f
5
5
  SHA512:
6
- metadata.gz: 9c3e3a3057cfef634b85c2e91b004a43b0308d3c46015231166b2badaea0ca6a460d14584d5391bc35817c94ba3674cc16ada0bb7ccefd876547450d3ae37f7b
7
- data.tar.gz: fd01e8c1ce72e058286c85d7c9eebeaeb3f14c81a25bb9c94b9b9aacc43dc84ddf166927bd0802222146cb21a24e816c3ddb82d00d79cc90bf7414b44bca1cfa
6
+ metadata.gz: df8d3f3c5361e26b23b6a8cce014c4c55f3b15cc09f754a5ed3c34f15250360e09d6a0964ed23df5b1eb81cb87bec575198f71237fd24e1ce5f3ff201b279dc6
7
+ data.tar.gz: fe0759a9e54c564ee235f421dbd8c7d8cfe34378777c1c3737fb29fa152a7f277c4a94808e138ec4fb61c0ef7a619bed8b60a9d14d3059299772a3ef4c2dbcda
@@ -0,0 +1,31 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module ActionDispatch
5
+ class Request
6
+ sig { returns(T::Hash[String, T.untyped]) }
7
+ def original_headers
8
+ # prefix content-type and content-length with `HTTP_` too,
9
+ # just for uniformity. apparently the rack specification
10
+ # requires content length and content type headers to not
11
+ # have the `HTTP_` prefix.
12
+ # see https://github.com/rack/rack/blob/main/SPEC.rdoc
13
+ #
14
+ @original_headers ||=
15
+ begin
16
+ original = {
17
+ "HTTP_CONTENT_LENGTH" => get_header("CONTENT_LENGTH"),
18
+ "HTTP_CONTENT_TYPE" => get_header("CONTENT_TYPE")
19
+ }
20
+
21
+ each_header do |header, value|
22
+ next unless header.start_with?("HTTP_")
23
+
24
+ original[header] = value
25
+ end
26
+
27
+ original.freeze
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/core_ext.rb CHANGED
@@ -2,3 +2,4 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "core_ext/object"
5
+ require_relative "core_ext/action_dispatch/request"
@@ -0,0 +1,28 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Vigiles
5
+ module Archive
6
+ class Parameter < T::Struct
7
+ class Visibility < T::Enum
8
+ enums do
9
+ PersonallyIdentifiableInformation = new("personally_identifiable_information")
10
+ AuthorizationKey = new("authorization_key")
11
+ Password = new("password")
12
+ end
13
+ end
14
+
15
+ class Source < T::Enum
16
+ enums do
17
+ Internal = new("internal")
18
+ External = new("external")
19
+ end
20
+ end
21
+
22
+ const :visibility, Visibility
23
+ const :encrypted, T::Boolean
24
+ const :source, Source
25
+ const :name, String
26
+ end
27
+ end
28
+ end
@@ -37,13 +37,18 @@ module Vigiles
37
37
 
38
38
  sig { params(request: ActionDispatch::Request).returns(Request) }
39
39
  def self.from(request)
40
+ preferred_headers = Vigiles.specification.request_headers
41
+ available_headers = request.original_headers
42
+ recorded_headers = (available_headers if preferred_headers.empty?)
43
+ recorded_headers ||= preferred_headers.to_h { |h| [h, available_headers[h]] }
44
+
40
45
  Request.new(
41
46
  content_type: request.content_type || (raise InvalidParameterError, "content_type"),
42
47
  user_agent: request.user_agent || "unknown_user_agent",
43
48
  timestamp: DateTime.now,
44
49
  remote_ip: IPAddr.new(request.remote_ip),
45
50
  protocol: request.protocol,
46
- headers: {},
51
+ headers: recorded_headers,
47
52
  origin: request.origin || "unknown_origin_url",
48
53
  payload: request.body.read,
49
54
  http_method: Types::HttpMethod.deserialize(request.method),
@@ -1,21 +1,40 @@
1
- # typed: strict
1
+ # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Vigiles
5
5
  module Archive
6
6
  class Response < T::Struct
7
+ const :rack_response, Rack::Response
7
8
  const :content_type, String
8
9
  const :headers, Types::Headers
9
10
  const :payload, Types::Payload
10
11
  const :status, Integer
11
12
 
12
- sig { params(response: ActionDispatch::Response).returns(Response) }
13
- def self.from(response)
13
+ sig { params(rack_response: Rack::Response).returns(Types::Payload) }
14
+ private_class_method def self.extract_payload(rack_response)
15
+ case (body = rack_response.body)
16
+ when Array
17
+ return { body: :empty_no_content } if body.empty?
18
+
19
+ { body: :not_empty_handle_later }
20
+ when Rack::BodyProxy
21
+ body_proxy = body
22
+ body_proxy = body_proxy.instance_variable_get(:@body) until body_proxy.is_a?(Array)
23
+ JSON.parse(body_proxy[0])
24
+ else
25
+ debugger
26
+ { body: :unknown_response_payload_type }
27
+ end
28
+ end
29
+
30
+ sig { params(res: Rack::Response).returns(Response) }
31
+ def self.from(res)
14
32
  Response.new(
15
- content_type: response.content_type,
16
- headers: response.headers.as_json,
17
- payload: response.body,
18
- status: response.status
33
+ rack_response: res,
34
+ content_type: res.headers["Content-Type"] || "unknown_content_type",
35
+ headers: res.headers.as_json,
36
+ payload: extract_payload(res),
37
+ status: res.status
19
38
  )
20
39
  end
21
40
  end
@@ -16,18 +16,17 @@ module Vigiles
16
16
  end
17
17
  end
18
18
 
19
- sig { params(ad_response: ActionDispatch::Response).returns(T.nilable(Conversation)) }
20
- def self.record_conversation(ad_response)
19
+ sig { params(req: ActionDispatch::Request, res: Rack::Response).returns(T.nilable(Conversation)) }
20
+ def self.record_conversation(req:, res:)
21
21
  # preferring to call `response.request` instead of preparing a new
22
22
  # request (via `ActionDispatch::Request.new(env)` and passing it
23
23
  # as an argument to this method because we can always recover the
24
24
  # specific request that elicited a given response, according to
25
25
  # https://github.com/rails/rails/blob/cacb8475a9d4373c0db437e7be4905685f03cefa/actionpack/lib/action_dispatch/http/response.rb#L53
26
- ad_request = ad_response.request
27
- response = Response.from(ad_response)
28
- metadata = Metadata.from(ad_request.env)
29
- request = Request.from(ad_request)
30
- extras = Extras.from(ad_request.env)
26
+ response = Response.from(res)
27
+ metadata = Metadata.from(req.env)
28
+ request = Request.from(req)
29
+ extras = Extras.from(req.env)
31
30
 
32
31
  case (content_type = request.content_type)
33
32
  when ContentType::ApplicationJson.serialize then record_json_conversation(request:, response:, metadata:, extras:)
@@ -63,7 +62,7 @@ module Vigiles
63
62
  request_id: request.id,
64
63
  response_content_type: response.content_type,
65
64
  response_headers: response.headers,
66
- response_payload: JSON.load(response.payload),
65
+ response_payload: response.payload,
67
66
  response_status: response.status
68
67
  )
69
68
  rescue => e
@@ -3,5 +3,19 @@
3
3
 
4
4
  module Vigiles
5
5
  module Constants
6
+ DEFAULT_CONTENT_TYPES = T.let(
7
+ Set.new(
8
+ %w[
9
+ application/json
10
+ ]
11
+ ), T::Set[String]
12
+ )
13
+
14
+ DEFAULT_CONTENT_TYPE_RECORDERS = T.let(
15
+ {
16
+ "application/json" => Vigiles::ConversationRecorders::ApplicationJson.instance
17
+ }.freeze,
18
+ T::Hash[String, Vigiles::ConversationRecorder]
19
+ )
6
20
  end
7
21
  end
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Vigiles
5
+ class ConversationRecorder
6
+ abstract!
7
+
8
+ sig { abstract.params(response: ActionDispatch::Response).returns(Archive::Conversation) }
9
+ def record(response); end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Vigiles
5
+ module ConversationRecorders
6
+ class ApplicationJson < ConversationRecorder
7
+ include Singleton
8
+
9
+ sig { override.params(_response: ActionDispatch::Response).returns(Archive::Conversation) }
10
+ def record(_response) = Archive::Conversation.new
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Vigiles
5
+ module ConversationRecorders
6
+ class Unknown < ConversationRecorder
7
+ sig { override.params(_response: ActionDispatch::Response).returns(Archive::Conversation) }
8
+ def record(_response) = Archive::Conversation.new
9
+ end
10
+ end
11
+ end
@@ -12,20 +12,19 @@ module Vigiles
12
12
 
13
13
  sig { params(env: T.untyped).returns(T.untyped) }
14
14
  def call(env)
15
- record_conversation do
16
- @app.call(env)
15
+ req = ActionDispatch::Request.new(env)
16
+ record_conversation(req) do
17
+ @app.call(req.env)
17
18
  end
18
19
  end
19
20
 
20
- sig { params(blk: T.proc.returns(T.untyped)).returns(T.untyped) }
21
- private def record_conversation(&blk)
22
- rack_response = blk.call
23
- _, _, body = rack_response
24
- response = body.instance_variable_get(:@response)
25
- Vigiles.maybe_record_conversation(response) unless response.nil?
26
- rack_response
21
+ sig { params(req: ActionDispatch::Request, blk: T.proc.returns(T.untyped)).returns(T.untyped) }
22
+ private def record_conversation(req, &blk)
23
+ res = Rack::Response[*blk.call]
24
+ Vigiles.maybe_record_conversation(req:, res:)
25
+ res.to_a
27
26
  ensure
28
- rack_response
27
+ res.to_a
29
28
  end
30
29
  end
31
30
  end
data/lib/vigiles/spec.rb CHANGED
@@ -3,7 +3,17 @@
3
3
 
4
4
  module Vigiles
5
5
  class Spec < T::Struct
6
+ const :content_type_recorders, T::Hash[String, ConversationRecorder]
7
+ const :request_content_types, T::Set[String]
8
+ const :request_headers, T::Set[String]
9
+
6
10
  sig { returns(Spec) }
7
- def self.make_default_spec = Spec.new
11
+ def self.make_default_spec
12
+ Spec.new(
13
+ content_type_recorders: Constants::DEFAULT_CONTENT_TYPE_RECORDERS,
14
+ request_content_types: Constants::DEFAULT_CONTENT_TYPES,
15
+ request_headers: Set[]
16
+ )
17
+ end
8
18
  end
9
19
  end
data/lib/vigiles/types.rb CHANGED
@@ -28,5 +28,9 @@ module Vigiles
28
28
  JsonPayload = T.type_alias { T::Hash[T.untyped, T.untyped] }
29
29
  HtmlPayload = String
30
30
  Payload = T.type_alias { T.any(JsonPayload, HtmlPayload) }
31
+
32
+ ContentTypeRecorder = T.type_alias do
33
+ T::Hash[String, T.proc.params(arg0: ActionDispatch::Response).returns(Vigiles::Archive::Conversation)]
34
+ end
31
35
  end
32
36
  end
@@ -1,6 +1,6 @@
1
- # typed: false
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Vigiles
5
- VERSION = "0.1.0-beta4"
5
+ VERSION = "0.1.0-beta6"
6
6
  end
data/lib/vigiles.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "core_ext"
7
7
  require "action_dispatch"
8
8
 
9
9
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
10
+ loader.inflector.inflect("uri" => "URI")
10
11
  loader.ignore("#{__dir__}/generators")
11
12
  loader.ignore("#{__dir__}/core_ext.rb")
12
13
  loader.ignore("#{__dir__}/core_ext")
@@ -15,19 +16,35 @@ loader.setup
15
16
  module Vigiles
16
17
  extend T::Sig
17
18
 
18
- sig { params(response: ActionDispatch::Response).returns(T.nilable(Archive::Conversation)) }
19
- def self.maybe_record_conversation(response)
20
- return unless should_record?(response)
19
+ sig { returns(Vigiles::Spec) }
20
+ def self.specification
21
+ @specification ||= T.let(
22
+ Vigiles::Spec.make_default_spec,
23
+ T.nilable(Vigiles::Spec)
24
+ )
25
+ end
26
+
27
+ sig { params(spec: Vigiles::Spec).returns(Vigiles::Spec) }
28
+ def self.specification=(spec)
29
+ @specification = spec
30
+ end
21
31
 
22
- Archive.record_conversation(response)
32
+ sig { params(req: ActionDispatch::Request, res: Rack::Response).returns(T.nilable(Archive::Conversation)) }
33
+ def self.maybe_record_conversation(req:, res:)
34
+ return unless should_record?(req)
35
+
36
+ Archive.record_conversation(req:, res:)
23
37
  rescue Archive::UnrecordableRequestError
24
38
  nil
25
39
  end
26
40
 
27
41
  sig { params(blk: T.untyped).void }
28
42
  def self.configure(&blk)
29
- default_spec = Vigiles::Spec.make_default_spec
30
- blk.call(default_spec)
43
+ blk.call(specification)
44
+
45
+ # TODO(yaw, 2024-06-15): ensure that the specification is valid.
46
+ # ensure that for every content type a recorder is configured. otherwise
47
+ # assign the general recorder for unknown content types.
31
48
  end
32
49
 
33
50
  sig { params(request: ActionDispatch::Request).returns(T::Boolean) }
@@ -35,10 +52,8 @@ module Vigiles
35
52
  !request.nil?
36
53
  end
37
54
 
38
- sig { params(response: ActionDispatch::Response).returns(T::Boolean) }
39
- private_class_method def self.should_record?(response)
40
- request = response.request
41
-
42
- content_type_match?(request)
55
+ sig { params(req: ActionDispatch::Request).returns(T::Boolean) }
56
+ private_class_method def self.should_record?(req)
57
+ content_type_match?(req)
43
58
  end
44
59
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vigiles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.beta4
4
+ version: 0.1.0.pre.beta6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaw Boakye
@@ -205,6 +205,7 @@ files:
205
205
  - README.md
206
206
  - Rakefile
207
207
  - lib/core_ext.rb
208
+ - lib/core_ext/action_dispatch/request.rb
208
209
  - lib/core_ext/object.rb
209
210
  - lib/generators/templates/archive_conversation_migration.rb.erb
210
211
  - lib/generators/templates/initializer.rb
@@ -216,9 +217,13 @@ files:
216
217
  - lib/vigiles/archive/conversation.rb
217
218
  - lib/vigiles/archive/extras.rb
218
219
  - lib/vigiles/archive/metadata.rb
220
+ - lib/vigiles/archive/parameter.rb
219
221
  - lib/vigiles/archive/request.rb
220
222
  - lib/vigiles/archive/response.rb
221
223
  - lib/vigiles/constants.rb
224
+ - lib/vigiles/conversation_recorder.rb
225
+ - lib/vigiles/conversation_recorders/application_json.rb
226
+ - lib/vigiles/conversation_recorders/unknown.rb
222
227
  - lib/vigiles/middleware/record_conversation.rb
223
228
  - lib/vigiles/spec.rb
224
229
  - lib/vigiles/types.rb