simplyq 0.8.0rc

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []