foundationapi 0.9.9
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +37 -0
- data/lib/foundation_api.rb +9 -0
- data/lib/foundation_api/errors.rb +34 -0
- data/lib/foundation_api/event.rb +6 -0
- data/lib/foundation_api/event/client.rb +105 -0
- data/lib/foundation_api/event/exceptions.rb +20 -0
- data/lib/foundation_api/json_rpc.rb +6 -0
- data/lib/foundation_api/json_rpc/client.rb +80 -0
- data/lib/foundation_api/json_rpc/exceptions.rb +47 -0
- data/lib/foundation_api/model.rb +7 -0
- data/lib/foundation_api/model/attribute_methods.rb +85 -0
- data/lib/foundation_api/model/cached.rb +13 -0
- data/lib/foundation_api/model/mapping.rb +24 -0
- data/lib/foundation_api/request.rb +29 -0
- data/lib/foundation_api/service.rb +56 -0
- data/lib/foundation_api/shoulda_matcher.rb +15 -0
- data/lib/foundation_api/shoulda_matcher/attribute_alias_matcher.rb +32 -0
- data/lib/foundation_api/shoulda_matcher/persistence_method_matcher.rb +91 -0
- data/lib/foundation_api/table.rb +6 -0
- data/lib/foundation_api/table/persistence.rb +115 -0
- data/lib/foundation_api/table/record.rb +143 -0
- data/lib/foundation_api/test_helper.rb +53 -0
- data/lib/foundation_api/version.rb +27 -0
- data/test/foundation_api_test.rb +31 -0
- data/test/test_helper.rb +31 -0
- data/test/unit/foundation_api/event/client_test.rb +129 -0
- data/test/unit/foundation_api/json_rpc/client_test.rb +143 -0
- data/test/unit/foundation_api/model/attribute_methods_test.rb +96 -0
- data/test/unit/foundation_api/model/cached_test.rb +20 -0
- data/test/unit/foundation_api/model/mapping_test.rb +22 -0
- data/test/unit/foundation_api/request_test.rb +33 -0
- data/test/unit/foundation_api/service_test.rb +54 -0
- data/test/unit/foundation_api/table/persistence_test.rb +182 -0
- data/test/unit/foundation_api/table/record_test.rb +176 -0
- metadata +209 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access.rb'
|
2
|
+
|
3
|
+
module FoundationApi
|
4
|
+
module Model
|
5
|
+
module Mapping
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def map_from_params(str)
|
10
|
+
HashWithIndifferentAccess[str.split('%%')[1..-1].collect do |p|
|
11
|
+
m = p.match /^([A-Za-z_]+)_\s(.*)$/
|
12
|
+
[m[1], m[2].strip]
|
13
|
+
end]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: change when on rails 4
|
18
|
+
# delegate :map_attribute, :reverse_map_attribute, to: :class
|
19
|
+
delegate :map_from_params, to: 'self.class'
|
20
|
+
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FoundationApi
|
2
|
+
module Request
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
delegate :request, :key_value_array, to: :'FoundationApi::JsonRPC::Client'
|
7
|
+
|
8
|
+
def generate_event(type, params = {})
|
9
|
+
FoundationApi::Event::Client.request :generate_event, {:type => type, :fields => params}
|
10
|
+
end
|
11
|
+
|
12
|
+
def quote(arg)
|
13
|
+
case arg
|
14
|
+
when String, Symbol, Date
|
15
|
+
"'#{arg}'"
|
16
|
+
when Array
|
17
|
+
arg.collect { |e| quote(e) }.join(', ')
|
18
|
+
else
|
19
|
+
arg.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# TODO: change when on rails 4
|
24
|
+
# delegate :request, :key_value_array, to: :class
|
25
|
+
# delegate :generate_event, to: :class
|
26
|
+
delegate :request, :key_value_array, to: 'self.class'
|
27
|
+
delegate :generate_event, to: 'self.class'
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FoundationApi
|
2
|
+
class Service
|
3
|
+
include Request
|
4
|
+
include Model::AttributeMethods
|
5
|
+
|
6
|
+
class_attribute :service_name
|
7
|
+
class_attribute :default_scope
|
8
|
+
self.default_scope = {}
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def where(options)
|
12
|
+
if options.is_a? Hash
|
13
|
+
options = {:conditions => options }
|
14
|
+
end
|
15
|
+
res = request_data(translate_options(options))
|
16
|
+
res && instantiate_objects(res)
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
res = request_data(default_scope)
|
21
|
+
res && instantiate_objects(res)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def request_data(options = {})
|
26
|
+
res = request service_name, options
|
27
|
+
end
|
28
|
+
|
29
|
+
def instantiate_objects(data)
|
30
|
+
data.collect { |attributes| new(attributes) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def translate_options(*args)
|
34
|
+
options = args.extract_options!
|
35
|
+
if filter = options.delete(:conditions)
|
36
|
+
filter = case filter
|
37
|
+
when Hash
|
38
|
+
Hash[filter.collect { |attr, value| [map_attribute(attr), value] }]
|
39
|
+
when String
|
40
|
+
[args.first.split(' ').collect { |word| map_attribute(word) }.join(' ')]
|
41
|
+
else
|
42
|
+
raise "Dirty filter. Cannot translate"
|
43
|
+
end
|
44
|
+
else
|
45
|
+
filter = [args.first.split(' ').collect { |word| map_attribute(word) }.join(' ')]
|
46
|
+
end
|
47
|
+
default_scope.merge(filter)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(attributes = {})
|
53
|
+
@attributes = HashWithIndifferentAccess.new attributes
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module FoundationApi
|
2
|
+
module ShouldaMatcher
|
3
|
+
autoload :AttributeAliasMatcher, 'foundation_api/shoulda_matcher/attribute_alias_matcher'
|
4
|
+
autoload :PersistenceMethodMatcher, 'foundation_api/shoulda_matcher/persistence_method_matcher'
|
5
|
+
|
6
|
+
def have_attribute_alias(alias_name)
|
7
|
+
FoundationApi::ShouldaMatcher::AttributeAliasMatcher.new(alias_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def have_persistence_method(method)
|
11
|
+
FoundationApi::ShouldaMatcher::PersistenceMethodMatcher.new(method)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module FoundationApi
|
2
|
+
module ShouldaMatcher
|
3
|
+
class AttributeAliasMatcher
|
4
|
+
def initialize(alias_name)
|
5
|
+
@expected_alias_name = alias_name.to_s
|
6
|
+
end
|
7
|
+
|
8
|
+
def matches?(subject)
|
9
|
+
@klass = subject.class
|
10
|
+
@klass.attribute_aliases.has_key?(@expected_alias_name) &&
|
11
|
+
(@source_attribute ? @klass.attribute_aliases[@expected_alias_name] == @source_attribute : true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message
|
15
|
+
"Expected attribute alias #{@expected_alias_name} for #{@source_attribute || 'unspecified source attribute'}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def negative_failure_message
|
19
|
+
"Didn't expect attribute alias #{@expected_alias_name} but got one anyway"
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
"have attribute alias #{@expected_alias_name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def for(source_attribute)
|
27
|
+
@source_attribute = source_attribute.to_s
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module FoundationApi
|
2
|
+
module ShouldaMatcher
|
3
|
+
class PersistenceMethodMatcher
|
4
|
+
def initialize(method)
|
5
|
+
@expected_method = method
|
6
|
+
@options = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(subject)
|
10
|
+
@klass = subject.class
|
11
|
+
method_supported? && interface_matches? && acceptable_options? && !option_failure?
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message
|
15
|
+
message = []
|
16
|
+
message << "You specified an illegal caller #{@expected_method.inspect}" unless method_supported?
|
17
|
+
message << "One of the options you specified #{@options.inspect} is not backed by the method #{@expected_method.inspect}" unless acceptable_options?
|
18
|
+
message << option_failure? if option_failure?
|
19
|
+
message.join(" and\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def negative_failure_message
|
23
|
+
"Something did not match. However should_not does not make sense here"
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
"specified persistence method #{@expected_method}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def calling(interface)
|
31
|
+
@interface = interface.to_s
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_uniqueness_of(attribute)
|
36
|
+
@options << :unique
|
37
|
+
@unique = attribute
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def with_destroy_key(key)
|
42
|
+
@options << :destroy_key
|
43
|
+
@destroy_key = key
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_destoy_type(type)
|
48
|
+
@options << :destroy_type
|
49
|
+
@destroy_type = type
|
50
|
+
self
|
51
|
+
end
|
52
|
+
private
|
53
|
+
ACCEPTABLE_OPTIONS = {
|
54
|
+
create: [:unique],
|
55
|
+
update: [],
|
56
|
+
destroy: [:destroy_key, :destroy_type]
|
57
|
+
}
|
58
|
+
|
59
|
+
def method_supported?
|
60
|
+
[:create, :update, :destroy].include? @expected_method
|
61
|
+
end
|
62
|
+
|
63
|
+
def acceptable_options?
|
64
|
+
@options.each do |option|
|
65
|
+
return false unless ACCEPTABLE_OPTIONS[@expected_method].include?(option)
|
66
|
+
end
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def interface_matches?
|
71
|
+
@klass.persistence_options[@expected_method].to_s == @interface
|
72
|
+
end
|
73
|
+
|
74
|
+
def option_failure?
|
75
|
+
case @expected_method
|
76
|
+
when :create
|
77
|
+
"Expected :unique option to be equal to #{@unique}, but was #{@klass.persistence_options[:unique].inspect}" unless @klass.persistence_options[:unique] == @unique
|
78
|
+
when :destroy
|
79
|
+
if @options.include? :destroy_key
|
80
|
+
"Expected :destroy_key to be equal to #{@destroy_key}, but was #{@klass.send(:destroy_key).inspect}" unless @klass.send(:destroy_key) == @destroy_key
|
81
|
+
end
|
82
|
+
if @options.include? :destroy_type
|
83
|
+
"Expected :destroy_type to be of #{@destroy_type}, but was #{@klass.persistence_options[:destroy_type].inspect}" unless @klass.persistence_options[:destroy_type] == @destroy_type
|
84
|
+
end
|
85
|
+
else
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
module FoundationApi
|
3
|
+
module Table
|
4
|
+
module Persistence
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :persistence_options
|
9
|
+
# TODO: change when on rails 4
|
10
|
+
# delegate :remote_create, to: :class
|
11
|
+
delegate :remote_create, :remote_destroy, to: 'self.class'
|
12
|
+
self.persistence_options = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# specify persistence support. You may specify all on one line if you like
|
17
|
+
# Example:
|
18
|
+
#
|
19
|
+
# persistence create: :CreatePossession, unique: :name
|
20
|
+
# persistence update: :UpdatePossession
|
21
|
+
# persistence destroy: :DestroyPossession, destroy_key: possessionId
|
22
|
+
#
|
23
|
+
def persistence(options = {})
|
24
|
+
options.assert_valid_keys(:create, :update, :destroy, :unique, :hash_to_array, :destroy_key, :destroy_type)
|
25
|
+
self.persistence_options = self.persistence_options.merge(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def remote_create(object)
|
29
|
+
unique_key = object.persistence_options[:unique]
|
30
|
+
if unique_key
|
31
|
+
existing_object = where(unique_key => object[unique_key], :deleted => 0).first
|
32
|
+
if existing_object
|
33
|
+
raise RecordNotUnique, "Record with key #{unique_key}: #{object[unique_key]} already exists"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
id = request persistence_options[:create], object.map_attributes
|
37
|
+
::Rails.logger.debug "FoundationApi::Table::Persistence.remote_create: request response: #{id.inspect}"
|
38
|
+
id.is_a?(Array) ? id.first : id
|
39
|
+
end
|
40
|
+
|
41
|
+
def remote_destroy(*ids)
|
42
|
+
if persistence_options[:destroy_type] == :array
|
43
|
+
request persistence_options[:destroy], ids
|
44
|
+
else
|
45
|
+
ids.flatten.each do |id|
|
46
|
+
request persistence_options[:destroy], {destroy_key => id }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create(attributes = {})
|
52
|
+
new(attributes).save
|
53
|
+
end
|
54
|
+
def create!(attributes = {})
|
55
|
+
new(attributes).save!
|
56
|
+
end
|
57
|
+
|
58
|
+
def destroy(*ids)
|
59
|
+
remote_destroy *ids
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def destroy_key
|
64
|
+
persistence_options[:destroy_key] || name.sub(/.*::([_a-zA-Z]+)$/, '\1').camelcase(:lower) + 'Id'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def save(options = {})
|
69
|
+
check_persistence_support rescue false
|
70
|
+
self[:id] ||= remote_create(self)
|
71
|
+
::Rails.logger.debug "FoundationApi::Table::Persistence.save: after save: #{self.inspect}"
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def save!(options = {})
|
76
|
+
check_persistence_support
|
77
|
+
self.save || raise( RecordNotSaved)
|
78
|
+
end
|
79
|
+
|
80
|
+
def persisted?
|
81
|
+
!!self[:id]
|
82
|
+
end
|
83
|
+
|
84
|
+
def new_record?
|
85
|
+
!persisted?
|
86
|
+
end
|
87
|
+
|
88
|
+
def map_attributes
|
89
|
+
if persistence_options[:hash_to_array]
|
90
|
+
Hash[attributes.collect { |key, value| [key, value.is_a?( Hash) ? value.to_a : value] } ]
|
91
|
+
else
|
92
|
+
attributes
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def destroy
|
97
|
+
raise DestroyNotSupported unless persistence_options[:destroy]
|
98
|
+
persisted? && remote_destroy(self.id)
|
99
|
+
end
|
100
|
+
|
101
|
+
def destroy!
|
102
|
+
raise RecordNotPersistent unless persisted?
|
103
|
+
destroy || raise(RecordNotDeleted)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def check_persistence_support
|
108
|
+
raise CreateNotSupported if new_record? && !persistence_options[:create]
|
109
|
+
raise UpdateNotSupported if !new_record? && !persistence_options[:update]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'active_model/conversion'
|
2
|
+
require 'active_model/naming'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
require 'foundation_api/errors'
|
5
|
+
module FoundationApi
|
6
|
+
module Table
|
7
|
+
class Record
|
8
|
+
class_attribute :database_name
|
9
|
+
class_attribute :table_name
|
10
|
+
class_attribute :caching_enabled
|
11
|
+
class_attribute :cache_expires
|
12
|
+
class_attribute :default_scope
|
13
|
+
class_attribute :columns
|
14
|
+
self.default_scope = {}
|
15
|
+
self.columns = []
|
16
|
+
|
17
|
+
include Request
|
18
|
+
include Model::AttributeMethods
|
19
|
+
include Model::Mapping
|
20
|
+
include Persistence
|
21
|
+
include ActiveModel::Conversion
|
22
|
+
extend ActiveModel::Naming
|
23
|
+
|
24
|
+
class << self
|
25
|
+
|
26
|
+
def find(*args)
|
27
|
+
with_optional_caching args.first do
|
28
|
+
res = get_table_data(fully_qualified_table_name, :filters => ["id = #{args.first}"])
|
29
|
+
res && res.size > 0 ? instantiate_objects(res).first : nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def where(options)
|
34
|
+
with_optional_caching :where, options do
|
35
|
+
if options.is_a? Hash
|
36
|
+
options = {:conditions => options }
|
37
|
+
end
|
38
|
+
res = get_table_data(fully_qualified_table_name, translate_options(options))
|
39
|
+
res && instantiate_objects(res)
|
40
|
+
end or []
|
41
|
+
end
|
42
|
+
|
43
|
+
def all
|
44
|
+
with_optional_caching :all do
|
45
|
+
res = get_table_data(fully_qualified_table_name)
|
46
|
+
res && instantiate_objects(res)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_optional_caching(*args)
|
51
|
+
raise ArgumentError, "You must specify block" unless block_given?
|
52
|
+
if caching_enabled
|
53
|
+
options = cache_expires ? {expires_in: cache_expires} : {}
|
54
|
+
Rails.cache.fetch([self.name] + args, options) do
|
55
|
+
yield
|
56
|
+
end
|
57
|
+
else
|
58
|
+
yield
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def get_table_data(table, options = {})
|
64
|
+
options[:columns] = columns if columns && !options.has_key?( :columns)
|
65
|
+
options[:columns] = translate_columns(options[:columns]) if options[:columns]
|
66
|
+
res = request "GetTableData", options.merge(:table => table)
|
67
|
+
if res.is_a? Hash
|
68
|
+
cols = res['columnNames']
|
69
|
+
res['rows'].collect do |r|
|
70
|
+
h = HashWithIndifferentAccess.new
|
71
|
+
cols.size.times do |i|
|
72
|
+
h[cols[i]] = r['fields'][i]
|
73
|
+
end
|
74
|
+
h
|
75
|
+
end
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def instantiate_objects(data)
|
82
|
+
data.collect { |attributes| new(:no_translate_attributes, attributes) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def translate_options(*args)
|
86
|
+
options = args.extract_options!
|
87
|
+
if filter = options.delete(:conditions)
|
88
|
+
filter = case filter
|
89
|
+
when Hash
|
90
|
+
filter.reverse_merge(default_scope).collect do |attr, value|
|
91
|
+
attr == :_expression ? translate_string(value) : "#{map_attribute(attr)} = #{quote(value)}"
|
92
|
+
end
|
93
|
+
when Array
|
94
|
+
filter
|
95
|
+
when String
|
96
|
+
[filter.split(' ').collect { |word| map_attribute(word) }.join(' ')]
|
97
|
+
else
|
98
|
+
raise TranslationError, "Dirty filter. Cannot translate"
|
99
|
+
end
|
100
|
+
else
|
101
|
+
filter = [translate_string(args.first)]
|
102
|
+
end
|
103
|
+
options.merge(:filters => filter)
|
104
|
+
end
|
105
|
+
|
106
|
+
def translate_columns(columns)
|
107
|
+
columns.collect { |word| word.to_s.split(/\s+/).collect{ |w| map_attribute(w) }.join(' ') }
|
108
|
+
end
|
109
|
+
|
110
|
+
def translate_string(str)
|
111
|
+
str.split(' ').collect { |word| map_attribute(word) }.join(' ')
|
112
|
+
end
|
113
|
+
|
114
|
+
def fully_qualified_table_name
|
115
|
+
@fully_qualified_table_name ||= begin
|
116
|
+
self.database_name ||= name.sub(/^([_a-zA-Z]+)::.+/, '\1').downcase
|
117
|
+
self.table_name ||= name.sub(/.*::([_a-zA-Z]+)$/, '\1')
|
118
|
+
"#{database_name}.#{table_name}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# TODO: change when on rails 4
|
124
|
+
# delegate :with_optional_caching, to: :class
|
125
|
+
delegate :with_optional_caching, to: 'self.class'
|
126
|
+
|
127
|
+
def initialize(*args)
|
128
|
+
attributes = args.extract_options!
|
129
|
+
if args.first == :no_translate_attributes
|
130
|
+
@attributes = HashWithIndifferentAccess.new attributes
|
131
|
+
else
|
132
|
+
@attributes = translate_attributes attributes
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def id
|
137
|
+
@id ||= self[:id].to_i
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|