justrelate_sdk 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ module JustRelate; module Core
2
+ # +ItemEnumerator+ provides methods for accessing items identified by their ID.
3
+ # It implements {#each} and includes the
4
+ # {http://ruby-doc.org/core/Enumerable.html Enumerable} mixin,
5
+ # which provides methods such as +#map+, +#select+ or +#take+.
6
+ # @api public
7
+ class ItemEnumerator
8
+ include Enumerable
9
+ include Core::Mixins::Inspectable
10
+ inspectable :length, :total
11
+
12
+ # Returns the IDs of the items to enumerate.
13
+ # @return [Array<String>]
14
+ # @api public
15
+ attr_reader :ids
16
+
17
+ # If the ItemEnumerator is the result of a search, it returns the total number of search hits.
18
+ # Otherwise, it returns {#length}.
19
+ # @return [Fixnum]
20
+ # @api public
21
+ attr_reader :total
22
+
23
+ def initialize(ids, total: nil)
24
+ @ids = ids
25
+ @total = total || ids.length
26
+ end
27
+
28
+ # Iterates over the {#ids} and fetches the corresponding items on demand.
29
+ # @overload each
30
+ # Calls the block once for each item, passing this item as a parameter.
31
+ # @yieldparam item [BasicResource]
32
+ # @return [void]
33
+ # @overload each
34
+ # If no block is given, an {http://ruby-doc.org/core/Enumerator.html enumerator}
35
+ # is returned instead.
36
+ # @return [Enumerator<BasicResource>]
37
+ # @raise [Errors::ResourceNotFound] if at least one of the IDs could not be found.
38
+ # @api public
39
+ def each(&block)
40
+ return enum_for(:each) unless block_given?
41
+
42
+ server_limit = 1_000
43
+ @ids.each_slice(server_limit) do |sliced_ids|
44
+ RestApi.instance.get('mget', {'ids' => sliced_ids}).map do |item|
45
+ block.call "JustRelate::#{item['base_type']}".constantize.new(item)
46
+ end
47
+ end
48
+ end
49
+
50
+ # Returns the number of items.
51
+ # Prefer this method over +Enumerable#count+
52
+ # because +#length+ doesn't fetch the items and therefore is faster than +Enumerable#count+.
53
+ # @return [Fixnum]
54
+ # @api public
55
+ def length
56
+ @ids.length
57
+ end
58
+
59
+ alias_method :size, :length
60
+ end
61
+ end; end
@@ -0,0 +1,41 @@
1
+ require 'action_dispatch'
2
+
3
+ module JustRelate; module Core
4
+ class LogSubscriber < ActiveSupport::LogSubscriber
5
+ def logger
6
+ self.class.logger.presence or super
7
+ end
8
+
9
+ def request(event)
10
+ info { "#{event.payload[:method].to_s.upcase} #{event.payload[:resource_path]}" }
11
+ request_payload = event.payload[:request_payload]
12
+ if request_payload.present?
13
+ debug { " request body: #{parameter_filter.filter(request_payload)}" }
14
+ end
15
+ end
16
+
17
+ def response(event)
18
+ r = event.payload[:response]
19
+ info {
20
+ " #{r.code} #{r.message} #{r.body.to_s.length} (total: #{event.duration.round(1)}ms)"
21
+ }
22
+ debug {
23
+ response_payload = MultiJson.load(r.body)
24
+ " response body: #{parameter_filter.filter(response_payload)}"
25
+ }
26
+ end
27
+
28
+ def establish_connection(event)
29
+ debug {
30
+ attempt = event.payload[:attempt]
31
+ " Establishing connection on attempt #{attempt} (#{event.duration.round(1)}ms)"
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ def parameter_filter
38
+ @parameter_filter ||= ::ActionDispatch::Http::ParameterFilter.new(['password'])
39
+ end
40
+ end
41
+ end; end
@@ -0,0 +1,135 @@
1
+ module JustRelate; module Core; module Mixins
2
+ # +AttributeProvider+ provides multiple ways to access the attributes of an item.
3
+ # All attributes are available using {#[]}, {#attributes}
4
+ # or a method named like the attribute.
5
+ # @example
6
+ # contact
7
+ # # => JustRelate::Contact
8
+ #
9
+ # contact.first_name
10
+ # # => "John"
11
+ #
12
+ # contact['first_name']
13
+ # # => "John"
14
+ #
15
+ # contact[:first_name]
16
+ # # => "John"
17
+ #
18
+ # contact.unknown_attribute
19
+ # # => raises NoMethodError
20
+ #
21
+ # contact['unknown_attribute']
22
+ # # => nil
23
+ #
24
+ # contact.attributes
25
+ # # => {
26
+ # # ...
27
+ # # 'first_name' => 'John',
28
+ # # ...
29
+ # # }
30
+ # @api public
31
+ module AttributeProvider
32
+ def initialize(attributes = nil)
33
+ load_attributes(attributes || {})
34
+
35
+ super()
36
+ end
37
+
38
+ # Makes all attributes accessible as methods.
39
+ def method_missing(method_name, *args)
40
+ return self[method_name] if has_attribute?(method_name)
41
+ [
42
+ "#{method_name}_id",
43
+ "#{method_name.to_s.singularize}_ids",
44
+ ].each do |id_name|
45
+ return JustRelate.find(self[id_name]) if has_attribute?(id_name)
46
+ end
47
+
48
+ super
49
+ end
50
+
51
+ # Motivation see http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/
52
+ def respond_to_missing?(method_name, *)
53
+ return true if has_attribute?(method_name)
54
+ return true if has_attribute?("#{method_name}_id")
55
+ return true if has_attribute?("#{method_name.to_s.singularize}_ids")
56
+
57
+ super
58
+ end
59
+
60
+ def methods(*args)
61
+ super | @extra_methods
62
+ end
63
+
64
+ # Returns the value associated with +attribute_name+.
65
+ # Returns +nil+ if not found.
66
+ # @example
67
+ # contact['first_name']
68
+ # # => "John"
69
+ #
70
+ # contact[:first_name]
71
+ # # => "John"
72
+ #
73
+ # contact['nonexistent']
74
+ # # => nil
75
+ # @param attribute_name [String, Symbol]
76
+ # @return [Object, nil]
77
+ # @api public
78
+ def [](attribute_name)
79
+ @attrs[attribute_name.to_s]
80
+ end
81
+
82
+ # Returns the hash of all attribute names and their values.
83
+ # @return [HashWithIndifferentAccess]
84
+ # @api public
85
+ def attributes
86
+ @attrs
87
+ end
88
+
89
+ # Returns the value before type cast.
90
+ # Returns +nil+ if attribute_name not found.
91
+ # @example
92
+ # contact[:created_at]
93
+ # # => 2012-05-07 17:15:00 +0200
94
+ #
95
+ # contact.raw(:created_at)
96
+ # # => "2012-05-07T15:15:00+00:00"
97
+ # @return [Object, nil]
98
+ # @api public
99
+ def raw(attribute_name)
100
+ @raw_attrs[attribute_name] || @attrs[attribute_name]
101
+ end
102
+
103
+ protected
104
+
105
+ def load_attributes(attributes)
106
+ @raw_attrs = HashWithIndifferentAccess.new
107
+ @attrs = attributes.each_with_object(HashWithIndifferentAccess.new) do |(key, value), hash|
108
+ if key.ends_with?('_at')
109
+ @raw_attrs[key] = value
110
+ value = begin
111
+ Time.parse(value.to_s).in_time_zone
112
+ rescue ArgumentError
113
+ nil
114
+ end
115
+ end
116
+ hash[key] = value
117
+ end
118
+ @extra_methods = []
119
+ @attrs.keys.each do |key|
120
+ key = key.to_s
121
+ @extra_methods << key.to_sym
122
+ @extra_methods << key.gsub(/_id$/, '').to_sym if key.ends_with?('_id')
123
+ @extra_methods << key.gsub(/_ids$/, '').pluralize.to_sym if key.ends_with?('_ids')
124
+ end
125
+ @attrs.freeze
126
+ self
127
+ end
128
+
129
+ private
130
+
131
+ def has_attribute?(attribute_name)
132
+ @attrs.has_key?(attribute_name.to_s)
133
+ end
134
+ end
135
+ end; end; end
@@ -0,0 +1,98 @@
1
+ module JustRelate; module Core; module Mixins
2
+ # +ChangeLoggable+ provides access to the change log of a resource.
3
+ # A {ChangeLoggable::Change change log entry} contains the +before+ and +after+ values
4
+ # of all attributes that were changed by an update.
5
+ # It also includes the author (+changed_by+) and the timestamp (+changed_at+) of the change.
6
+ # @example
7
+ # contact
8
+ # # => JustRelate::Contact
9
+ #
10
+ # changes = contact.changes
11
+ # # => Array<ChangeLoggable::Change>
12
+ #
13
+ # change = changes.first
14
+ # # => ChangeLoggable::Change
15
+ #
16
+ # change.changed_by
17
+ # # => 'john_smith'
18
+ #
19
+ # change.changed_at
20
+ # # => Time
21
+ #
22
+ # change.details.keys
23
+ # # => ['email', 'locality']
24
+ #
25
+ # detail = change.details[:email]
26
+ # # => ChangeLoggable::Change::Detail
27
+ #
28
+ # detail.before
29
+ # # => old@example.com
30
+ #
31
+ # detail.after
32
+ # # => new@example.com
33
+ # @api public
34
+ module ChangeLoggable
35
+ # +Change+ represents a single change log entry contained in
36
+ # {ChangeLoggable#changes item.changes}.
37
+ # See {JustRelate::Core::Mixins::ChangeLoggable ChangeLoggable} for details.
38
+ # @api public
39
+ class Change
40
+ include Core::Mixins::AttributeProvider
41
+
42
+ def initialize(raw_change)
43
+ change = raw_change.dup
44
+ change['details'] = change['details'].each_with_object({}) do |(attr_name, detail), hash|
45
+ hash[attr_name] = Detail.new(detail)
46
+ end
47
+ super(change)
48
+ end
49
+
50
+ # @!attribute [r] changed_at
51
+ # Returns the timestamp of the change to the item.
52
+ # @return [Time]
53
+ # @api public
54
+
55
+ # @!attribute [r] changed_by
56
+ # Returns the login of the API user who made the change.
57
+ # @return [String]
58
+ # @api public
59
+
60
+ # @!attribute [r] details
61
+ # Returns the details of the change
62
+ # (i.e. +before+ and +after+ of every changed attribute)
63
+ # @return [Array<Detail>]
64
+ # @api public
65
+
66
+ # +Detail+ represents a single detail of a
67
+ # {JustRelate::Core::Mixins::ChangeLoggable::Change change},
68
+ # which can be accessed by means of {Change#details change.details}.
69
+ # See {JustRelate::Core::Mixins::ChangeLoggable ChangeLoggable} for details.
70
+ # @api public
71
+ class Detail
72
+ # @!attribute [r] before
73
+ # Returns the value before the change.
74
+ # @return [Object]
75
+ # @api public
76
+
77
+ # @!attribute [r] after
78
+ # Returns the value after the change.
79
+ # @return [Object]
80
+ # @api public
81
+
82
+ # flush yardoc!!!!
83
+ include Core::Mixins::AttributeProvider
84
+ end
85
+ end
86
+
87
+ # Returns the most recent changes made to this item.
88
+ # @param limit [Fixnum] the number of changes to return at most.
89
+ # Maximum: +100+. Default: +10+.
90
+ # @return [Array<Change>]
91
+ # @api public
92
+ def changes(limit: 10)
93
+ RestApi.instance.get("#{path}/changes", {"limit" => limit})['results'].map do |change|
94
+ Change.new(change)
95
+ end
96
+ end
97
+ end
98
+ end; end; end
@@ -0,0 +1,24 @@
1
+ module JustRelate; module Core; module Mixins
2
+ # +Findable+ lets you fetch an item using {ClassMethods#find .find}.
3
+ # @example
4
+ # JustRelate::Contact.find('e70a7123f499c5e0e9972ab4dbfb8fe3')
5
+ # # => JustRelate::Contact
6
+ # @api public
7
+ module Findable
8
+ extend ActiveSupport::Concern
9
+
10
+ # @api public
11
+ module ClassMethods
12
+ # Returns the requested item.
13
+ # @param id [String] the ID of the item.
14
+ # @return [BasicResource]
15
+ # @raise [Errors::ResourceNotFound]
16
+ # if the ID could not be found or the base type did not match.
17
+ # @api public
18
+ def find(id)
19
+ new({'id' => id}).reload
20
+ end
21
+ end
22
+ # @!parse extend ClassMethods
23
+ end
24
+ end; end; end
@@ -0,0 +1,27 @@
1
+ module JustRelate; module Core; module Mixins
2
+ module Inspectable
3
+ extend ActiveSupport::Concern
4
+
5
+ def inspect
6
+ field_values = self.class.inspectable_fields.map do |field|
7
+ value = self.send(field).inspect
8
+
9
+ "#{field}=#{value}"
10
+ end
11
+
12
+ "#<#{self.class.name} #{field_values.join(', ')}>"
13
+ end
14
+
15
+ included do
16
+ cattr_accessor :inspectable_fields
17
+
18
+ self.inspectable_fields = []
19
+ end
20
+
21
+ module ClassMethods
22
+ def inspectable(*fields)
23
+ self.inspectable_fields = *fields
24
+ end
25
+ end
26
+ end
27
+ end; end; end
@@ -0,0 +1,17 @@
1
+ module JustRelate; module Core; module Mixins
2
+ # +MergeAndDeletable+ provides the common {#merge_and_delete} method
3
+ # for {Account accounts} and {Contact contacts}.
4
+ # @api public
5
+ module MergeAndDeletable
6
+ # Assigns the items associated with this item to the account or contact
7
+ # whose ID is +merge_into_id+. Afterwards, the current item is deleted.
8
+ # @param merge_into_id [String]
9
+ # the ID of the account or contact to which the associated items are assigned.
10
+ # @return [self] the deleted item.
11
+ # @api public
12
+ def merge_and_delete(merge_into_id)
13
+ load_attributes(
14
+ RestApi.instance.post("#{path}/merge_and_delete", {"merge_into_id" => merge_into_id}))
15
+ end
16
+ end
17
+ end; end; end
@@ -0,0 +1,102 @@
1
+ require "active_support/concern"
2
+
3
+ module JustRelate; module Core; module Mixins
4
+ # +Modifiable+ is a collection of methods that are used to {ClassMethods#create .create},
5
+ # {#update}, {#delete}, and {#undelete} a JustRelate item.
6
+ # @api public
7
+ module Modifiable
8
+ extend ActiveSupport::Concern
9
+
10
+ # @api public
11
+ module ClassMethods
12
+ # Creates a new item using the given +attributes+.
13
+ # @example
14
+ # JustRelate::Contact.create({
15
+ # language: 'en',
16
+ # last_name: 'Smith',
17
+ # })
18
+ # # => JustRelate::Contact
19
+ # @param attributes [Hash{String, Symbol => String}] the attributes of the new item.
20
+ # @return [BasicResource] the created item.
21
+ # @raise [Errors::InvalidKeys] if +attributes+ contains unknown attribute names.
22
+ # @raise [Errors::InvalidValues] if +attributes+ contains incorrect values.
23
+ # @api public
24
+ def create(attributes = {})
25
+ new(RestApi.instance.post(path, attributes))
26
+ end
27
+ end
28
+ # @!parse extend ClassMethods
29
+
30
+ # Updates the attributes of this item.
31
+ # @example
32
+ # contact.last_name
33
+ # # => 'Smith'
34
+ #
35
+ # contact.locality
36
+ # # => 'New York'
37
+ #
38
+ # contact.update({locality: 'Boston'})
39
+ # # => JustRelate::Contact
40
+ #
41
+ # contact.last_name
42
+ # # => 'Smith'
43
+ #
44
+ # contact.locality
45
+ # # => 'Boston'
46
+ # @param attributes [Hash{String, Symbol => String}] the new attributes.
47
+ # @return [self] the updated item.
48
+ # @raise [Errors::InvalidKeys] if +attributes+ contains unknown attribute names.
49
+ # @raise [Errors::InvalidValues] if +attributes+ contains incorrect values.
50
+ # @raise [Errors::ResourceConflict] if the item has been changed concurrently.
51
+ # {Core::BasicResource#reload Reload} it, review the changes and retry.
52
+ # @api public
53
+ def update(attributes = {})
54
+ load_attributes(RestApi.instance.put(path, attributes, if_match_header))
55
+ end
56
+
57
+ # Soft-deletes this item (i.e. marks it as deleted).
58
+ #
59
+ # The deleted item can be {#undelete undeleted}.
60
+ # @example
61
+ # contact.deleted?
62
+ # # => false
63
+ #
64
+ # contact.delete
65
+ # # => JustRelate::Contact
66
+ #
67
+ # contact.deleted?
68
+ # # => true
69
+ # @return [self] the deleted item.
70
+ # @raise [Errors::ResourceConflict] if the item has been changed concurrently.
71
+ # {Core::BasicResource#reload Reload} it, review the changes and retry.
72
+ # @api public
73
+ def delete
74
+ load_attributes(RestApi.instance.delete(path, nil, if_match_header))
75
+ end
76
+
77
+ # Undeletes this deleted item.
78
+ # @example
79
+ # contact.deleted?
80
+ # # => true
81
+ #
82
+ # contact.undelete
83
+ # # => JustRelate::Contact
84
+ #
85
+ # contact.deleted?
86
+ # # => false
87
+ # @return [self] the undeleted item.
88
+ # @api public
89
+ def undelete
90
+ load_attributes(RestApi.instance.put("#{path}/undelete", {}))
91
+ end
92
+
93
+ # Returns +true+ if the item was deleted (i.e. +item.deleted_at+ is not empty).
94
+ # @return [Boolean]
95
+ # @api public
96
+ def deleted?
97
+ self['deleted_at'].present?
98
+ end
99
+
100
+ alias_method :destroy, :delete
101
+ end
102
+ end; end; end
@@ -0,0 +1,88 @@
1
+ module JustRelate; module Core; module Mixins
2
+ # +Searchable+ provides several methods related to searching.
3
+ # @api public
4
+ module Searchable
5
+ extend ActiveSupport::Concern
6
+
7
+ # @api public
8
+ module ClassMethods
9
+ # Returns the item of this base type that was created first.
10
+ # @return [BasicResource]
11
+ # @api public
12
+ def first
13
+ search_configurator.
14
+ sort_by('created_at').
15
+ limit(1).
16
+ perform_search.
17
+ first
18
+ end
19
+
20
+ # Returns an {JustRelate::Core::ItemEnumerator enumerator} for iterating over all items
21
+ # of this base type. The items are sorted by +created_at+.
22
+ # @param include_deleted [Boolean] whether to include deleted items. Default: +false+.
23
+ # @return [ItemEnumerator]
24
+ # @api public
25
+ def all(include_deleted: false)
26
+ search_configurator.
27
+ sort_by('created_at').
28
+ unlimited.
29
+ include_deleted(include_deleted).
30
+ perform_search
31
+ end
32
+
33
+ # Returns a new {JustRelate::Core::SearchConfigurator SearchConfigurator} set to the given
34
+ # filter (+field+, +condition+, +value+). Additionally, it is limited
35
+ # to this base type and can be further refined using chainable methods.
36
+ # This method is equivalent to +search_configurator.and(field, condition, value)+.
37
+ # See {SearchConfigurator#and} for parameters and examples.
38
+ # @return [SearchConfigurator]
39
+ # @api public
40
+ def where(field, condition, value = nil)
41
+ search_configurator.and(field, condition, value)
42
+ end
43
+
44
+ # Returns a new {JustRelate::Core::SearchConfigurator SearchConfigurator} set to the given
45
+ # negated filter (+field+, +condition+, +value+). Additionally, it is limited
46
+ # to this base type and can be further refined using chainable methods.
47
+ # This method is equivalent to +search_configurator.and_not(field, condition, value)+.
48
+ # See {SearchConfigurator#and_not} for parameters and examples.
49
+ # @return [SearchConfigurator]
50
+ # @api public
51
+ def where_not(field, condition, value = nil)
52
+ search_configurator.and_not(field, condition, value)
53
+ end
54
+
55
+ # Returns a new {JustRelate::Core::SearchConfigurator SearchConfigurator} set to the given
56
+ # +query+. Additionally, it is limited
57
+ # to this base type and can be further refined using chainable methods.
58
+ # This method is equivalent to +search_configurator.query(query)+.
59
+ # See {SearchConfigurator#query} for examples.
60
+ # @return [SearchConfigurator]
61
+ # @api public
62
+ def query(query)
63
+ search_configurator.query(query)
64
+ end
65
+
66
+ # Returns a new {JustRelate::Core::SearchConfigurator SearchConfigurator} limited
67
+ # to this base type. It can be further refined using chainable methods.
68
+ # @return [SearchConfigurator]
69
+ # @api public
70
+ def search_configurator
71
+ SearchConfigurator.new({
72
+ filters: filters_for_base_type,
73
+ })
74
+ end
75
+
76
+ private
77
+
78
+ def filters_for_base_type
79
+ [{
80
+ field: 'base_type',
81
+ condition: 'equals',
82
+ value: base_type,
83
+ }]
84
+ end
85
+ end
86
+ # @!parse extend ClassMethods
87
+ end
88
+ end; end; end
@@ -0,0 +1,6 @@
1
+ module JustRelate; module Core
2
+ # @api public
3
+ module Mixins
4
+ JustRelate.autoload_module(self, File.expand_path(__FILE__))
5
+ end
6
+ end; end