userlist 0.2.1 → 0.5.0

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