active_record-remote 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d72e68a69e314e8e66a694f12fe52dd5a46c16d1
4
+ data.tar.gz: d4c80560035c03ad31a25f7ff4fe730558bb364c
5
+ SHA512:
6
+ metadata.gz: 467fe7bd459140a835db5cbfa3309e24e684198cf82891f9ca6729c4a83d15ccde8acce9007aad57a724f328c9cbf15b2b38eaa8c295d52acdde45e8c90f84bd
7
+ data.tar.gz: 312a3f2a0ffb0771820d952bf3b5f275df9743c459c65f4a3d3b2904c726769cab55b1eb369b83e4ba6f9d2a545d3606376804727e9da98db06424a0d09b90cb
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_record-remote.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Justin Grubbs
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # ActiveRecord::Remote
2
+
3
+ This gem is a work in progress. The goal is to create a library that allows Ruby wrappers to be agnostic of the communication method. SOAP, XML, JSON, and Flat File all can be written in a similar format. To see a working example for a SOAP API [jGRUBBS/rlm_logistics](https://github.com/jGRUBBS/rlm_logistics).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'active_record-remote'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install active_record-remote
20
+
21
+ ## TODO
22
+ - [ ] implement for JSON
23
+ - [ ] implement for Flat File
24
+ - [ ] implement Rspec
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it ( https://github.com/[my-github-username]/active_record-remote/fork )
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'active_record/remote/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "active_record-remote"
8
+ spec.version = ActiveRecord::Remote::VERSION
9
+ spec.authors = ["Justin Grubbs"]
10
+ spec.email = ["justin@jgrubbs.net"]
11
+ spec.summary = %q{Active Record pattern for remote APIs}
12
+ spec.description = %q{Active Record pattern for remote APIs}
13
+ spec.homepage = "https://github.com/jGRUBBS/active_record-remote"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "virtus", "~> 1.0.5"
22
+ spec.add_dependency "activesupport", "~> 4.2.5"
23
+ spec.add_dependency "builder", "~> 3.2.2"
24
+ spec.add_dependency "activemodel", "~> 4.2.5"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.7"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ end
@@ -0,0 +1,9 @@
1
+ require 'virtus'
2
+ require 'active_model'
3
+ require 'active_support/core_ext/hash/conversions'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_record/remote/core_ext/hash'
6
+ require 'active_record/remote/version'
7
+ require 'active_record/remote/base'
8
+ require 'active_record/remote/client'
9
+ require 'active_record/remote/response'
@@ -0,0 +1,112 @@
1
+ require 'active_record/remote/helpers/association_helper'
2
+ require 'active_record/remote/helpers/request_helper'
3
+ require 'active_record/remote/helpers/serialization_helper'
4
+ require 'active_record/remote/helpers/xml_helper'
5
+ require 'active_record/remote/helpers/soap_helper'
6
+
7
+ module ActiveRecord
8
+ module Remote
9
+ class Base
10
+
11
+ class_attribute :action_path, :api_type, :operation_path, :base_element_name
12
+
13
+ include Virtus.model
14
+ include ActiveModel::Validations
15
+ extend ActiveRecord::Remote::Helpers::AssociationHelper
16
+ extend ActiveRecord::Remote::Helpers::RequestHelper
17
+ include ActiveRecord::Remote::Helpers::SerializationHelper
18
+ include ActiveRecord::Remote::Helpers::XMLHelper
19
+ include ActiveRecord::Remote::Helpers::SOAPHelper
20
+
21
+ attr_accessor :response, :raw_data, :parsed_data
22
+
23
+ def self.api_type(type)
24
+ self.api_type = type
25
+ end
26
+
27
+ def initialize(options = {})
28
+ super(options.merge(custom_options))
29
+ end
30
+
31
+ def custom_options
32
+ # overwrite in subclass to provide custom options to initalizer
33
+ end
34
+
35
+ def save
36
+ @response = handle_response(request)
37
+ valid?
38
+ end
39
+
40
+ def self.where(attrs)
41
+ instance = new(attrs)
42
+ instance.response = instance.handle_response(instance.request)
43
+ instance.parse_records
44
+ end
45
+
46
+ def self.all
47
+ where({})
48
+ end
49
+
50
+ def request
51
+ request_body = send("as_#{api_type}")
52
+ client.api_type = api_type
53
+ client.request(request_body)
54
+ end
55
+
56
+ def update_attributes(attrs)
57
+ attrs.each { |k, v| send("#{k}=", v) }
58
+ end
59
+
60
+ def valid?
61
+ if response.present?
62
+ # all model validations may pass, but response
63
+ # may have contained an error message
64
+ response.success?
65
+ else
66
+ # use model validations
67
+ super
68
+ end
69
+ end
70
+
71
+ def base_module
72
+ self.class.to_s.split('::').first.constantize
73
+ end
74
+
75
+ def full_module
76
+ self.class.to_s.split('::')[0..-2].join('::').constantize
77
+ end
78
+
79
+ def record_class
80
+ self.class.to_s.split('::').last
81
+ end
82
+
83
+ def parse_records
84
+ data = response.data.is_a?(Array) ? response.data : [response.data]
85
+ return [] if data.compact.empty?
86
+ data.flat_map do |data_item|
87
+ instance = self.class.new
88
+ read_attributes = "#{record_class}ReadAttributes"
89
+ if full_module.const_defined?(read_attributes)
90
+ instance.extend(read_attributes.constantize)
91
+ end
92
+ attrs = data_item.transform_keys! {|k| k.downcase.to_sym }
93
+ instance.update_attributes(attrs)
94
+ instance
95
+ end
96
+ end
97
+
98
+ def handle_response(response)
99
+ base_module.const_get("Response").new(
100
+ operation: self.class.operation_path,
101
+ raw_response: response,
102
+ instance: self
103
+ )
104
+ end
105
+
106
+ def client
107
+ base_module.const_get("Client").new(self.class.action_path)
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,70 @@
1
+ require "net/http"
2
+ require "net/https"
3
+
4
+ module ActiveRecord
5
+ module Remote
6
+ class Client
7
+
8
+ cattr_accessor :content_type, :host, :read_timeout, :api_type, :secure
9
+ attr_accessor :action
10
+
11
+ def initialize(action)
12
+ @action = action
13
+ end
14
+
15
+ def self.content_type(content_type)
16
+ self.content_type = content_type
17
+ end
18
+
19
+ def self.host(host)
20
+ self.host = host
21
+ end
22
+
23
+ def self.read_timeout(read_timeout)
24
+ self.read_timeout = read_timeout
25
+ end
26
+
27
+ def self.secure(secure)
28
+ self.secure = secure
29
+ end
30
+
31
+ def http
32
+ @http ||= Net::HTTP.new(host)
33
+ end
34
+
35
+ def formatted_action
36
+ action
37
+ end
38
+
39
+ def formatted_path
40
+ "/#{endpoint_path}/#{formatted_action}"
41
+ end
42
+
43
+ def complete_request_url
44
+ "#{http_protocol}://#{host}#{formatted_path}"
45
+ end
46
+
47
+ def http_protocol
48
+ secure ? "https" : "http"
49
+ end
50
+
51
+ def endpoint_path
52
+ # define in subclass
53
+ end
54
+
55
+ def request(request_body)
56
+ request = Net::HTTP::Post.new(formatted_path)
57
+ request.body = request_body
58
+ request.content_type = content_type
59
+ if secure
60
+ http.use_ssl = true
61
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
62
+ end
63
+ request.add_field("SOAPAction", "") if api_type == :soap
64
+ http.read_timeout = read_timeout if read_timeout
65
+ http.request(request)
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ class Hash
2
+
3
+ def to_soap(options = {})
4
+ require 'active_support/builder' unless defined?(Builder)
5
+
6
+ options = options.dup
7
+ options[:indent] ||= 2
8
+ options[:root] ||= 'hash'
9
+ options[:soap_builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
10
+
11
+ soap_builder = options[:soap_builder]
12
+
13
+ soap_builder.Envelope xmlns: "http://schemas.xmlsoap.org/soap/envelope/" do
14
+
15
+ soap_builder.Body do
16
+
17
+ soap_builder.tag!(options[:operation], xmlns: options[:namespace]) do
18
+ soap_builder.tag!(options[:base_element], "\n#{build_internal_xml(options)}")
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+
26
+ def build_internal_xml(options = {})
27
+ options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
28
+ builder = options[:builder]
29
+
30
+ root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
31
+
32
+ builder.tag!(root) do
33
+ each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
34
+ yield builder if block_given?
35
+ end
36
+
37
+ builder.target!.gsub(" ", "")
38
+ end
39
+
40
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveRecord::Remote
2
+ module Helpers
3
+ module AssociationHelper
4
+
5
+ def has_many(association, options = {})
6
+ association_name = parse_association_name(association, options)
7
+ set_inflection(association, options)
8
+ self.attribute association, Array[association_klass(association_name)], options
9
+ end
10
+
11
+ def parse_association_name(association, options = {})
12
+ if options[:collection].present?
13
+ options[:collection]
14
+ else
15
+ association
16
+ end
17
+ end
18
+
19
+ def set_inflection(association, options)
20
+ return if options[:collection].nil?
21
+ # since RLM has an irregular API we have to adjust the inflections
22
+ # so we can have children collections that do not match the parents
23
+ # i.e.
24
+ # <DETAILS>
25
+ # <LINE>
26
+ # instead of default behavior
27
+ # <DETAILS>
28
+ # <DETAIL>
29
+ #
30
+ # the code below dynamically adjusts active_support inflections
31
+ # here is a basic example
32
+ #
33
+ # ActiveSupport::Inflector.inflections do |inflect|
34
+ # inflect.singular 'DETAILS', 'LINE'
35
+ # end
36
+ if options[:strict]
37
+ assoc_name = association.to_s
38
+ collection = options[:collection].to_s
39
+ else
40
+ assoc_name = association.to_s.upcase
41
+ collection = options[:collection].to_s.upcase
42
+ end
43
+ ActiveSupport::Inflector.inflections do |inflect|
44
+ inflect.singular assoc_name, collection
45
+ end
46
+ end
47
+
48
+ def association_klass(name)
49
+ singular = name.to_s.singularize
50
+ parent_module = to_s.split('::')[0..-2].join('::').constantize
51
+ parent_module.const_get(singular.classify)
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord::Remote
2
+ module Helpers
3
+ module RequestHelper
4
+
5
+ def action(kind)
6
+ self.action_path = kind
7
+ end
8
+
9
+ def operation(kind)
10
+ self.operation_path = kind
11
+ end
12
+
13
+ def base_element(name)
14
+ self.base_element_name = name
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ module ActiveRecord::Remote
2
+ module Helpers
3
+ module SerializationHelper
4
+
5
+ def serializable_hash
6
+ Hash.new.tap do |attribute_hash|
7
+ attribute_set.each do |attribute|
8
+ serialize_attribute(attribute_hash, attribute)
9
+ end
10
+ end
11
+ end
12
+
13
+ def serialize_attribute(attribute_hash, attribute)
14
+ return if attributes[attribute.name].nil?
15
+ name = _attribute_name(attribute)
16
+ attribute_hash[name] = _serialize(attributes[attribute.name], attribute)
17
+ end
18
+
19
+ def _attribute_name(attribute)
20
+ if !!attribute.options[:as]
21
+ attribute.options[:as]
22
+ elsif !!attribute.options[:strict]
23
+ attribute.name
24
+ else
25
+ attribute.name.upcase
26
+ end
27
+ end
28
+
29
+ def _serialize(serialized, attribute = nil)
30
+ if serialized.respond_to?(:serializable_hash)
31
+ serialized.serializable_hash
32
+ else
33
+ case serialized
34
+ when Array
35
+ serialized.map { |attr| _serialize(attr) }
36
+ when BigDecimal
37
+ serialized.to_s("F")
38
+ when Hash
39
+ Hash[
40
+ serialized.map do |k, v|
41
+ k = attribute.options[:strict] ? k : k.upcase
42
+ [k, v]
43
+ end
44
+ ]
45
+ else
46
+ serialized
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord::Remote
2
+ module Helpers
3
+ module SOAPHelper
4
+
5
+ def as_soap
6
+ serializable_hash.to_soap(soap_options)
7
+ end
8
+
9
+ def soap_options
10
+ {
11
+ dasherize: false,
12
+ skip_types: true,
13
+ omit_nils: true
14
+ }
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord::Remote
2
+ module Helpers
3
+ module XMLHelper
4
+
5
+ def as_xml
6
+ serializable_hash.to_xml(xml_options)
7
+ end
8
+
9
+ def xml_options
10
+ {
11
+ root: "ITEM_FILTER",
12
+ dasherize: false,
13
+ skip_types: true
14
+ }
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveRecord
2
+ module Remote
3
+ class Response
4
+
5
+ attr_accessor :parsed_response, :options
6
+
7
+ alias_method :parsed, :parsed_response
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ handle_response(options[:raw_response])
12
+ end
13
+
14
+ def operation
15
+ options[:operation]
16
+ end
17
+
18
+ def record_instance
19
+ options[:instance]
20
+ end
21
+
22
+ def success?
23
+ record_instance.errors.blank?
24
+ end
25
+
26
+ def handle_response(response)
27
+ # implement in subclass
28
+ end
29
+
30
+ def response_message
31
+ # implement in subclass
32
+ end
33
+
34
+ def valid?
35
+ # implement in subclass
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Remote
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record-remote
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Justin Grubbs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 4.2.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 4.2.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: builder
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 3.2.2
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 3.2.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: activemodel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.5
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 4.2.5
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ description: Active Record pattern for remote APIs
98
+ email:
99
+ - justin@jgrubbs.net
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .gitignore
105
+ - Gemfile
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - active_record-remote.gemspec
110
+ - lib/active_record/remote.rb
111
+ - lib/active_record/remote/base.rb
112
+ - lib/active_record/remote/client.rb
113
+ - lib/active_record/remote/core_ext/hash.rb
114
+ - lib/active_record/remote/helpers/association_helper.rb
115
+ - lib/active_record/remote/helpers/request_helper.rb
116
+ - lib/active_record/remote/helpers/serialization_helper.rb
117
+ - lib/active_record/remote/helpers/soap_helper.rb
118
+ - lib/active_record/remote/helpers/xml_helper.rb
119
+ - lib/active_record/remote/response.rb
120
+ - lib/active_record/remote/version.rb
121
+ homepage: https://github.com/jGRUBBS/active_record-remote
122
+ licenses:
123
+ - MIT
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.2.2
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Active Record pattern for remote APIs
145
+ test_files: []