object_json_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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