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