simplyq 0.8.0rc

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,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Simplyq
4
+ module Model
5
+ class Event
6
+ attr_accessor :uid
7
+
8
+ attr_accessor :event_type
9
+
10
+ attr_accessor :topics
11
+
12
+ attr_accessor :payload
13
+
14
+ attr_accessor :retention_period
15
+
16
+ attr_accessor :created_at
17
+
18
+ def initialize(attributes = {})
19
+ self.uid = attributes[:uid] if attributes.key?(:uid)
20
+
21
+ self.event_type = attributes[:event_type] if attributes.key?(:event_type)
22
+
23
+ self.topics = attributes[:topics] if attributes.key?(:topics)
24
+
25
+ self.payload = attributes[:payload] if attributes.key?(:payload)
26
+
27
+ self.retention_period = attributes[:retention_period] if attributes.key?(:retention_period)
28
+
29
+ self.created_at = attributes[:created_at] if attributes.key?(:created_at)
30
+ end
31
+
32
+ # The model identifier attribute used in list operations
33
+ #
34
+ # @return [Symbol]
35
+ def self.identifier
36
+ :uid
37
+ end
38
+
39
+ # Serializes the object from a hash
40
+ #
41
+ # @param hash [Hash] Hash with the object data
42
+ # @return [Simplyq::Model::Event]
43
+ def self.from_hash(hash)
44
+ return if hash.nil?
45
+
46
+ new(hash)
47
+ end
48
+
49
+ # Show invalid properties with the reasons. Usually used together with valid?
50
+ # @return Array for valid properties with the reasons
51
+ def validation_errors
52
+ invalid_properties = []
53
+ if !@uid.nil? && @uid.to_s.length > 255
54
+ invalid_properties.push('invalid value for "uid", the character length must be smaller than or equal to 255.')
55
+ end
56
+
57
+ if !@uid.nil? && @uid.to_s.empty?
58
+ invalid_properties.push('invalid value for "uid", the character length must be great than or equal to 1.')
59
+ end
60
+
61
+ pattern = Regexp.new(/^[a-zA-Z0-9\-_.]+$/)
62
+ if !@uid.nil? && @uid !~ pattern
63
+ invalid_properties.push("invalid value for \"uid\", must conform to the pattern #{pattern}.")
64
+ end
65
+
66
+ if !@event_type.nil? && @event_type.to_s.length > 255
67
+ invalid_properties.push('invalid value for "event_type", the character length must be smaller than or equal to 255.')
68
+ end
69
+
70
+ if !@event_type.nil? && @event_type.to_s.empty?
71
+ invalid_properties.push('invalid value for "event_type", the character length must be great than or equal to 1.')
72
+ end
73
+
74
+ pattern = Regexp.new(/^[a-zA-Z0-9\-_.]+$/)
75
+ if !@event_type.nil? && @event_type !~ pattern
76
+ invalid_properties.push("invalid value for \"event_type\", must conform to the pattern #{pattern}.")
77
+ end
78
+
79
+ if !@retention_period.nil? && @retention_period > 90
80
+ invalid_properties.push('invalid value for "retention_period", must be smaller than or equal to 90.')
81
+ end
82
+
83
+ invalid_properties
84
+ end
85
+
86
+ # Check if the model is valid
87
+ # @return true if valid, false otherwise
88
+ def valid?
89
+ validation_errors.empty?
90
+ end
91
+
92
+ def [](key)
93
+ instance_variable_get(:"@#{key}")
94
+ end
95
+
96
+ def ==(other)
97
+ return false unless other.is_a?(Event)
98
+
99
+ uid == other.uid &&
100
+ event_type == other.event_type &&
101
+ topics == other.topics &&
102
+ payload == other.payload &&
103
+ retention_period == other.retention_period &&
104
+ created_at == other.created_at
105
+ end
106
+
107
+ def to_h
108
+ {
109
+ uid: uid,
110
+ event_type: event_type,
111
+ topics: topics,
112
+ payload: _safe_parse_payload_to_hash(payload),
113
+ retention_period: retention_period,
114
+ created_at: created_at
115
+ }
116
+ end
117
+
118
+ def _safe_parse_payload_to_hash(value)
119
+ return if value.nil?
120
+ return value if value.is_a?(Hash)
121
+ return value.to_h if value.respond_to?(:to_h)
122
+
123
+ JSON.parse(value)
124
+ rescue JSON::ParserError
125
+ value
126
+ end
127
+
128
+ def to_json(*args)
129
+ to_h.to_json(*args)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Simplyq
4
+ module Model
5
+ class InboundEvent
6
+ attr_accessor :data
7
+
8
+ def initialize(data)
9
+ self.data = data
10
+ end
11
+
12
+ def self.from_hash(hash)
13
+ return if hash.nil?
14
+
15
+ new(hash)
16
+ end
17
+
18
+ def ==(other)
19
+ return false unless other.is_a?(InboundEvent)
20
+
21
+ data == other.data
22
+ end
23
+
24
+ def to_h
25
+ data
26
+ end
27
+
28
+ def [](key)
29
+ data[key]
30
+ end
31
+
32
+ def to_json(*args)
33
+ to_h.to_json(*args)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Simplyq
6
+ module Model
7
+ class List
8
+ extend Forwardable
9
+ include Enumerable
10
+
11
+ attr_accessor :data
12
+ attr_accessor :has_more
13
+
14
+ attr_accessor :data_type
15
+ attr_accessor :api_method
16
+ attr_accessor :list_args
17
+ attr_accessor :filters
18
+ attr_accessor :api
19
+
20
+ def initialize(data_type, attributes = {}, api_method:, api:, filters: {}, list_args: [])
21
+ self.data = if attributes.key?(:data)
22
+ attributes[:data].map do |item|
23
+ if data_type == Hash
24
+ item
25
+ else
26
+ data_type.from_hash(item)
27
+ end
28
+ end
29
+ else
30
+ []
31
+ end
32
+
33
+ self.has_more = attributes[:has_more]
34
+
35
+ self.data_type = data_type
36
+
37
+ self.api_method = api_method
38
+
39
+ self.filters = filters
40
+
41
+ self.list_args = list_args
42
+
43
+ self.api = api
44
+ end
45
+
46
+ def_delegators :data, :size, :length, :count, :empty?,
47
+ :first, :last, :each_with_index, :each_with_object,
48
+ :reduce, :inject, :find, :find_index, :index, :rindex
49
+
50
+ def [](key)
51
+ if key.is_a?(Integer)
52
+ data[key]
53
+ else
54
+ instance_variable_get(:"@#{key}")
55
+ end
56
+ end
57
+
58
+ # Iterates through each resource in the page represented by the current
59
+ # `List`.
60
+ #
61
+ # Note that this method will not attempt to fetch the next page when it
62
+ # reaches the end of the current page.
63
+ def each(&blk)
64
+ data.each(&blk)
65
+ end
66
+
67
+ # Iterates through each resource in the page represented by the current
68
+ # `List`.
69
+ #
70
+ # Note that this method will not attempt to fetch the next page when it
71
+ # reaches the end of the current page.
72
+ def map(&blk)
73
+ data.map(&blk)
74
+ end
75
+
76
+ def next_page
77
+ return nil unless has_more || !empty?
78
+
79
+ query_params = filters.dup.tap { |h| h.delete(:ending_before) }
80
+ query_params[:start_after] = last.send(data_type.identifier)
81
+ api.send(api_method, *list_args, query_params)
82
+ end
83
+
84
+ def prev_page
85
+ return nil if empty?
86
+
87
+ query_params = filters.dup.tap { |h| h.delete(:start_after) }
88
+ query_params[:ending_before] = first.send(data_type.identifier)
89
+ api.send(api_method, *list_args, query_params)
90
+ end
91
+
92
+ def to_h
93
+ {
94
+ data: data.map(&:to_h),
95
+ has_more: has_more
96
+ }
97
+ end
98
+
99
+ def to_json(*args)
100
+ to_h.to_json(*args)
101
+ end
102
+
103
+ # Serializes the object from a hash
104
+ #
105
+ # @param hash [Hash] Hash with the object data
106
+ # @return [Simplyq::Model::List]
107
+ def self.from_hash(hash, data_type, api:, filters: {})
108
+ return if hash.nil?
109
+
110
+ new(data_type, hash, api: api, filters: filters)
111
+ end
112
+
113
+ def ==(other)
114
+ return false unless other.is_a?(List)
115
+
116
+ data == other.data && has_more == other.has_more
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Simplyq
4
+ VERSION = "0.8.0rc"
5
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Simplyq
7
+ module Webhook
8
+ DEFAULT_TOLERANCE = 300
9
+ TIMESTAMP_HEADER = "x-simplyq-timestamp"
10
+ SIGNATURE_HEADER = "x-simplyq-signature"
11
+
12
+ # Verify signature of webhook request
13
+ #
14
+ # @param payload_body [String] raw payload body
15
+ # @param signatures [String] request signature header
16
+ # @param timestamp [String] request timestamp header
17
+ # @param secret [String] your endpoint secret
18
+ # @param tolerance [Integer] duration before signature expires
19
+ # (default: 300 seconds) replay attack protection
20
+ # @raise [SignatureVerificationError] if signature verification fails
21
+ #
22
+ # @return [Boolean] true if signature verification succeeds
23
+ def self.verify_signature(payload_body, signatures:, timestamp:, secret:, tolerance: DEFAULT_TOLERANCE)
24
+ Signature.verify_header(payload_body, signatures, timestamp, secret, tolerance: tolerance)
25
+ end
26
+
27
+ # Decerialize and verify signature of payload into an InboundEvent
28
+ #
29
+ # @param payload_body [String] raw payload body
30
+ # @param signatures [String] request signature header
31
+ # @param timestamp [String] request timestamp header
32
+ # @param secret [String] your endpoint secret
33
+ # @param tolerance [Integer] duration before signature expires
34
+ # (default: 300 seconds) replay attack protection
35
+ # @raise [SignatureVerificationError] if signature verification fails
36
+ # @raise [JSON::ParserError] if payload_body is not valid JSON
37
+ #
38
+ # @return [Model::InboundEvent]
39
+ def self.construct_event(payload_body, signatures:, timestamp:, secret:, tolerance: DEFAULT_TOLERANCE)
40
+ Signature.verify_header(payload_body, signatures, timestamp, secret, tolerance: tolerance)
41
+
42
+ Model::InboundEvent.from_hash(JSON.parse(payload_body, symbolize_names: true))
43
+ end
44
+
45
+ module Signature
46
+ def self.calculate_signature(timestamp, payload, secret)
47
+ raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
48
+ raise ArgumentError, "payload should be a string" unless payload.is_a?(String)
49
+ raise ArgumentError, "secret should be a string" unless secret.is_a?(String)
50
+
51
+ sig = OpenSSL::HMAC.digest("SHA256", secret, "#{timestamp.to_i}#{payload}")
52
+ Base64.strict_encode64(sig)
53
+ end
54
+
55
+ def self.verify_header(payload, signatures_header, timestamp_header, secret, tolerance: nil)
56
+ timestamp, signatures = parse_timestampt_and_sigs(signatures_header, timestamp_header)
57
+
58
+ expected_sig = calculate_signature(timestamp, payload, secret)
59
+ unless signatures.any? { |sig| secure_compare(sig, expected_sig) }
60
+ header = { TIMESTAMP_HEADER => timestamp_header, SIGNATURE_HEADER => signatures_header }
61
+ raise Simplyq::SignatureVerificationError.new(
62
+ "No signatures found matching the expected signature for payload",
63
+ http_headers: header, http_body: payload
64
+ )
65
+ end
66
+
67
+ if tolerance && timestamp < Time.now - tolerance
68
+ header = { TIMESTAMP_HEADER => timestamp_header, SIGNATURE_HEADER => signatures_header }
69
+ raise SignatureVerificationError.new(
70
+ "Timestamp outside the tolerance zone (#{timestamp.to_i})",
71
+ http_headers: header, http_body: payload
72
+ )
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ def self.parse_timestampt_and_sigs(signatures_header, timestamp_header)
79
+ timestamp = Integer(timestamp_header)
80
+
81
+ signatures = signatures_header.split(",").map(&:strip)
82
+ raise Simplyq::SignatureVerificationError.new("No signatures found", http_headers: header) if signatures.empty?
83
+
84
+ [Time.at(timestamp), signatures]
85
+ end
86
+ private_class_method :parse_timestampt_and_sigs
87
+
88
+ # Constant time string comparison to prevent timing attacks
89
+ # Code borrowed from ActiveSupport
90
+ def self.secure_compare(str_a, str_b)
91
+ return false unless str_a.bytesize == str_b.bytesize
92
+
93
+ l = str_a.unpack "C#{str_a.bytesize}"
94
+
95
+ res = 0
96
+ str_b.each_byte { |byte| res |= byte ^ l.shift }
97
+ res.zero?
98
+ end
99
+ private_class_method :secure_compare
100
+ end
101
+ end
102
+ end
data/lib/simplyq.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Version
4
+ require_relative "simplyq/version"
5
+
6
+ # API resources support classes
7
+ require "simplyq/errors"
8
+ require "simplyq/client"
9
+ require "simplyq/configuration"
10
+ require "simplyq/webhook"
11
+
12
+ # API models
13
+ require "simplyq/models/list"
14
+ require "simplyq/models/application"
15
+ require "simplyq/models/endpoint"
16
+ require "simplyq/models/event"
17
+ require "simplyq/models/delivery_attempt"
18
+ require "simplyq/models/inbound_event"
19
+
20
+ # APIs
21
+ require "simplyq/api/application_api"
22
+ require "simplyq/api/endpoint_api"
23
+ require "simplyq/api/event_api"
24
+
25
+ module Simplyq
26
+ end
data/sig/simplyq.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Simplyq
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/simplyq.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/simplyq/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "simplyq"
7
+ spec.version = Simplyq::VERSION
8
+ spec.authors = ["simplyq-dxtimer"]
9
+ spec.email = ["ivan@simplyq.io"]
10
+
11
+ spec.summary = "The SimplyQ API client for Ruby"
12
+ spec.description = "The SimplyQ API client for Ruby"
13
+ spec.homepage = "https://github.com/simplyqio/apis"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.7"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/simplyqio/apis/tree/main/ruby"
19
+ spec.metadata["changelog_uri"] = "https://github.com/simplyqio/apis/tree/main/ruby/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ spec.add_dependency "faraday", [">= 0.15", "< 2.0"]
34
+ spec.add_dependency "multi_json", "~> 1.0"
35
+ spec.add_dependency "net-http-persistent"
36
+ # For more information and examples about making a new gem, check out our
37
+ # guide at: https://bundler.io/guides/creating_gem.html
38
+ # We are looking to automate the release process so MFA is not supported yet
39
+ # spec.metadata["rubygems_mfa_required"] = "true"
40
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simplyq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0rc
5
+ platform: ruby
6
+ authors:
7
+ - simplyq-dxtimer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-11-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.15'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0.15'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: multi_json
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: net-http-persistent
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ description: The SimplyQ API client for Ruby
62
+ email:
63
+ - ivan@simplyq.io
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ".rspec"
69
+ - ".rubocop.yml"
70
+ - ".rubocop_todo.yml"
71
+ - ".tool-versions"
72
+ - CHANGELOG.md
73
+ - CODE_OF_CONDUCT.md
74
+ - Gemfile
75
+ - LICENSE.txt
76
+ - README.md
77
+ - Rakefile
78
+ - lib/simplyq.rb
79
+ - lib/simplyq/api/application_api.rb
80
+ - lib/simplyq/api/endpoint_api.rb
81
+ - lib/simplyq/api/event_api.rb
82
+ - lib/simplyq/client.rb
83
+ - lib/simplyq/configuration.rb
84
+ - lib/simplyq/errors.rb
85
+ - lib/simplyq/models/application.rb
86
+ - lib/simplyq/models/delivery_attempt.rb
87
+ - lib/simplyq/models/endpoint.rb
88
+ - lib/simplyq/models/event.rb
89
+ - lib/simplyq/models/inbound_event.rb
90
+ - lib/simplyq/models/list.rb
91
+ - lib/simplyq/version.rb
92
+ - lib/simplyq/webhook.rb
93
+ - sig/simplyq.rbs
94
+ - simplyq.gemspec
95
+ homepage: https://github.com/simplyqio/apis
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ homepage_uri: https://github.com/simplyqio/apis
100
+ source_code_uri: https://github.com/simplyqio/apis/tree/main/ruby
101
+ changelog_uri: https://github.com/simplyqio/apis/tree/main/ruby/CHANGELOG.md
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">"
114
+ - !ruby/object:Gem::Version
115
+ version: 1.3.1
116
+ requirements: []
117
+ rubygems_version: 3.3.11
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: The SimplyQ API client for Ruby
121
+ test_files: []