foundationapi 0.9.9
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.
- 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
|
+
|