foundationapi 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Rakefile +37 -0
  2. data/lib/foundation_api.rb +9 -0
  3. data/lib/foundation_api/errors.rb +34 -0
  4. data/lib/foundation_api/event.rb +6 -0
  5. data/lib/foundation_api/event/client.rb +105 -0
  6. data/lib/foundation_api/event/exceptions.rb +20 -0
  7. data/lib/foundation_api/json_rpc.rb +6 -0
  8. data/lib/foundation_api/json_rpc/client.rb +80 -0
  9. data/lib/foundation_api/json_rpc/exceptions.rb +47 -0
  10. data/lib/foundation_api/model.rb +7 -0
  11. data/lib/foundation_api/model/attribute_methods.rb +85 -0
  12. data/lib/foundation_api/model/cached.rb +13 -0
  13. data/lib/foundation_api/model/mapping.rb +24 -0
  14. data/lib/foundation_api/request.rb +29 -0
  15. data/lib/foundation_api/service.rb +56 -0
  16. data/lib/foundation_api/shoulda_matcher.rb +15 -0
  17. data/lib/foundation_api/shoulda_matcher/attribute_alias_matcher.rb +32 -0
  18. data/lib/foundation_api/shoulda_matcher/persistence_method_matcher.rb +91 -0
  19. data/lib/foundation_api/table.rb +6 -0
  20. data/lib/foundation_api/table/persistence.rb +115 -0
  21. data/lib/foundation_api/table/record.rb +143 -0
  22. data/lib/foundation_api/test_helper.rb +53 -0
  23. data/lib/foundation_api/version.rb +27 -0
  24. data/test/foundation_api_test.rb +31 -0
  25. data/test/test_helper.rb +31 -0
  26. data/test/unit/foundation_api/event/client_test.rb +129 -0
  27. data/test/unit/foundation_api/json_rpc/client_test.rb +143 -0
  28. data/test/unit/foundation_api/model/attribute_methods_test.rb +96 -0
  29. data/test/unit/foundation_api/model/cached_test.rb +20 -0
  30. data/test/unit/foundation_api/model/mapping_test.rb +22 -0
  31. data/test/unit/foundation_api/request_test.rb +33 -0
  32. data/test/unit/foundation_api/service_test.rb +54 -0
  33. data/test/unit/foundation_api/table/persistence_test.rb +182 -0
  34. data/test/unit/foundation_api/table/record_test.rb +176 -0
  35. metadata +209 -0
@@ -0,0 +1,13 @@
1
+ module FoundationApi
2
+ module Model
3
+ module Cached
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def cached_find(id)
8
+ Rails.cache.fetch([self.name, id]) { find(id) }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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,6 @@
1
+ module FoundationApi
2
+ module Table
3
+ autoload :Record, 'foundation_api/table/record'
4
+ autoload :Persistence, 'foundation_api/table/persistence'
5
+ end
6
+ 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
+