object_json_mapper 0.0.1

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.
@@ -0,0 +1,163 @@
1
+ module ObjectJSONMapper
2
+ class Base
3
+ include ActiveModel::Validations
4
+ include ActiveModel::Validations::Callbacks
5
+
6
+ include Local
7
+ include Conversion
8
+ include Serialization
9
+ include Errors
10
+ include Associations
11
+ include Persistence
12
+
13
+ extend ActiveModel::Callbacks
14
+
15
+ attr_accessor :persisted, :attributes
16
+
17
+ define_model_callbacks :save, :create, :update, :destroy, :validation
18
+
19
+ delegate :slice, :[], :[]=, to: :attributes
20
+
21
+ def initialize(attributes = {})
22
+ self.attributes = attributes
23
+ @persisted = false
24
+ end
25
+
26
+ alias persisted? persisted
27
+
28
+ def new_record?
29
+ !persisted?
30
+ end
31
+
32
+ def persist
33
+ @persisted = true
34
+ end
35
+
36
+ def reloadable?
37
+ to_key.any?
38
+ end
39
+
40
+ # @param value [Hash]
41
+ def attributes=(value)
42
+ @attributes = HashWithIndifferentAccess.new(value)
43
+
44
+ @attributes.each do |method_name, _|
45
+ define_singleton_method(method_name) do
46
+ @attributes[method_name]
47
+ end
48
+ end
49
+ end
50
+
51
+ def ==(other)
52
+ attributes == other.attributes && persisted == other.persisted
53
+ end
54
+
55
+ def client
56
+ self.class.client[id]
57
+ end
58
+
59
+ class << self
60
+ attr_accessor :associations, :relation, :root_url
61
+
62
+ def inherited(base)
63
+ base.root_url = base.name.underscore.pluralize
64
+ base.associations = Associations::Registry.new
65
+ base.relation = Relation.new(klass: base)
66
+ end
67
+
68
+ def client
69
+ RestClient::Resource.new(
70
+ URI.join(ObjectJSONMapper.base_url, root_url).to_s,
71
+ headers: ObjectJSONMapper.headers
72
+ )
73
+ end
74
+
75
+ def configure
76
+ yield self
77
+ end
78
+
79
+ def name=(value)
80
+ @name = value.to_s
81
+ end
82
+
83
+ def name
84
+ @name || super
85
+ end
86
+
87
+ # @param name [Symbol]
88
+ # @param type [Dry::Types::Constructor]
89
+ # @param default [Proc]
90
+ def attribute(name, type: nil, default: nil)
91
+ define_method(name) do
92
+ return default.call if attributes.exclude?(name) && default
93
+ return type.call(attributes[name]) if type
94
+
95
+ attributes[name]
96
+ end
97
+
98
+ define_method("#{name}=") do |value|
99
+ attributes[name] = value
100
+ end
101
+ end
102
+
103
+ # @param name [Symbol]
104
+ # @param block [Proc]
105
+ def scope(name, block)
106
+ define_singleton_method(name) do
107
+ relation.deep_clone.instance_exec(&block)
108
+ end
109
+
110
+ relation.define_singleton_method(name) do
111
+ instance_exec(&block)
112
+ end
113
+ end
114
+
115
+ def root(value)
116
+ clone.tap do |base|
117
+ base.name = name
118
+ base.root_url = value.to_s
119
+ end
120
+ end
121
+
122
+ # Same as `new` but for persisted records
123
+ # @param attributes [Hash]
124
+ # @return [ObjectJSONMapper::Base]
125
+ def persist(attributes = {})
126
+ new(attributes).tap do |base|
127
+ base.persisted = true
128
+ end
129
+ end
130
+
131
+ # @param conditions [Hash]
132
+ # @return [ObjectJSONMapper::Relation<ObjectJSONMapper::Base>] collection of model instances
133
+ def where(conditions = {})
134
+ relation.tap { |relation| relation.klass = self }.where(conditions)
135
+ end
136
+ alias all where
137
+
138
+ # @param id [Integer]
139
+ # @return [ObjectJSONMapper::Base] current model instance
140
+ def find(id)
141
+ raise ActiveRecord::RecordNotFound if id.nil?
142
+
143
+ result = HTTP.parse_json(client[id].get.body)
144
+
145
+ persist(result)
146
+ rescue RestClient::ExceptionWithResponse
147
+ raise ActiveRecord::RecordNotFound
148
+ end
149
+
150
+ # rubocop:disable Rails/FindBy
151
+ #
152
+ # @param conditions [Hash]
153
+ # @return [ObjectJSONMapper::Base] current model instance
154
+ def find_by(conditions = {})
155
+ where(conditions).first
156
+ end
157
+
158
+ def none
159
+ NullRelation.new(klass: self)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,24 @@
1
+ module ObjectJSONMapper
2
+ module Conversion
3
+ def to_model
4
+ self
5
+ end
6
+
7
+ # @return [Array] all key attributes
8
+ def to_key
9
+ [].tap do |a|
10
+ a << id if respond_to?(:id)
11
+ end
12
+ end
13
+
14
+ # @return [String] object's key suitable for use in URLs
15
+ def to_param
16
+ id.to_s
17
+ end
18
+
19
+ def to_partial_path
20
+ name = self.class.name.underscore
21
+ [name.pluralize, name].join('/')
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module ObjectJSONMapper
2
+ module Errors
3
+ # @param messages [Hash]
4
+ # @example
5
+ # load_errors(
6
+ # {
7
+ # "email": ["blank"]
8
+ # }
9
+ # )
10
+ def load_errors(messages)
11
+ errors.clear
12
+ messages.each do |key, values|
13
+ values.each do |value|
14
+ case value
15
+ when String
16
+ errors.add(key, value)
17
+ when Hash
18
+ errors.add(key, value[:error].to_sym, value.except(:error))
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ # In comparison with `ActiveModel::Validations#valid?`
25
+ # we should not clear errors because it would clear remote errors as well,
26
+ # but we can remove duplicates in the error messages
27
+ # @return [TrueClass,FalseClass]
28
+ def valid?(context = nil)
29
+ current_context, self.validation_context = validation_context, context
30
+ run_validations!
31
+ ensure
32
+ errors.messages.each { |_, v| v.uniq! }
33
+ self.validation_context = current_context
34
+ end
35
+ alias validate valid?
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ module Kaminari
2
+ module ObjectJSONMapper
3
+ module ObjectJSONMapperExtension
4
+ include Kaminari::ConfigurationMethods
5
+
6
+ def self.included(base)
7
+ base.define_singleton_method(Kaminari.config.page_method_name) do |num|
8
+ where(page: num, per_page: Kaminari.config.default_per_page)
9
+ end
10
+ end
11
+ end
12
+
13
+ module ObjectJSONMapperCriteriaMethods
14
+ def limit_value
15
+ collection unless @limit_value
16
+ @limit_value
17
+ end
18
+
19
+ def total_count
20
+ collection unless @total_count
21
+ @total_count
22
+ end
23
+
24
+ def total_pages
25
+ return 1 if limit_value.zero?
26
+ (total_count.to_f / limit_value).ceil
27
+ end
28
+
29
+ def offset_value
30
+ limit_value * current_page
31
+ end
32
+
33
+ def current_page
34
+ page = conditions[:page].to_i
35
+ page > 0 ? page : 1
36
+ end
37
+
38
+ def max_pages
39
+ total_count / offset_value
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ ObjectJSONMapper::Base.send(:include, Kaminari::ObjectJSONMapper::ObjectJSONMapperExtension)
46
+ ObjectJSONMapper::Relation.send(:include, Kaminari::ObjectJSONMapper::ObjectJSONMapperExtension)
47
+ ObjectJSONMapper::Relation.send(:include, Kaminari::PageScopeMethods)
48
+ ObjectJSONMapper::Relation.send(:include, Kaminari::ObjectJSONMapper::ObjectJSONMapperCriteriaMethods)
@@ -0,0 +1,9 @@
1
+ module ObjectJSONMapper
2
+ module HTTP
3
+ def self.parse_json(json)
4
+ JSON.parse(json, symbolize_names: true)
5
+ rescue JSON::ParserError
6
+ {}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,58 @@
1
+ module ObjectJSONMapper
2
+ module Local
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ def local
8
+ @local ||= self.class.local.find_or_initialize_by(id: id)
9
+ # sometimes local fetched before remote was saved, thus there is no id
10
+ @local.id ||= id
11
+ @local
12
+ end
13
+
14
+ def find_by_local(source, &scope)
15
+ self.class.find_by_local(source, &scope)
16
+ end
17
+
18
+ module ClassMethods
19
+ def local
20
+ return @local if @local
21
+ @local = Class.new(ActiveRecord::Base)
22
+ @local.table_name = name.underscore.pluralize
23
+ @local
24
+ end
25
+
26
+ # Allows you to apply filters from local model to remote data.
27
+ #
28
+ # @param source [ObjectJSONMapper::Relation]
29
+ # @param scope [Proc] scope to execute on local results
30
+ # @return [ObjectJSONMapper:Relation]
31
+ #
32
+ # @example
33
+ # class User < ObjectJSONMapper::Base
34
+ # def self.local
35
+ # LocalUser
36
+ # end
37
+ # end
38
+ #
39
+ # class LocalUser < ActiveRecord::Base
40
+ # end
41
+ #
42
+ # User.find_by_local(User.all) do
43
+ # where(local_column: 'value')
44
+ # end
45
+ # # => SELECT * FROM local_users WHERE local_column = 'value'
46
+ # # => GET http://example.com/users?id_in=1,2,3
47
+ def find_by_local(source, &scope)
48
+ source.where(
49
+ id_in: source.klass
50
+ .local
51
+ .all
52
+ .instance_exec(&scope)
53
+ .pluck(:id)
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,7 @@
1
+ module ObjectJSONMapper
2
+ class NullRelation < Relation
3
+ def collection
4
+ []
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,94 @@
1
+ module ObjectJSONMapper
2
+ module Persistence
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ # @return [ObjectJSONMapper::Base,FalseClass]
8
+ def save(*)
9
+ return update(attributes) if persisted?
10
+
11
+ response = self.class.client.post(attributes)
12
+
13
+ result = if response.headers[:location]
14
+ RestClient.get(response.headers[:location], ObjectJSONMapper.headers)
15
+ else
16
+ response.body
17
+ end
18
+
19
+ persist
20
+ errors.clear
21
+ attributes.merge!(HTTP.parse_json(result))
22
+
23
+ self
24
+ rescue RestClient::ExceptionWithResponse => e
25
+ raise e unless e.response.code == 422
26
+
27
+ load_errors(HTTP.parse_json(e.response.body))
28
+
29
+ false
30
+ ensure
31
+ validate
32
+ end
33
+ alias save! save
34
+
35
+ # @param params [Hash]
36
+ # @return [ObjectJSONMapper::Base,FalseClass]
37
+ def update(params = {})
38
+ return false if new_record?
39
+
40
+ client.patch(params)
41
+
42
+ reload
43
+ errors.clear
44
+
45
+ self
46
+ rescue RestClient::ExceptionWithResponse => e
47
+ raise e unless e.response.code == 422
48
+
49
+ load_errors(HTTP.parse_json(e.response.body))
50
+
51
+ false
52
+ end
53
+ alias update_attributes update
54
+
55
+ # @result [TrueClass,FalseClass]
56
+ def destroy
57
+ client.delete
58
+
59
+ true
60
+ rescue RestClient::ExceptionWithResponse
61
+ false
62
+ end
63
+ alias delete destroy
64
+
65
+ # @return [ObjectJSONMapper::Base]
66
+ def reload
67
+ tap do |base|
68
+ base.attributes = HTTP.parse_json(client.get.body) if reloadable?
69
+ end
70
+ end
71
+
72
+ module ClassMethods
73
+ # @param params [Hash]
74
+ # @return [ObjectJSONMapper::Base] current model instance
75
+ def create(params = {})
76
+ response = client.post(params)
77
+
78
+ result = if response.headers[:location]
79
+ RestClient.get(response.headers[:location], ObjectJSONMapper.headers)
80
+ else
81
+ response.body
82
+ end
83
+
84
+ persist(HTTP.parse_json(result))
85
+ rescue RestClient::ExceptionWithResponse => e
86
+ raise e unless e.response.code == 422
87
+
88
+ new.tap do |base|
89
+ base.load_errors(HTTP.parse_json(e.response.body))
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,100 @@
1
+ module ObjectJSONMapper
2
+ class Relation
3
+ include Enumerable
4
+
5
+ attr_accessor :collection, :conditions, :klass, :path
6
+
7
+ delegate :each,
8
+ :map,
9
+ :first,
10
+ :last,
11
+ :empty?,
12
+ :select,
13
+ :select!,
14
+ :to_ary,
15
+ :+,
16
+ :inspect,
17
+ :size,
18
+ to: :collection
19
+
20
+ def initialize(options = {})
21
+ @klass ||= options[:klass]
22
+ @path ||= options[:path]
23
+ @collection ||= options.fetch(:collection, [])
24
+ @conditions ||= options.fetch(:conditions, {})
25
+ end
26
+
27
+ def find_by(conditions = {})
28
+ collection.find do |record|
29
+ conditions.all? { |k, v| record.public_send(k) == v }
30
+ end
31
+ end
32
+
33
+ def find(id)
34
+ find_by(id: id.to_i)
35
+ end
36
+
37
+ def exists?(id)
38
+ find(id).present?
39
+ end
40
+
41
+ def where(conditions = {})
42
+ deep_clone.tap { |relation| relation.conditions.merge!(conditions) }
43
+ end
44
+
45
+ # @return [Array,Array<Array>]
46
+ def pluck(*attributes)
47
+ map { |record| record.slice(*attributes).values }
48
+ .tap { |result| result.flatten! if attributes.size == 1 }
49
+ end
50
+
51
+ # TODO: refactor
52
+ def collection
53
+ return @collection if @collection.any? && conditions.empty?
54
+
55
+ response = RestClient.get(path,
56
+ (ObjectJSONMapper.headers || {}).merge(params: prepare_params(conditions)))
57
+
58
+ @total_count = response.headers[:total].to_i
59
+ @limit_value = response.headers[:per_page].to_i
60
+
61
+ @collection = HTTP.parse_json(response.body)
62
+ .map { |attributes| klass.persist(attributes) }
63
+ end
64
+
65
+ def deep_clone
66
+ clone.tap do |object|
67
+ object.conditions = conditions.clone
68
+ object.collection = @collection.clone
69
+ end
70
+ end
71
+
72
+ def none
73
+ NullRelation.new(klass: klass, conditions: conditions)
74
+ end
75
+
76
+ # Find and return relation of local records by `id`
77
+ # @return [ActiveRecord::Relation]
78
+ def locals
79
+ return [] if collection.empty?
80
+ klass.local.where(id: collection.map(&:id))
81
+ end
82
+
83
+ private
84
+
85
+ def prepare_params(conditions)
86
+ conditions.deep_merge(conditions) do |_, _, value|
87
+ case value
88
+ when Array
89
+ value.join(',')
90
+ else
91
+ value
92
+ end
93
+ end
94
+ end
95
+
96
+ def path
97
+ @path || klass.client.url
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,7 @@
1
+ module ObjectJSONMapper
2
+ module Serialization
3
+ def serializable_hash
4
+ attributes
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module ObjectJSONMapper
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_model'
2
+ require 'rest-client'
3
+ require 'json'
4
+
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+ require 'active_support/core_ext/object/to_query'
7
+ require 'active_support/core_ext/enumerable'
8
+
9
+ require 'object_json_mapper/version'
10
+
11
+ require 'object_json_mapper/associations/association'
12
+ require 'object_json_mapper/associations/has_many'
13
+ require 'object_json_mapper/associations/has_one'
14
+ require 'object_json_mapper/associations/registry'
15
+ require 'object_json_mapper/associations'
16
+ require 'object_json_mapper/conversion'
17
+ require 'object_json_mapper/serialization'
18
+ require 'object_json_mapper/persistence'
19
+ require 'object_json_mapper/errors'
20
+ require 'object_json_mapper/http'
21
+ require 'object_json_mapper/local'
22
+
23
+ require 'object_json_mapper/relation'
24
+ require 'object_json_mapper/null_relation'
25
+ require 'object_json_mapper/base'
26
+
27
+ if defined?(Kaminari)
28
+ require 'object_json_mapper/extensions/kaminari'
29
+ end
30
+
31
+ module ObjectJSONMapper
32
+ mattr_accessor :base_url, :headers
33
+
34
+ def self.configure
35
+ yield self
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'object_json_mapper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'object_json_mapper'
8
+ spec.version = ObjectJSONMapper::VERSION
9
+ spec.authors = ['droptheplot']
10
+ spec.email = ['novikov359@gmail.com']
11
+
12
+ spec.summary = 'ActiveResource for UMS.'
13
+ spec.homepage = 'https://github.com/InspireNL/ObjectJSONMapper'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.12'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
+ spec.add_development_dependency 'awesome_print', '~> 1.7'
25
+ spec.add_development_dependency 'pry', '~> 0.10'
26
+ spec.add_development_dependency 'webmock', '~> 2.3'
27
+ spec.add_development_dependency 'pry-byebug', '~> 3.4'
28
+ spec.add_development_dependency 'kaminari', '~> 0.17'
29
+
30
+ spec.add_dependency 'activemodel', '>= 4.2'
31
+ spec.add_dependency 'activesupport', '>= 4.2'
32
+ spec.add_dependency 'rest-client', '~> 2.0'
33
+ end