userlist 0.2.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +21 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -4
- data/Gemfile +3 -0
- data/README.md +185 -6
- data/lib/userlist.rb +28 -1
- data/lib/userlist/config.rb +11 -4
- data/lib/userlist/push.rb +38 -35
- data/lib/userlist/push/client.rb +20 -5
- data/lib/userlist/push/company.rb +23 -0
- data/lib/userlist/push/event.rb +26 -0
- data/lib/userlist/push/operations/create.rb +25 -0
- data/lib/userlist/push/operations/delete.rb +23 -0
- data/lib/userlist/push/relation.rb +30 -0
- data/lib/userlist/push/relationship.rb +30 -0
- data/lib/userlist/push/resource.rb +125 -0
- data/lib/userlist/push/resource_collection.rb +32 -0
- data/lib/userlist/push/serializer.rb +66 -0
- data/lib/userlist/push/strategies.rb +20 -6
- data/lib/userlist/push/strategies/sidekiq.rb +37 -0
- data/lib/userlist/push/strategies/sidekiq/worker.rb +18 -0
- data/lib/userlist/push/strategies/threaded.rb +1 -1
- data/lib/userlist/push/strategies/threaded/worker.rb +4 -4
- data/lib/userlist/push/user.rb +19 -0
- data/lib/userlist/token.rb +68 -0
- data/lib/userlist/version.rb +1 -1
- data/userlist.gemspec +6 -5
- metadata +48 -16
- data/.travis.yml +0 -5
data/lib/userlist/push/client.rb
CHANGED
@@ -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
|
16
|
-
request(
|
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 =
|
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.
|
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', inverse: :company
|
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,30 @@
|
|
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' unless payload[:user]
|
13
|
+
raise Userlist::ArgumentError, 'Missing required parameter :company' unless payload[:company]
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def url
|
19
|
+
raise Userlist::Error, "Cannot generate url for #{self.class.name} without a user" unless user
|
20
|
+
raise Userlist::Error, "Cannot generate url for #{self.class.name} without a company" unless company
|
21
|
+
|
22
|
+
"#{self.class.endpoint}/#{user.identifier}/#{company.identifier}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def push?
|
26
|
+
user&.push? && company&.push?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,125 @@
|
|
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 ||= relationships.keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def relationships
|
27
|
+
@relationships ||= {}
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def has_one(name, type:) # rubocop:disable Naming/PredicateName
|
33
|
+
relationships[name.to_sym] = { type: type }
|
34
|
+
|
35
|
+
generated_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
36
|
+
def #{name}
|
37
|
+
#{type}.from_payload(payload[:#{name}], config)
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_many(name, **options) # rubocop:disable Naming/PredicateName
|
43
|
+
relationships[name.to_sym] = options
|
44
|
+
|
45
|
+
generated_methods.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
46
|
+
def #{name}
|
47
|
+
relationship = self.class.relationships[:#{name}]
|
48
|
+
|
49
|
+
ResourceCollection.new(payload[:#{name}], relationship, self, config)
|
50
|
+
end
|
51
|
+
RUBY
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def generated_methods
|
57
|
+
@generated_methods ||= Module.new.tap { |mod| include mod }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :payload, :config
|
62
|
+
|
63
|
+
def initialize(payload = {}, config = Userlist.config)
|
64
|
+
@payload = payload
|
65
|
+
@config = config
|
66
|
+
end
|
67
|
+
|
68
|
+
def respond_to_missing?(method, include_private = false)
|
69
|
+
attribute = method.to_s.sub(/=$/, '')
|
70
|
+
payload.key?(attribute.to_sym) || super
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_hash
|
74
|
+
Serializer.serialize(self)
|
75
|
+
end
|
76
|
+
alias to_h to_hash
|
77
|
+
|
78
|
+
def to_json(*args)
|
79
|
+
to_hash.to_json(*args)
|
80
|
+
end
|
81
|
+
|
82
|
+
def url
|
83
|
+
"#{self.class.endpoint}/#{identifier}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def identifier
|
87
|
+
payload[:identifier]
|
88
|
+
end
|
89
|
+
|
90
|
+
def hash
|
91
|
+
self.class.hash & payload.hash
|
92
|
+
end
|
93
|
+
|
94
|
+
def eql?(other)
|
95
|
+
hash == other.hash
|
96
|
+
end
|
97
|
+
alias == eql?
|
98
|
+
|
99
|
+
def attribute_names
|
100
|
+
payload.keys.map(&:to_sym) - relationship_names
|
101
|
+
end
|
102
|
+
|
103
|
+
def relationship_names
|
104
|
+
self.class.relationship_names.to_a
|
105
|
+
end
|
106
|
+
|
107
|
+
def push?
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def method_missing(method, *args, &block)
|
114
|
+
if method.to_s =~ /=$/
|
115
|
+
attribute = method.to_s.sub(/=$/, '')
|
116
|
+
payload[attribute.to_sym] = args.first
|
117
|
+
elsif payload.key?(method.to_sym)
|
118
|
+
payload[method.to_sym]
|
119
|
+
else
|
120
|
+
super
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Userlist
|
2
|
+
class Push
|
3
|
+
class ResourceCollection
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :collection, :relationship, :owner, :config
|
7
|
+
|
8
|
+
def initialize(collection, relationship, owner, config = Userlist.config)
|
9
|
+
@collection = Array(collection)
|
10
|
+
@relationship = relationship
|
11
|
+
@owner = owner
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
collection.each do |resource|
|
17
|
+
resource[inverse] = owner if inverse && resource.is_a?(Hash)
|
18
|
+
|
19
|
+
yield type.from_payload(resource, config)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def type
|
24
|
+
Object.const_get(relationship[:type])
|
25
|
+
end
|
26
|
+
|
27
|
+
def inverse
|
28
|
+
relationship[:inverse]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Userlist
|
4
|
+
class Push
|
5
|
+
class Serializer
|
6
|
+
def self.serialize(resource)
|
7
|
+
new.serialize(resource)
|
8
|
+
end
|
9
|
+
|
10
|
+
def serialize(resource)
|
11
|
+
resource = serialize_resource(resource) if resource.is_a?(Userlist::Push::Resource)
|
12
|
+
resource
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def serialize_resource(resource)
|
18
|
+
return resource.identifier if serialized_resources.include?(resource)
|
19
|
+
|
20
|
+
serialized_resources << resource
|
21
|
+
|
22
|
+
return unless resource.push?
|
23
|
+
|
24
|
+
serialized = {}
|
25
|
+
|
26
|
+
resource.attribute_names.each do |name|
|
27
|
+
serialized[name] = resource.send(name)
|
28
|
+
end
|
29
|
+
|
30
|
+
resource.relationship_names.each do |name|
|
31
|
+
next unless result = serialize_relationship(resource.send(name))
|
32
|
+
|
33
|
+
serialized[name] = result
|
34
|
+
end
|
35
|
+
|
36
|
+
serialized
|
37
|
+
end
|
38
|
+
|
39
|
+
def serialize_relationship(relationship)
|
40
|
+
return unless relationship
|
41
|
+
|
42
|
+
case relationship
|
43
|
+
when Userlist::Push::ResourceCollection
|
44
|
+
serialize_collection(relationship)
|
45
|
+
when Userlist::Push::Resource
|
46
|
+
serialize_resource(relationship)
|
47
|
+
else
|
48
|
+
raise "Cannot serialize relationship type: #{relationship.class}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def serialize_collection(collection)
|
53
|
+
serialized = collection
|
54
|
+
.map(&method(:serialize_relationship))
|
55
|
+
.compact
|
56
|
+
.reject(&:empty?)
|
57
|
+
|
58
|
+
serialized unless serialized.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def serialized_resources
|
62
|
+
@serialized_resources ||= Set.new
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
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 =
|
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
|