seat-belt 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +29 -0
  3. data/.travis.yml +6 -0
  4. data/Changelog.md +55 -0
  5. data/Gemfile +15 -0
  6. data/Guardfile +13 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +705 -0
  9. data/Rakefile +1 -0
  10. data/ext/.gitkeep +0 -0
  11. data/lib/seatbelt.rb +37 -0
  12. data/lib/seatbelt/collections/collection.rb +56 -0
  13. data/lib/seatbelt/core.rb +15 -0
  14. data/lib/seatbelt/core/callee.rb +35 -0
  15. data/lib/seatbelt/core/eigenmethod.rb +150 -0
  16. data/lib/seatbelt/core/eigenmethod_proxy.rb +45 -0
  17. data/lib/seatbelt/core/ext/core_ext.rb +0 -0
  18. data/lib/seatbelt/core/gate.rb +198 -0
  19. data/lib/seatbelt/core/ghost_tunnel.rb +81 -0
  20. data/lib/seatbelt/core/implementation.rb +158 -0
  21. data/lib/seatbelt/core/interface.rb +51 -0
  22. data/lib/seatbelt/core/iterators/method_config.rb +50 -0
  23. data/lib/seatbelt/core/lookup_table.rb +101 -0
  24. data/lib/seatbelt/core/pool.rb +90 -0
  25. data/lib/seatbelt/core/property.rb +161 -0
  26. data/lib/seatbelt/core/proxy.rb +135 -0
  27. data/lib/seatbelt/core/synthesizeable.rb +50 -0
  28. data/lib/seatbelt/core/terminal.rb +59 -0
  29. data/lib/seatbelt/dependencies.rb +5 -0
  30. data/lib/seatbelt/document.rb +175 -0
  31. data/lib/seatbelt/errors.rb +1 -0
  32. data/lib/seatbelt/errors/errors.rb +150 -0
  33. data/lib/seatbelt/gate_config.rb +59 -0
  34. data/lib/seatbelt/ghost.rb +140 -0
  35. data/lib/seatbelt/models.rb +9 -0
  36. data/lib/seatbelt/seatbelt.rb +10 -0
  37. data/lib/seatbelt/synthesizer.rb +3 -0
  38. data/lib/seatbelt/synthesizers/document.rb +16 -0
  39. data/lib/seatbelt/synthesizers/mongoid.rb +16 -0
  40. data/lib/seatbelt/synthesizers/synthesizer.rb +146 -0
  41. data/lib/seatbelt/tape.rb +2 -0
  42. data/lib/seatbelt/tape_deck.rb +71 -0
  43. data/lib/seatbelt/tapes/tape.rb +105 -0
  44. data/lib/seatbelt/tapes/util/delegate.rb +56 -0
  45. data/lib/seatbelt/translator.rb +66 -0
  46. data/lib/seatbelt/version.rb +3 -0
  47. data/seatbelt.gemspec +27 -0
  48. data/spec/lib/seatbelt/core/eigenmethod_spec.rb +102 -0
  49. data/spec/lib/seatbelt/core/gate_spec.rb +521 -0
  50. data/spec/lib/seatbelt/core/ghost_tunnel_spec.rb +21 -0
  51. data/spec/lib/seatbelt/core/lookup_table_spec.rb +234 -0
  52. data/spec/lib/seatbelt/core/pool_spec.rb +270 -0
  53. data/spec/lib/seatbelt/core/proxy_spec.rb +108 -0
  54. data/spec/lib/seatbelt/core/terminal_spec.rb +184 -0
  55. data/spec/lib/seatbelt/document_spec.rb +287 -0
  56. data/spec/lib/seatbelt/gate_config_spec.rb +98 -0
  57. data/spec/lib/seatbelt/ghost_spec.rb +568 -0
  58. data/spec/lib/seatbelt/synthesizers/document_spec.rb +47 -0
  59. data/spec/lib/seatbelt/synthesizers/mongoid_spec.rb +134 -0
  60. data/spec/lib/seatbelt/synthesizers/synthesizer_spec.rb +112 -0
  61. data/spec/lib/seatbelt/tape_deck_spec.rb +180 -0
  62. data/spec/lib/seatbelt/tapes/tape_spec.rb +115 -0
  63. data/spec/lib/seatbelt/translator_spec.rb +108 -0
  64. data/spec/spec_helper.rb +16 -0
  65. data/spec/support/implementations/seatbelt_environment.rb +19 -0
  66. data/spec/support/shared_examples/shared_api_class.rb +7 -0
  67. data/spec/support/shared_examples/shared_collection_child.rb +7 -0
  68. data/spec/support/worlds/eigenmethod_world.rb +7 -0
  69. metadata +205 -0
@@ -0,0 +1,161 @@
1
+ module Seatbelt
2
+ module Property
3
+ module ClassMethods
4
+
5
+ # Public: Defines an API property. This is only working if its called within
6
+ # the #interface method block.
7
+ #
8
+ # Wraps Seatbelt::Pool::Api#api_method with a getter and setter method name.
9
+ #
10
+ # *args - An argument list containing:
11
+ # name - Name of the API property (required)
12
+ # options - An optional options Hash containing:
13
+ # :accessible - Property is mass assignable
14
+ # (defaults to false)
15
+ #
16
+ # Examples:
17
+ #
18
+ # class Book
19
+ # interface :instance do
20
+ # define_property :author
21
+ # define_property :title
22
+ # end
23
+ # end
24
+ def define_property(*args)
25
+ if @scope.eql?(:class)
26
+ raise Seatbelt::Errors::PropertyOnClassLevelDefinedError
27
+ end
28
+ hsh = {}
29
+ options = args.extract_options!
30
+ accessible = options.fetch(:accessible, false)
31
+ name = args.pop
32
+ hsh[:scope] = @scope
33
+
34
+ self.send(:api_method, name, hsh)
35
+ hsh[:args] = [:value]
36
+ self.send(:api_method, :"#{name}=", hsh)
37
+
38
+ property_list << name
39
+ self.send(:property_accessible, name) if accessible
40
+
41
+ end
42
+
43
+ # Public: Mass defining of properties.
44
+ #
45
+ # See #define_property for details
46
+ #
47
+ # - properties - A list of property names to define.
48
+ #
49
+ def define_properties(*properties)
50
+ properties.each { |property| define_property(property) }
51
+ end
52
+
53
+ # Public: All properties that are marked as accessible.
54
+ #
55
+ # Returns the property list or an empty Array.
56
+ def accessible_properties
57
+ @accessible_properties ||= []
58
+ end
59
+
60
+ # Public: Defines one or more properties to be mass assignable due
61
+ # the #properties= setter method.
62
+ #
63
+ # If the class implements an #attributes method (e.g. coming from
64
+ # Seatbelt::Document) these attributes are includeable in this list too.
65
+ #
66
+ # properties - A list of property names. Requires at least one property.
67
+ #
68
+ def property_accessible(*properties)
69
+ properties.each do |property|
70
+ attribute_defined = false
71
+ if self.respond_to?(:attributes)
72
+ if self.attribute_set.map(&:name).include?(property)
73
+ attribute_defined = true
74
+ end
75
+ end
76
+ unless attribute_defined
77
+ unless property_list.include?(property) && !attribute_defined
78
+ raise Seatbelt::Errors::PropertyNotDefinedYetError.new(property)
79
+ end
80
+ end
81
+ accessible_properties << property
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Internal: All properties of the class defined with #define_property.
88
+ #
89
+ # Returns the the properties as Array or an empty Array.
90
+ def property_list
91
+ @property_list ||= []
92
+ end
93
+
94
+ end
95
+
96
+ module InstanceMethods
97
+
98
+ require 'active_support/core_ext/hash/keys'
99
+
100
+ # Public: Gets a list of key-value pairs that includes the properties
101
+ # with its values.
102
+ #
103
+ # role - The scope of properties that will be selected
104
+ # Defaults to :accessibles which will only select properties that
105
+ # are defined as accessible.
106
+ #
107
+ # Returns an Hash containing the property names and its values.
108
+ def properties(role = :accessibles)
109
+ injector = lambda do |result, item|
110
+ result[item] = self.send(item)
111
+ result
112
+ end
113
+ list = case role
114
+ when :accessibles then
115
+ self.class.accessible_properties.inject({}, &injector)
116
+ when :all then
117
+ all = self.class.send(:property_list).inject({}, &injector)
118
+ all.merge!(attributes) if self.respond_to?(:attributes)
119
+ all
120
+ else
121
+ {}
122
+ end
123
+ list.with_indifferent_access
124
+ end
125
+
126
+ # Public: Sets a bunch of properties that are marked as accessible.
127
+ #
128
+ # property_hsh - The Hash containing the property name and the property
129
+ # value.
130
+ #
131
+ # Examples
132
+ #
133
+ # class Car
134
+ # include Seatbelt::Ghost
135
+ #
136
+ # interface :instance do
137
+ # define_property :wheels
138
+ # define_property :color
139
+ #
140
+ # property_accessible :color
141
+ # end
142
+ # end
143
+ #
144
+ # car = Car.new
145
+ # car.properties = { :color => "yellow", :wheels => 3 }
146
+ #
147
+ # car.color # => 'yellow' because it's accessible
148
+ # car.wheels # => nil because it's not accessible
149
+ #
150
+ def properties=(property_hsh)
151
+ property_hsh.each do |property_key, property_value|
152
+ if self.class.accessible_properties.include?(property_key) || \
153
+ self.class.accessible_properties.map(&:to_s).include?(property_key)
154
+ self.send("#{property_key}=", property_value)
155
+ end
156
+ end
157
+ end
158
+
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,135 @@
1
+ module Seatbelt
2
+
3
+ # Public: A Seatbelt::Proxy class is a shadow of any class that includes
4
+ # Seatbelt::Gate.
5
+ # It provides access to the API class or instance of the API class and
6
+ # implements the DynamicProxy pattern.
7
+ #
8
+ # A Proxy class provides a private dynamic accessor #klass that changes with
9
+ # the implementation scope of the API methods implementation.
10
+ #
11
+ # Let's show this by example:
12
+ #
13
+ # class ApiClass
14
+ # include Seatbelt::Ghost
15
+ #
16
+ # api_method :an_instance_method_to_implement
17
+ # api_method :a_class_method_to_implement
18
+ # def helper(a,b,c)
19
+ # #....
20
+ # end
21
+ #
22
+ # def self.c_helper(options={})
23
+ # # ...
24
+ # end
25
+ # end
26
+ #
27
+ # class ApiClassImplementation
28
+ #
29
+ # def instance_implementation
30
+ # local = proxy.call(:helper, 1,2,4) # proxy scope is instance of ApiClass
31
+ # end
32
+ # implement :instance_implementation,
33
+ # :as => "ApiClass#an_instance_method_to_implement"
34
+ #
35
+ # def class_implementation
36
+ # local = proxy.call(:c_helper, {:foo => 19})
37
+ # # proxy scope is ApiClass
38
+ # end
39
+ # implement :class_implementation,
40
+ # :as => "ApiClass.a_class_method_to_implement"
41
+ # end
42
+ class Proxy
43
+
44
+ NOT_ALLOWABLE_CALLS_ON_OBJECT = %w{ call object klass tunnel }
45
+
46
+ # Public: Send a method message to the current #klass scope receiver.
47
+ # See class documentation section for further informations about #klass.
48
+ #
49
+ # method_name - The method to call
50
+ # *args - The methods argument list
51
+ # &block - An optional block that should be passed to the callable
52
+ # method
53
+ #
54
+ # Returns the return value of the callable method or raises a
55
+ # NoMethodError.
56
+ def call(method_name, *args, &block)
57
+ object.send(method_name,*args,&block)
58
+ end
59
+
60
+ def object
61
+ self.send(:klass)
62
+ end
63
+
64
+ # Public: Calls a private API attribute or method of an object defined
65
+ # in a API class.
66
+ #
67
+ # It is only working on a second level.
68
+ #
69
+ # chain - The attribute call chain as String.
70
+ #
71
+ # Example:
72
+ #
73
+ # class ApiA
74
+ # include Seatbelt::Document
75
+ # include Seatbelt::Ghost
76
+ #
77
+ # has :b, "Models::B"
78
+ #
79
+ # end
80
+ #
81
+ # class Models::B
82
+ # include Seatbelt::Document
83
+ # include Seatbelt::Ghost
84
+ #
85
+ # # definitions
86
+ #
87
+ # end
88
+ #
89
+ # class ImplementationB
90
+ # field :name, :type => String
91
+ # end
92
+ #
93
+ # In a implementation method of ApiA
94
+ #
95
+ # proxy.tunnel("b.name")
96
+ #
97
+ #
98
+ # Returns the duplicated String.
99
+ def tunnel(chain)
100
+ proxy_associated_object,object_method = chain.split(".")
101
+ unless object.respond_to?(proxy_associated_object)
102
+ raise Seatbelt::Errors::ObjectDoesNotExistError
103
+ else
104
+ tunneled_object = object.send(proxy_associated_object)
105
+ callees = tunneled_object.eigenmethods.map do |eigenmethod|
106
+ eigenmethod.send(:callee)
107
+ end
108
+
109
+ callee = callees.uniq.first
110
+ unless callee.nil?
111
+ unless object_method
112
+ warn "You called a single object. Use #call instead."
113
+ object_method = proxy_associated_object
114
+ end
115
+ return callee.send(object_method)
116
+ else
117
+ raise Seatbelt::Errors::MethodNotImplementedError
118
+ end
119
+ end
120
+ end
121
+
122
+ # Public: Delegates a method message to the #object receiver if the
123
+ # message is not included in NOT_ALLOWABLE_CALLS_ON_OBJECT or the
124
+ # class responds to.
125
+ def method_missing(method_name, *args, &block)
126
+ unless method_name.to_s.in?(NOT_ALLOWABLE_CALLS_ON_OBJECT) &&
127
+ (not self.respond_to?(method_name))
128
+ self.call(method_name, *args, &block)
129
+ else
130
+ super
131
+ end
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,50 @@
1
+ module Seatbelt
2
+ module Synthesizeable
3
+ extend self
4
+
5
+ # Public: Combined getter and setter for a map of attributes that should
6
+ # be used for synthesizing the proxy and the implementation object.
7
+ #
8
+ # map=nil - a Hash of proxy attributes as keys and implementation object
9
+ # attributes as values.
10
+ #
11
+ # If map is nil, it returns the synthesize map (or an Empty hash if not
12
+ # previously set).
13
+ def synthesize_map(map=nil)
14
+ if map.nil?
15
+ return @synthesize_map ||={}
16
+ else
17
+ @synthesize_map=map
18
+ end
19
+ end
20
+
21
+ # Public: Defines a synthesizer for the implementation class. It takes an
22
+ # Hash argument to define the class the object should by synthesized and
23
+ # an additional adapter.
24
+ #
25
+ # options - A Hash containing synthesize configurations
26
+ # :from - The class the implementation and proxy object has to
27
+ # synthesized (String or Constant)
28
+ # :adapter- An optional class name that points to a implemented
29
+ # Synthesizer. Defaults to
30
+ # Seatbelt::Synthesizers::Document
31
+ #
32
+ def synthesize(options)
33
+ klass_to_synthesize = options[:from]
34
+ synthesize_adapter = options.fetch(:adapter,
35
+ "Seatbelt::Synthesizers::Document")
36
+ synthesize_adapter = Module.const_get(synthesize_adapter)
37
+ unless klass_to_synthesize.respond_to?(:name)
38
+ klass_to_synthesize = Module.const_get(klass_to_synthesize)
39
+ end
40
+ synthesize_obj = {:klass => klass_to_synthesize.name,
41
+ :adapter => synthesize_adapter}
42
+ klass_to_synthesize.class.class_eval do
43
+ def synthesizers
44
+ @synthesizers ||= []
45
+ end
46
+ end
47
+ klass_to_synthesize.synthesizers << synthesize_obj
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ module Seatbelt
2
+
3
+ # Public: Interface between API class and Implementation class.
4
+ # Calls the implementation of an API method with passed arguments and block.
5
+ #
6
+ class Terminal
7
+ # The implementation methods config store.
8
+ def self.luggage=(luggage_pack)
9
+ @luggage = luggage_pack
10
+ end
11
+
12
+ # Public: The implementation methods config store.
13
+ #
14
+ # Returns implementation methods config store.
15
+ def self.luggage
16
+ @luggage ||= []
17
+ end
18
+
19
+ def self.for_scope_and_namespace(scope, namespace)
20
+ Terminal.luggage.select do |package|
21
+ package.scope_level.eql?(scope) && package.namespace.eql?(namespace)
22
+ end
23
+ end
24
+
25
+
26
+ # Public: calls the implementation of an API method with passed arguments
27
+ # and block.
28
+ # Before sending the method message to the receiver, it defines the
29
+ # receivers proxy scope depending on klass (see below).
30
+ #
31
+ # action - The API method name to be called
32
+ # klass - The API class name on which the API method is declared
33
+ # arity - Number of required arguments
34
+ # *args - An argument list passed to the implementation method
35
+ # &block - An optional block passed to the implementation method.
36
+ #
37
+ # Returns the return value of the implementation method.
38
+ def self.call(action, klass, arity, *args, &block)
39
+ raise Seatbelt::Errors::MethodNotImplementedError if luggage.empty?
40
+ scope = klass.class.eql?(Class) ? :class : :instance
41
+ klass_namespace = scope.eql?(:class) ? klass.name : klass.class.name
42
+
43
+ eigenmethod = klass.eigenmethods.detect do |meth|
44
+ meth.implemented_as.eql?(action)
45
+ end
46
+
47
+ unless eigenmethod
48
+ raise Seatbelt::Errors::MethodNotImplementedError
49
+ end
50
+
51
+ if (not eigenmethod.delegated) && (not eigenmethod.arity.eql?(arity))
52
+ raise Seatbelt::Errors::ArgumentMissmatchError
53
+ end
54
+
55
+ eigenmethod.call(*args, &block)
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_support/core_ext/object/inclusion'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'active_support'
4
+ require 'virtus'
5
+ require 'active_model'
@@ -0,0 +1,175 @@
1
+ module Seatbelt
2
+
3
+ # Public: A Document is an interface to specify attributes for a API class.
4
+ # Accessing and setting attributes for an API class within an implementation
5
+ # class is possible during the proxy object.
6
+ #
7
+ # Example:
8
+ #
9
+ # class Airport
10
+ # include Seatbelt::Ghost
11
+ # include Seatbelt::Document
12
+ #
13
+ # attribute :name, String
14
+ # attribute :lat, Float
15
+ # attribute :lng, Float
16
+ #
17
+ # api_method :identifier
18
+ #
19
+ # end
20
+ #
21
+ # class ImplementationKlass
22
+ # include Seatbelt::Gate
23
+ #
24
+ # def aiport_identifier
25
+ # name = proxy.call(:name) # <= calls the Aiport attribute
26
+ # return "airport_#{name}".underscore
27
+ # end
28
+ # implement :airport_identifier, :as => "Airport#identifier"
29
+ # end
30
+ #
31
+ # aiport = Airport.new(:name => "London Stansted")
32
+ # airport.identifier # => "airport_london_stansted"
33
+ #
34
+ # For more information about definining and working with attributes see
35
+ # 'Virtus' project page: https://github.com/solnic/virtus
36
+ #
37
+ # To have validations for attributes just implement ActiveModel validations.
38
+ # For more informations about that see the ActiveModel validations docs:
39
+ # http://api.rubyonrails.org/classes/ActiveModel/Validations.html
40
+ #
41
+ # Example
42
+ #
43
+ # class Airport
44
+ # include Seatbelt::Ghost
45
+ # include Seatbelt::Document
46
+ #
47
+ # attribute :name, String
48
+ # attribute :lat, Float
49
+ # attribute :lng, Float
50
+ #
51
+ # validates_presence_of :name
52
+ #
53
+ # api_method :identifier
54
+ #
55
+ # end
56
+ #
57
+ # For associations between Documents see Association module below.
58
+ module Document
59
+
60
+ def self.included(base)
61
+ base.class_eval do
62
+ include ::Virtus.model
63
+ include ::ActiveModel::Validations
64
+ extend Document::Associations
65
+ end
66
+ end
67
+
68
+
69
+ # Associations are a set of macro-like class methods for tying Ruby objects
70
+ # together.
71
+ # Each macro adds a getter and setter method that expresses the realtionship
72
+ # depending on its type.
73
+ # A 'has_many' relation defines an Array of models that also acts just the
74
+ # same as the definition says.
75
+ # A 'has' relation defines a single relation to another Ruby object.
76
+ #
77
+ # Note that every Class used for associations has to include
78
+ # Seatbelt::Document.
79
+ #
80
+ # A little bit more about 'has_many':
81
+ #
82
+ # The resulting 'has_many' Array is an abstraction of
83
+ # Virtus::Attribute::Collection.
84
+ #
85
+ # If it's defined
86
+ #
87
+ # has_many :horses, Horse
88
+ #
89
+ # Seatbelt awaits a collection definition like:
90
+ #
91
+ # module Seatbelt
92
+ # module Collections
93
+ # class HorseCollection < Seatbelt::Collections::Collection
94
+ # # do smth
95
+ # end
96
+ # end
97
+ # end
98
+ #
99
+ # The most important part is the collections namespace, it should be
100
+ # prefixed with Seatbelt::Collections. The collection itself has to
101
+ # implement a Virtus::Attribute.
102
+ #
103
+ module Associations
104
+
105
+ # Public: Defines a 1:n association.
106
+ #
107
+ # That is - in fact - an Array of models. There is no "other side" like
108
+ # it's known from ActiveRecord or Mongoid.
109
+ #
110
+ # collection_name - The name of the collection. That will be also the
111
+ # name of accessor method.
112
+ # collection_model - The model class used for this collection.
113
+ #
114
+ # The resulting accessor method as like an Array, with some features:
115
+ #
116
+ # Adding a model to the relationship awaits an instance of the class
117
+ # defined in 'collection_model' (otherwise a
118
+ # Seatbelt::Errors::TypeMissmatchError is raised) or a hash of attribute
119
+ # values. Any attributes included in this Hash that are not defined
120
+ # within the model are ignored.
121
+ #
122
+ # Example
123
+ #
124
+ # module HorseFarm
125
+ # module Models
126
+ # class Barnstable
127
+ # include Seatbelt::Document
128
+ #
129
+ # has_many :horses, HorseFarm::Models::Horse
130
+ #
131
+ # end
132
+ # end
133
+ # end
134
+ def has_many(collection_name, collection_model)
135
+ model_name = collection_model.name.to_s.demodulize
136
+ collection_module_name = "Seatbelt::Collections::#{model_name}Collection"
137
+ collection = Module.const_get(collection_module_name)
138
+ collection.initialize_primitive(collection_model)
139
+ self.send(:attribute, collection_name, collection[collection_model])
140
+ end
141
+
142
+ # Public: Defines a single reference to another Seatbelt::Document class.
143
+ #
144
+ # reference_name - The name of the reference that will be also the
145
+ # name of accessor method
146
+ # attribute_type = nil - The reference attribute type (optional).
147
+ # If the attribute type if omitted
148
+ # Seabelt::Models::[reference_name class name] is
149
+ # used.
150
+ #
151
+ # Example
152
+ #
153
+ # module HorseFarm
154
+ # module Models
155
+ # class Horse
156
+ # include Seatbelt::Document
157
+ #
158
+ # # needs Seatbelt::Models::Horse
159
+ # has :horseman
160
+ # has :barnstable, HorseFarm::Models::Barnstable
161
+ # end
162
+ # end
163
+ # end
164
+ #
165
+ def has(reference_name, attribute_type = nil)
166
+ unless attribute_type
167
+ attribute_type = "Seatbelt::Models::#{reference_name.to_s.classify}".
168
+ constantize
169
+ end
170
+ self.send(:attribute, reference_name, attribute_type)
171
+ end
172
+
173
+ end
174
+ end
175
+ end