userlist 0.2.1 → 0.5.0

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: 366ff35750f8111ac69fba96a4f08f63b8f70a6328db3d36162f3d7b92d7250f
4
- data.tar.gz: '05969b9adf2194fdaa571ca9f110229ffe9f414bb709c792a199dae7da497e6d'
3
+ metadata.gz: a76254952c47cc9014e31019550491b96b1a8f46814cb6e44750ddf4c4d966f1
4
+ data.tar.gz: 17eabd156ef0d1eda09a7480c9eb3f5822098f045123d2a2d9f33351630493d6
5
5
  SHA512:
6
- metadata.gz: 6796685aeb36076ae0bdd821d6d185b9e83dd4118612ede2eae793dca67b40ae3f4eb0079f886e62423f464bcc327a32fc4f0ea7b56743adfc7f3537065bbd32
7
- data.tar.gz: b34097b284e2db292f8ad84fc36d69eab34abcafc59a452305e72fcfb4722cba58f4981e9fe2d93a6bee5e21577dbca258a9597dcb1726a38f84d19304ded3bc
6
+ metadata.gz: 7d3311432655c8b7758d59733845558f526208874f31fee9034dbbb8a75bca653dc39b8b18007928b7fae7d2cf3efae449126c2c355bd6f3b3e5d0e82f2a24a7
7
+ data.tar.gz: 8531012b1ec1824c2ff60b537c641b17b534e60ea4665e476b66055f1f623ed51787c4a336402fb1adb63b1645e238008bb94ec64a2c7caf87cb21956d35111c
@@ -0,0 +1,21 @@
1
+ name: Tests
2
+ on: [push]
3
+ jobs:
4
+ build:
5
+ strategy:
6
+ matrix:
7
+ ruby:
8
+ - 2.4
9
+ - 2.5
10
+ - 2.6
11
+ - 2.7
12
+ - 3.0
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ bundler-cache: true
20
+ - name: RSpec
21
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+ /.env
@@ -1,10 +1,11 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.2
2
+ TargetRubyVersion: 2.4
3
+ NewCops: enable
3
4
  Exclude:
4
5
  - 'bin/*'
5
6
  - 'Guardfile'
6
7
 
7
- Metrics/LineLength:
8
+ Layout/LineLength:
8
9
  Enabled: false
9
10
 
10
11
  Metrics/ModuleLength:
@@ -32,13 +33,13 @@ Layout/ElseAlignment:
32
33
  Lint/AssignmentInCondition:
33
34
  Enabled: false
34
35
 
35
- Lint/EndAlignment:
36
+ Layout/EndAlignment:
36
37
  EnforcedStyleAlignWith: start_of_line
37
38
 
38
39
  Layout/AccessModifierIndentation:
39
40
  EnforcedStyle: outdent
40
41
 
41
- Layout/AlignParameters:
42
+ Layout/ParameterAlignment:
42
43
  EnforcedStyle: with_fixed_indentation
43
44
 
44
45
  Style/FrozenStringLiteralComment:
data/Gemfile CHANGED
@@ -6,3 +6,6 @@ gemspec
6
6
 
7
7
  gem 'guard-rspec', '~> 4.7'
8
8
  gem 'guard-rubocop', '~> 1.3'
9
+ gem 'rubocop', '~> 0.68'
10
+
11
+ gem 'sidekiq'
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Userlist
1
+ # Userlist for Ruby [![Build Status](https://github.com/userlist/userlist-ruby/workflows/Tests/badge.svg)](https://github.com/userlist/userlist-ruby)
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/userlist`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ This gem helps with integrating [Userlist](https://userlist.com) into Ruby applications.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ > To integrate Userlist into a Ruby on Rails application, please use [userlist-rails](https://github.com/userlist/userlist-rails)
6
6
 
7
7
  ## Installation
8
8
 
@@ -20,9 +20,180 @@ Or install it yourself as:
20
20
 
21
21
  $ gem install userlist
22
22
 
23
+ ## Configuration
24
+
25
+ The only required configuration is the Push API key. You can get your Push API key via the [Push API settings](https://app.userlist.com/settings/push) in your Userlist account.
26
+
27
+ Configuration values can either be set via the `Userlist.configure` method or as environment variables. The environment variables take precedence over configuration values from the initializer.
28
+
29
+ Configuration via environment variables:
30
+
31
+ ```shell
32
+ USERLIST_PUSH_KEY=VvB7pjDrv0V2hoaOCeZ5rIiUEPbEhSUN
33
+ USERLIST_PUSH_ID=6vPkJl44cm82y4aLBIzaOhuEHJd0Bm7b
34
+ ```
35
+
36
+ Configuration via an initializer:
37
+
38
+ ```ruby
39
+ Userlist.configure do |config|
40
+ config.push_key = 'VvB7pjDrv0V2hoaOCeZ5rIiUEPbEhSUN'
41
+ config.push_id = '6vPkJl44cm82y4aLBIzaOhuEHJd0Bm7b'
42
+ end
43
+ ```
44
+
45
+ The possible configuration values are listed in the table below.
46
+
47
+ | Name | Default value | Description |
48
+ | ----------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
49
+ | `push_key` | `nil` | The push key for your account. See [Push API settings](https://app.userlist.com/settings/push). |
50
+ | `push_id` | `nil` | The push id for your account. See [Push API settings](https://app.userlist.com/settings/push). |
51
+ | `push_endpoint` | `https://push.userlist.com/` | The HTTP endpoint that the library will send data to. |
52
+ | `push_strategy` | `:threaded` | The strategy to use to send data to the HTTP endpoint. Possible values are `:threaded`, `:sidekiq`, `:direct`, and `:null`. |
53
+ | `push_strategy_options` | `{}` | Any additional options for the push strategy. |
54
+ | `log_level` | `:warn` | The log level for Userlist related log messages. Possible values are `:debug`, `:error`, `:fatal`, `:info`, and `:warn` |
55
+ | `token_lifetime` | `3600` | The lifetime of generated in-app messages tokens in seconds |
56
+
57
+ ### Disabling in development and test environments
58
+
59
+ As sending test and development data into data into Userlist isn't very desirable, you can disable transmissions by setting the push strategy to `:null`.
60
+
61
+ ```ruby
62
+ Userlist.configure do |config|
63
+ config.push_strategy = :null
64
+ end
65
+ ```
66
+
23
67
  ## Usage
24
68
 
25
- TODO: Write usage instructions here
69
+ This library is a wrapper for Userlist's Push API. For details about the accepted payloads, please check [its documentation](https://userlist.com/docs/getting-started/integration-guide/).
70
+
71
+ ### Tracking Users
72
+
73
+ To manually send user data into Userlist, use the `Userlist::Push.users.push` method.
74
+
75
+ ```ruby
76
+ Userlist::Push.users.push(
77
+ identifier: 'user-1',
78
+ email: 'foo@example.com',
79
+ properties: {
80
+ first_name: 'Foo',
81
+ last_name: 'Example'
82
+ }
83
+ )
84
+ ```
85
+
86
+ It's also possible to delete a user from Userlist, using the `Userlist::Push.users.delete` method.
87
+
88
+ ```ruby
89
+ Userlist::Push.users.delete('user-1')
90
+ ```
91
+
92
+ ### Tracking Companies
93
+
94
+ To manually send company data into Userlist, use the `Userlist::Push.companies.push` method.
95
+
96
+ ```ruby
97
+ Userlist::Push.companies.push(
98
+ identifier: 'company-1',
99
+ email: 'Example, Inc.',
100
+ properties: {
101
+ industry: 'Software Testing'
102
+ }
103
+ )
104
+ ```
105
+
106
+ It's also possible to delete a user from Userlist, using the `Userlist::Push.companies.delete` method.
107
+
108
+ ```ruby
109
+ Userlist::Push.companies.delete('user-1')
110
+ ```
111
+
112
+ ### Tracking Relationships
113
+
114
+ Tracking relationships can either be done using nested properties in user or company payloads or via the `Userlist::Push.relationships.push` method.
115
+
116
+ ```ruby
117
+ Userlist::Push.relationships.push(
118
+ user: 'user-1',
119
+ company: 'company-1',
120
+ properties: {
121
+ role: 'owner'
122
+ }
123
+ )
124
+ ```
125
+
126
+ This is equivalent to specifying the relationship on the user model.
127
+
128
+ ```ruby
129
+ Userlist::Push.users.push(
130
+ identifier: 'user-1',
131
+ relationships: [
132
+ {
133
+ company: 'company-1',
134
+ properties: {
135
+ role: 'owner'
136
+ }
137
+ }
138
+ ]
139
+ )
140
+ ```
141
+
142
+ It's also equivalent specifying the relationship on the company model.
143
+
144
+ ```ruby
145
+ Userlist::Push.companies.push(
146
+ identifier: 'company-1',
147
+ relationships: [
148
+ {
149
+ user: 'user-1',
150
+ properties: {
151
+ role: 'owner'
152
+ }
153
+ }
154
+ ]
155
+ )
156
+ ```
157
+
158
+ ### Tracking Events
159
+
160
+ To track custom events use the `Userlist::Push.events.push` method.
161
+
162
+ ```ruby
163
+ Userlist::Push.events.push(
164
+ name: 'project_created',
165
+ user: 'user-1',
166
+ properties: {
167
+ project_name: 'Example project'
168
+ }
169
+ )
170
+ ```
171
+
172
+ Instead of just sending a user or company identifier, it's also possible to expand the properties into objects. This will update the user / company record as well as trigger the event in one operation.
173
+
174
+ ```ruby
175
+ Userlist::Push.events.push(
176
+ name: 'project_created',
177
+ user: {
178
+ identifier: 'user-1',
179
+ properties: {
180
+ projects: 5
181
+ }
182
+ },
183
+ properties: {
184
+ project_name: 'Example project'
185
+ }
186
+ )
187
+ ```
188
+
189
+ ### Tokens for in-app messages
190
+
191
+ In order to use in-app messages, you must create a JWT token for the currently signed in user on the server side. To do this, please configure both the `push_key` and the `push_id` configuration variables. Afterwards, you can use the `Userlist::Token.generate` method to get a signed token for the given user identifier.
192
+
193
+ ```ruby
194
+ Userlist::Token.generate('user-1')
195
+ # => "eyJraWQiOiI2dlBrSmw0NGNtODJ5NGFMQkl6YU9odU...kPGe8KX8JZBTQ"
196
+ ```
26
197
 
27
198
  ## Development
28
199
 
@@ -32,7 +203,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
203
 
33
204
  ## Contributing
34
205
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/userlist. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
206
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/userlist/userlist-ruby>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
207
 
37
208
  ## License
38
209
 
@@ -40,4 +211,12 @@ The gem is available as open source under the terms of the [MIT License](http://
40
211
 
41
212
  ## Code of Conduct
42
213
 
43
- Everyone interacting in the Userlist project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/userlist/blob/master/CODE_OF_CONDUCT.md).
214
+ Everyone interacting in the Userlist project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/userlist/userlist-ruby/blob/master/CODE_OF_CONDUCT.md).
215
+
216
+ ## What is Userlist?
217
+
218
+ [![Userlist](https://userlist.com/images/external/userlist-logo-github.svg)](https://userlist.com/)
219
+
220
+ [Userlist](https://userlist.com/) allows you to onboard and engage your SaaS users with targeted behavior-based campaigns using email or in-app messages.
221
+
222
+ Userlist was started in 2017 as an alternative to bulky enterprise messaging tools. We believe that running SaaS products should be more enjoyable. Learn more [about us](https://userlist.com/about-us/).
@@ -4,8 +4,35 @@ require 'userlist/version'
4
4
  require 'userlist/config'
5
5
  require 'userlist/logging'
6
6
  require 'userlist/push'
7
+ require 'userlist/token'
7
8
 
8
9
  module Userlist
10
+ class Error < StandardError; end
11
+
12
+ class ArgumentError < Error; end
13
+
14
+ class ConfigurationError < Error
15
+ attr_reader :key
16
+
17
+ def initialize(key)
18
+ @key = key.to_sym
19
+
20
+ super <<~MESSAGE
21
+ Missing required configuration value for `#{key}`
22
+
23
+ Please set a value for `#{key}` using an environment variable:
24
+
25
+ USERLIST_#{key.to_s.upcase}=some-value-here
26
+
27
+ or via the `Userlist.configure` method:
28
+
29
+ Userlist.configure do |config|
30
+ config.#{key} = 'some-value-here'
31
+ end
32
+ MESSAGE
33
+ end
34
+ end
35
+
9
36
  class << self
10
37
  def config
11
38
  @config ||= Userlist::Config.new
@@ -13,9 +40,9 @@ module Userlist
13
40
 
14
41
  def logger
15
42
  @logger ||= begin
16
- logger = Logger.new(STDOUT)
43
+ logger = Logger.new($stdout)
17
44
  logger.progname = 'userlist'
18
- logger.level = config.log_level
45
+ logger.level = Logger.const_get(config.log_level.to_s.upcase)
19
46
  logger
20
47
  end
21
48
  end
@@ -2,9 +2,12 @@ module Userlist
2
2
  class Config
3
3
  DEFAULT_CONFIGURATION = {
4
4
  push_key: nil,
5
- push_endpoint: 'https://push.userlist.io/',
5
+ push_id: nil,
6
+ push_endpoint: 'https://push.userlist.com/',
6
7
  push_strategy: :threaded,
7
- log_level: :warn
8
+ push_strategy_options: {},
9
+ log_level: :warn,
10
+ token_lifetime: 3600
8
11
  }.freeze
9
12
 
10
13
  def initialize(config_from_initialize = {})
@@ -36,6 +39,10 @@ module Userlist
36
39
  config == other.config && parent == other.parent
37
40
  end
38
41
 
42
+ def token_lifetime
43
+ self[:token_lifetime]&.to_i
44
+ end
45
+
39
46
  protected
40
47
 
41
48
  attr_reader :config, :parent
@@ -52,11 +59,11 @@ module Userlist
52
59
  end
53
60
 
54
61
  def key?(key)
55
- config.key?(key) || parent && parent.key?(key)
62
+ config.key?(key) || parent&.key?(key)
56
63
  end
57
64
 
58
65
  def [](key)
59
- config[key] || parent && parent[key]
66
+ config.key?(key) ? config[key] : parent && parent[key]
60
67
  end
61
68
 
62
69
  def []=(key, value)
@@ -1,10 +1,24 @@
1
1
  require 'userlist/push/client'
2
2
  require 'userlist/push/strategies'
3
3
 
4
+ require 'userlist/push/resource'
5
+ require 'userlist/push/resource_collection'
6
+ require 'userlist/push/relation'
7
+
8
+ require 'userlist/push/operations/create'
9
+ require 'userlist/push/operations/delete'
10
+
11
+ require 'userlist/push/user'
12
+ require 'userlist/push/company'
13
+ require 'userlist/push/relationship'
14
+ require 'userlist/push/event'
15
+
16
+ require 'userlist/push/serializer'
17
+
4
18
  module Userlist
5
19
  class Push
6
20
  class << self
7
- [:event, :track, :user, :identify, :company].each do |method|
21
+ [:event, :track, :user, :identify, :company, :users, :events, :companies, :relationships].each do |method|
8
22
  define_method(method) { |*args| default_push_instance.send(method, *args) }
9
23
  end
10
24
 
@@ -15,53 +29,42 @@ module Userlist
15
29
  end
16
30
  end
17
31
 
18
- def initialize(config = {})
19
- @config = Userlist.config.merge(config)
20
- @mutex = Mutex.new
32
+ def initialize(configuration = {})
33
+ @config = Userlist.config.merge(configuration)
34
+ @strategy = Userlist::Push::Strategies.strategy_for(config.push_strategy, config)
21
35
  end
22
36
 
23
- def event(payload = {})
24
- with_mutex do
25
- raise ArgumentError, 'Missing required payload hash' unless payload
26
- raise ArgumentError, 'Missing required parameter :name' unless payload[:name]
27
- raise ArgumentError, 'Missing required parameter :user' unless payload[:user]
28
-
29
- payload[:occured_at] ||= Time.now
37
+ attr_reader :config, :strategy
30
38
 
31
- strategy.call(:post, '/events', payload)
32
- end
39
+ def events
40
+ @events ||= Relation.new(self, Event, [Operations::Create])
33
41
  end
34
- alias track event
35
-
36
- def user(payload = {})
37
- with_mutex do
38
- raise ArgumentError, 'Missing required payload hash' unless payload
39
- raise ArgumentError, 'Missing required parameter :identifier' unless payload[:identifier]
40
42
 
41
- strategy.call(:post, '/users', payload)
42
- end
43
+ def users
44
+ @users ||= Relation.new(self, User, [Operations::Create, Operations::Delete])
43
45
  end
44
- alias identify user
45
46
 
46
- def company(payload = {})
47
- with_mutex do
48
- raise ArgumentError, 'Missing required payload hash' unless payload
49
- raise ArgumentError, 'Missing required parameter :identifier' unless payload[:identifier]
50
-
51
- strategy.call(:post, '/companies', payload)
52
- end
47
+ def companies
48
+ @companies ||= Relation.new(self, Company, [Operations::Create, Operations::Delete])
53
49
  end
54
50
 
55
- private
51
+ def relationships
52
+ @relationships ||= Relation.new(self, Relationship, [Operations::Create, Operations::Delete])
53
+ end
56
54
 
57
- attr_reader :config
55
+ def event(payload = {})
56
+ events.create(payload)
57
+ end
58
58
 
59
- def strategy
60
- @strategy ||= Userlist::Push::Strategies.strategy_for(config.push_strategy, config)
59
+ def user(payload = {})
60
+ users.create(payload)
61
61
  end
62
62
 
63
- def with_mutex(&block)
64
- @mutex.synchronize(&block)
63
+ def company(payload = {})
64
+ companies.create(payload)
65
65
  end
66
+
67
+ alias track event
68
+ alias identify user
66
69
  end
67
70
  end
@@ -10,10 +10,25 @@ module Userlist
10
10
 
11
11
  def initialize(config = {})
12
12
  @config = Userlist.config.merge(config)
13
+
14
+ raise Userlist::ConfigurationError, :push_key unless @config.push_key
15
+ raise Userlist::ConfigurationError, :push_endpoint unless @config.push_endpoint
16
+ end
17
+
18
+ def get(endpoint)
19
+ request(Net::HTTP::Get, endpoint)
20
+ end
21
+
22
+ def post(endpoint, payload = nil)
23
+ request(Net::HTTP::Post, endpoint, payload)
24
+ end
25
+
26
+ def put(endpoint, payload = nil)
27
+ request(Net::HTTP::Put, endpoint, payload)
13
28
  end
14
29
 
15
- def post(endpoint, payload = {})
16
- request(endpoint, payload)
30
+ def delete(endpoint)
31
+ request(Net::HTTP::Delete, endpoint)
17
32
  end
18
33
 
19
34
  private
@@ -33,12 +48,12 @@ module Userlist
33
48
  end
34
49
  end
35
50
 
36
- def request(path, payload = {})
37
- request = Net::HTTP::Post.new(path)
51
+ def request(method, path, payload = nil)
52
+ request = method.new(path)
38
53
  request['Accept'] = 'application/json'
39
54
  request['Authorization'] = "Push #{token}"
40
55
  request['Content-Type'] = 'application/json; charset=UTF-8'
41
- request.body = JSON.dump(payload)
56
+ request.body = JSON.generate(payload) if payload
42
57
 
43
58
  logger.debug "Sending #{request.method} to #{URI.join(endpoint, request.path)} with body #{request.body}"
44
59
 
@@ -0,0 +1,23 @@
1
+ module Userlist
2
+ class Push
3
+ class Company < Resource
4
+ include Operations::Create
5
+ include Operations::Delete
6
+
7
+ def self.endpoint
8
+ '/companies'
9
+ end
10
+
11
+ has_many :relationships, type: 'Userlist::Push::Relationship'
12
+ has_many :users, type: 'Userlist::Push::User'
13
+ has_one :user, type: 'Userlist::Push::User'
14
+
15
+ def initialize(payload = {}, config = Userlist.config)
16
+ raise Userlist::ArgumentError, 'Missing required payload hash' unless payload
17
+ raise Userlist::ArgumentError, 'Missing required parameter :identifier' unless payload[:identifier]
18
+
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module Userlist
2
+ class Push
3
+ class Event < Resource
4
+ include Operations::Create
5
+
6
+ has_one :user, type: 'Userlist::Push::User'
7
+ has_one :company, type: 'Userlist::Push::Company'
8
+
9
+ def initialize(payload = {}, config = Userlist.config)
10
+ raise Userlist::ArgumentError, 'Missing required payload' unless payload
11
+ raise Userlist::ArgumentError, 'Missing required parameter :name' unless payload[:name]
12
+ raise Userlist::ArgumentError, 'Missing required parameter :user or :company' unless payload[:user] || payload[:company]
13
+
14
+ super
15
+ end
16
+
17
+ def occured_at
18
+ payload[:occured_at] || Time.now
19
+ end
20
+
21
+ def push?
22
+ (user.nil? || user.push?) && (company.nil? || company.push?)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Userlist
2
+ class Push
3
+ module Operations
4
+ module Create
5
+ module ClassMethods
6
+ def create(payload = {}, config = self.config)
7
+ return false unless resource = from_payload(payload, config)
8
+
9
+ strategy.call(:post, endpoint, resource) if resource.create?
10
+ end
11
+
12
+ alias push create
13
+ end
14
+
15
+ def self.included(base)
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ def create?
20
+ push?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Userlist
2
+ class Push
3
+ module Operations
4
+ module Delete
5
+ module ClassMethods
6
+ def delete(payload = {}, config = self.config)
7
+ return false unless resource = from_payload(payload, config)
8
+
9
+ strategy.call(:delete, resource.url) if resource.delete?
10
+ end
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ def delete?
18
+ true
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ module Userlist
2
+ class Push
3
+ class Relation
4
+ def initialize(scope, type, operations = [])
5
+ @scope = scope
6
+ @type = type
7
+
8
+ operations.each { |operation| singleton_class.send(:include, operation::ClassMethods) }
9
+ end
10
+
11
+ attr_reader :scope, :type
12
+
13
+ def from_payload(payload, config = self.config)
14
+ type.from_payload(payload, config)
15
+ end
16
+
17
+ def endpoint
18
+ type.endpoint
19
+ end
20
+
21
+ def strategy
22
+ scope.strategy
23
+ end
24
+
25
+ def config
26
+ scope.config
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ module Userlist
2
+ class Push
3
+ class Relationship < Resource
4
+ include Operations::Create
5
+ include Operations::Delete
6
+
7
+ has_one :user, type: 'Userlist::Push::User'
8
+ has_one :company, type: 'Userlist::Push::Company'
9
+
10
+ def initialize(payload = {}, config = Userlist.config)
11
+ raise Userlist::ArgumentError, 'Missing required payload' unless payload
12
+ raise Userlist::ArgumentError, 'Missing required parameter :user or :company' unless payload[:user] || payload[:company]
13
+
14
+ super
15
+ end
16
+
17
+ def url
18
+ raise Userlist::Error, "Cannot generate url for #{self.class.name} without a user" unless user
19
+ raise Userlist::Error, "Cannot generate url for #{self.class.name} without a company" unless company
20
+
21
+ "#{self.class.endpoint}/#{user.identifier}/#{company.identifier}"
22
+ end
23
+
24
+ def push?
25
+ user&.push? && company&.push?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,119 @@
1
+ module Userlist
2
+ class Push
3
+ class Resource
4
+ class << self
5
+ def resource_name
6
+ name.split('::')[-1]
7
+ end
8
+
9
+ def endpoint
10
+ "/#{resource_name.downcase}s"
11
+ end
12
+
13
+ def from_payload(payload, config = Userlist.config)
14
+ return payload if payload.nil?
15
+ return payload if payload.is_a?(self)
16
+
17
+ payload = { identifier: payload } if payload.is_a?(String) || payload.is_a?(Numeric)
18
+
19
+ new(payload, config)
20
+ end
21
+
22
+ def relationship_names
23
+ @relationship_names ||= Set.new
24
+ end
25
+
26
+ protected
27
+
28
+ def has_one(name, type:) # rubocop:disable Naming/PredicateName
29
+ relationship_names << name.to_sym
30
+
31
+ generated_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
32
+ def #{name}
33
+ #{type}.from_payload(payload[:#{name}], config)
34
+ end
35
+ RUBY
36
+ end
37
+
38
+ def has_many(name, type:) # rubocop:disable Naming/PredicateName
39
+ relationship_names << name.to_sym
40
+
41
+ generated_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
42
+ def #{name}
43
+ ResourceCollection.new(payload[:#{name}], #{type}, config)
44
+ end
45
+ RUBY
46
+ end
47
+
48
+ private
49
+
50
+ def generated_methods
51
+ @generated_methods ||= Module.new.tap { |mod| include mod }
52
+ end
53
+ end
54
+
55
+ attr_reader :payload, :config
56
+
57
+ def initialize(payload = {}, config = Userlist.config)
58
+ @payload = payload
59
+ @config = config
60
+ end
61
+
62
+ def respond_to_missing?(method, include_private = false)
63
+ attribute = method.to_s.sub(/=$/, '')
64
+ payload.key?(attribute.to_sym) || super
65
+ end
66
+
67
+ def to_hash
68
+ Serializer.serialize(self)
69
+ end
70
+ alias to_h to_hash
71
+
72
+ def to_json(*args)
73
+ to_hash.to_json(*args)
74
+ end
75
+
76
+ def url
77
+ "#{self.class.endpoint}/#{identifier}"
78
+ end
79
+
80
+ def identifier
81
+ payload[:identifier]
82
+ end
83
+
84
+ def hash
85
+ self.class.hash & payload.hash
86
+ end
87
+
88
+ def eql?(other)
89
+ hash == other.hash
90
+ end
91
+ alias == eql?
92
+
93
+ def attribute_names
94
+ payload.keys.map(&:to_sym) - relationship_names
95
+ end
96
+
97
+ def relationship_names
98
+ self.class.relationship_names.to_a
99
+ end
100
+
101
+ def push?
102
+ true
103
+ end
104
+
105
+ private
106
+
107
+ def method_missing(method, *args, &block)
108
+ if method.to_s =~ /=$/
109
+ attribute = method.to_s.sub(/=$/, '')
110
+ payload[attribute.to_sym] = args.first
111
+ elsif payload.key?(method.to_sym)
112
+ payload[method.to_sym]
113
+ else
114
+ super
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,21 @@
1
+ module Userlist
2
+ class Push
3
+ class ResourceCollection
4
+ include Enumerable
5
+
6
+ attr_reader :collection, :type, :config
7
+
8
+ def initialize(collection, type, config = Userlist.config)
9
+ @collection = Array(collection)
10
+ @type = type
11
+ @config = config
12
+ end
13
+
14
+ def each
15
+ collection.each do |resource|
16
+ yield type.from_payload(resource, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ module Userlist
2
+ class Push
3
+ class Serializer
4
+ def self.serialize(resource)
5
+ new.serialize(resource)
6
+ end
7
+
8
+ def serialize(resource)
9
+ resource = serialize_resource(resource) if resource.is_a?(Userlist::Push::Resource)
10
+ resource
11
+ end
12
+
13
+ private
14
+
15
+ def serialize_resource(resource)
16
+ return resource.identifier if serialized_resources.include?(resource)
17
+
18
+ serialized_resources << resource
19
+
20
+ return unless resource.push?
21
+
22
+ serialized = {}
23
+
24
+ resource.attribute_names.each do |name|
25
+ serialized[name] = resource.send(name)
26
+ end
27
+
28
+ resource.relationship_names.each do |name|
29
+ next unless result = serialize_relationship(resource.send(name))
30
+
31
+ serialized[name] = result
32
+ end
33
+
34
+ serialized
35
+ end
36
+
37
+ def serialize_relationship(relationship)
38
+ return unless relationship
39
+
40
+ case relationship
41
+ when Userlist::Push::ResourceCollection
42
+ serialize_collection(relationship)
43
+ when Userlist::Push::Resource
44
+ serialize_resource(relationship)
45
+ else
46
+ raise "Cannot serialize relationship type: #{relationship.class}"
47
+ end
48
+ end
49
+
50
+ def serialize_collection(collection)
51
+ serialized = collection
52
+ .map(&method(:serialize_relationship))
53
+ .compact
54
+ .reject(&:empty?)
55
+
56
+ serialized unless serialized.empty?
57
+ end
58
+
59
+ def serialized_resources
60
+ @serialized_resources ||= Set.new
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,17 +1,31 @@
1
- require 'userlist/push/strategies/null'
2
- require 'userlist/push/strategies/direct'
3
- require 'userlist/push/strategies/threaded'
4
-
5
1
  module Userlist
6
2
  class Push
7
3
  module Strategies
8
4
  def self.strategy_for(strategy, config = {})
9
- strategy = Userlist::Push::Strategies.const_get(strategy.to_s.capitalize) if strategy.is_a?(Symbol) || strategy.is_a?(String)
10
-
5
+ strategy = lookup_strategy(strategy)
11
6
  strategy = strategy.new(config) if strategy.respond_to?(:new)
12
7
 
13
8
  strategy
14
9
  end
10
+
11
+ def self.lookup_strategy(strategy)
12
+ return strategy unless strategy.is_a?(Symbol) || strategy.is_a?(String)
13
+
14
+ require_strategy(strategy)
15
+ const_get(strategy.to_s.capitalize, false)
16
+ end
17
+
18
+ def self.strategy_defined?(strategy)
19
+ return true unless strategy.is_a?(Symbol) || strategy.is_a?(String)
20
+
21
+ const_defined?(strategy.to_s.capitalize, false)
22
+ end
23
+
24
+ def self.require_strategy(strategy)
25
+ return unless strategy.is_a?(Symbol) || strategy.is_a?(String)
26
+
27
+ require("userlist/push/strategies/#{strategy}") unless strategy_defined?(strategy)
28
+ end
15
29
  end
16
30
  end
17
31
  end
@@ -0,0 +1,37 @@
1
+ require 'sidekiq'
2
+
3
+ require 'userlist/push/strategies/sidekiq/worker'
4
+
5
+ module Userlist
6
+ class Push
7
+ module Strategies
8
+ class Sidekiq
9
+ def initialize(config = {})
10
+ @config = Userlist.config.merge(config)
11
+ end
12
+
13
+ def call(*args)
14
+ ::Sidekiq::Client.push(default_options.merge(options).merge('args' => args))
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :config
20
+
21
+ def options
22
+ @options ||= begin
23
+ options = config.push_strategy_options || {}
24
+ options.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
25
+ end
26
+ end
27
+
28
+ def default_options
29
+ {
30
+ 'class' => 'Userlist::Push::Strategies::Sidekiq::Worker',
31
+ 'queue' => 'default'
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ require 'sidekiq'
2
+
3
+ module Userlist
4
+ class Push
5
+ module Strategies
6
+ class Sidekiq
7
+ class Worker
8
+ include ::Sidekiq::Worker
9
+
10
+ def perform(method, *args)
11
+ client = Userlist::Push::Client.new
12
+ client.public_send(method, *args)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -20,7 +20,7 @@ module Userlist
20
20
  attr_reader :queue, :worker
21
21
 
22
22
  def stop_worker
23
- worker && worker.stop
23
+ worker&.stop
24
24
  end
25
25
  end
26
26
  end
@@ -19,12 +19,12 @@ module Userlist
19
19
 
20
20
  loop do
21
21
  begin
22
- method, url, payload = *queue.pop
22
+ method, *args = *queue.pop
23
23
  break if method == :stop
24
24
 
25
- client.public_send(method, url, payload)
26
- rescue StandardError => exception
27
- logger.error "Failed to deliver payload: [#{exception.class.name}] #{exception.message}"
25
+ client.public_send(method, *args)
26
+ rescue StandardError => e
27
+ logger.error "Failed to deliver payload: [#{e.class.name}] #{e.message}"
28
28
  end
29
29
  end
30
30
 
@@ -0,0 +1,19 @@
1
+ module Userlist
2
+ class Push
3
+ class User < Resource
4
+ include Operations::Create
5
+ include Operations::Delete
6
+
7
+ has_many :relationships, type: 'Userlist::Push::Relationship'
8
+ has_many :companies, type: 'Userlist::Push::Company'
9
+ has_one :company, type: 'Userlist::Push::Company'
10
+
11
+ def initialize(payload = {}, config = Userlist.config)
12
+ raise Userlist::ArgumentError, 'Missing required payload' unless payload
13
+ raise Userlist::ArgumentError, 'Missing required parameter :identifier' unless payload[:identifier]
14
+
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ module Userlist
2
+ class Token
3
+ def self.generate(user, configuration = {})
4
+ config = Userlist.config.merge(configuration)
5
+
6
+ raise Userlist::ConfigurationError, :push_key unless config.push_key
7
+ raise Userlist::ConfigurationError, :push_id unless config.push_id
8
+ raise Userlist::ArgumentError, 'Missing required user or identifier' unless user
9
+
10
+ user = Userlist::Push::User.from_payload(user, config)
11
+
12
+ now = Time.now.utc.to_i
13
+
14
+ header = {
15
+ kid: config.push_id,
16
+ alg: 'HS256'
17
+ }
18
+
19
+ payload = {
20
+ sub: user.identifier,
21
+ exp: now + config.token_lifetime,
22
+ iat: now
23
+ }
24
+
25
+ new(payload: payload, header: header, key: config.push_key).to_s
26
+ end
27
+
28
+ def initialize(payload:, header:, key:, algorithm: 'HS256')
29
+ @payload = payload
30
+ @header = header
31
+ @algorithm = algorithm
32
+ @key = key
33
+ end
34
+
35
+ def encoded_header
36
+ encode(header)
37
+ end
38
+
39
+ def encoded_payload
40
+ encode(payload)
41
+ end
42
+
43
+ def encoded_header_and_payload
44
+ "#{encoded_header}.#{encoded_payload}"
45
+ end
46
+
47
+ def signature
48
+ digest = OpenSSL::Digest.new(algorithm.sub('HS', 'SHA'))
49
+ signature = OpenSSL::HMAC.digest(digest, key, encoded_header_and_payload)
50
+
51
+ encode(signature)
52
+ end
53
+
54
+ def to_s
55
+ "#{encoded_header_and_payload}.#{signature}"
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :payload, :header, :algorithm, :key
61
+
62
+ def encode(payload)
63
+ payload = JSON.generate(payload) unless payload.is_a?(String)
64
+
65
+ Base64.urlsafe_encode64(payload).tr('=', '')
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module Userlist
2
- VERSION = '0.2.1'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
@@ -6,9 +6,9 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'userlist'
7
7
  spec.version = Userlist::VERSION
8
8
  spec.authors = ['Benedikt Deicke']
9
- spec.email = ['benedikt@userlist.io']
9
+ spec.email = ['benedikt@userlist.com']
10
10
 
11
- spec.summary = 'Ruby wrapper for the Userlist.io API'
11
+ spec.summary = 'Ruby wrapper for the Userlist API'
12
12
  spec.homepage = 'http://github.com/userlistio/userlist-ruby'
13
13
  spec.license = 'MIT'
14
14
 
@@ -19,10 +19,11 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.required_ruby_version = '>= 2.3'
22
+ spec.required_ruby_version = '>= 2.4'
23
23
 
24
- spec.add_development_dependency 'bundler', '~> 1.15'
25
- spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'bundler', '>= 1.15'
25
+ spec.add_development_dependency 'jwt', '~> 2.2'
26
+ spec.add_development_dependency 'rake', '~> 12.3', '>= 12.3.3'
26
27
  spec.add_development_dependency 'rspec', '~> 3.0'
27
28
  spec.add_development_dependency 'webmock', '~> 1.22'
28
29
  end
metadata CHANGED
@@ -1,43 +1,63 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: userlist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benedikt Deicke
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-13 00:00:00.000000000 Z
11
+ date: 2021-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.15'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '10.0'
47
+ version: '12.3'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 12.3.3
34
51
  type: :development
35
52
  prerelease: false
36
53
  version_requirements: !ruby/object:Gem::Requirement
37
54
  requirements:
38
55
  - - "~>"
39
56
  - !ruby/object:Gem::Version
40
- version: '10.0'
57
+ version: '12.3'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 12.3.3
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: rspec
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -66,17 +86,17 @@ dependencies:
66
86
  - - "~>"
67
87
  - !ruby/object:Gem::Version
68
88
  version: '1.22'
69
- description:
89
+ description:
70
90
  email:
71
- - benedikt@userlist.io
91
+ - benedikt@userlist.com
72
92
  executables: []
73
93
  extensions: []
74
94
  extra_rdoc_files: []
75
95
  files:
96
+ - ".github/workflows/test.yml"
76
97
  - ".gitignore"
77
98
  - ".rspec"
78
99
  - ".rubocop.yml"
79
- - ".travis.yml"
80
100
  - CODE_OF_CONDUCT.md
81
101
  - Gemfile
82
102
  - Guardfile
@@ -90,18 +110,31 @@ files:
90
110
  - lib/userlist/logging.rb
91
111
  - lib/userlist/push.rb
92
112
  - lib/userlist/push/client.rb
113
+ - lib/userlist/push/company.rb
114
+ - lib/userlist/push/event.rb
115
+ - lib/userlist/push/operations/create.rb
116
+ - lib/userlist/push/operations/delete.rb
117
+ - lib/userlist/push/relation.rb
118
+ - lib/userlist/push/relationship.rb
119
+ - lib/userlist/push/resource.rb
120
+ - lib/userlist/push/resource_collection.rb
121
+ - lib/userlist/push/serializer.rb
93
122
  - lib/userlist/push/strategies.rb
94
123
  - lib/userlist/push/strategies/direct.rb
95
124
  - lib/userlist/push/strategies/null.rb
125
+ - lib/userlist/push/strategies/sidekiq.rb
126
+ - lib/userlist/push/strategies/sidekiq/worker.rb
96
127
  - lib/userlist/push/strategies/threaded.rb
97
128
  - lib/userlist/push/strategies/threaded/worker.rb
129
+ - lib/userlist/push/user.rb
130
+ - lib/userlist/token.rb
98
131
  - lib/userlist/version.rb
99
132
  - userlist.gemspec
100
133
  homepage: http://github.com/userlistio/userlist-ruby
101
134
  licenses:
102
135
  - MIT
103
136
  metadata: {}
104
- post_install_message:
137
+ post_install_message:
105
138
  rdoc_options: []
106
139
  require_paths:
107
140
  - lib
@@ -109,15 +142,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
142
  requirements:
110
143
  - - ">="
111
144
  - !ruby/object:Gem::Version
112
- version: '2.3'
145
+ version: '2.4'
113
146
  required_rubygems_version: !ruby/object:Gem::Requirement
114
147
  requirements:
115
148
  - - ">="
116
149
  - !ruby/object:Gem::Version
117
150
  version: '0'
118
151
  requirements: []
119
- rubygems_version: 3.0.3
120
- signing_key:
152
+ rubygems_version: 3.1.4
153
+ signing_key:
121
154
  specification_version: 4
122
- summary: Ruby wrapper for the Userlist.io API
155
+ summary: Ruby wrapper for the Userlist API
123
156
  test_files: []
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.4.1
5
- before_install: gem install bundler -v 1.15.4