protip 0.20.7 → 0.30.0

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/protip.rb +10 -0
  3. data/lib/protip/{wrapper.rb → decorator.rb} +98 -82
  4. data/lib/protip/extensions.rb +0 -1
  5. data/lib/protip/resource.rb +56 -32
  6. data/lib/protip/resource/extra_methods.rb +5 -3
  7. data/lib/protip/tasks/compile.rake +2 -2
  8. data/lib/protip/transformer.rb +14 -0
  9. data/lib/protip/transformers/abstract_transformer.rb +11 -0
  10. data/lib/protip/transformers/active_support/time_with_zone_transformer.rb +35 -0
  11. data/lib/protip/transformers/decorating_transformer.rb +33 -0
  12. data/lib/protip/transformers/default_transformer.rb +22 -0
  13. data/lib/protip/transformers/delegating_transformer.rb +44 -0
  14. data/lib/protip/transformers/deprecated_transformer.rb +90 -0
  15. data/lib/protip/transformers/enum_transformer.rb +93 -0
  16. data/lib/protip/transformers/primitives_transformer.rb +78 -0
  17. data/lib/protip/transformers/timestamp_transformer.rb +31 -0
  18. data/test/functional/protip/decorator_test.rb +204 -0
  19. data/test/unit/protip/decorator_test.rb +665 -0
  20. data/test/unit/protip/resource_test.rb +76 -92
  21. data/test/unit/protip/transformers/decorating_transformer_test.rb +92 -0
  22. data/test/unit/protip/transformers/delegating_transformer_test.rb +83 -0
  23. data/test/unit/protip/transformers/deprecated_transformer_test.rb +139 -0
  24. data/test/unit/protip/transformers/enum_transformer_test.rb +157 -0
  25. data/test/unit/protip/transformers/primitives_transformer_test.rb +139 -0
  26. data/test/unit/protip/transformers/timestamp_transformer_test.rb +46 -0
  27. metadata +23 -12
  28. data/definitions/google/protobuf/wrappers.proto +0 -99
  29. data/lib/google/protobuf/descriptor.rb +0 -1
  30. data/lib/google/protobuf/wrappers.rb +0 -48
  31. data/lib/protip/converter.rb +0 -22
  32. data/lib/protip/standard_converter.rb +0 -185
  33. data/test/unit/protip/standard_converter_test.rb +0 -502
  34. data/test/unit/protip/wrapper_test.rb +0 -646
@@ -1,3 +1,5 @@
1
+ require 'protip/decorator'
2
+
1
3
  module Protip
2
4
  module Resource
3
5
  # Internal helpers for non-resourceful member/collection methods. Never use these directly;
@@ -9,15 +11,15 @@ module Protip
9
11
  method: method,
10
12
  message: message,
11
13
  response_type: response_type
12
- nil == response ? nil : ::Protip::Wrapper.new(response, resource.class.converter)
14
+ nil == response ? nil : ::Protip::Decorator.new(response, resource.class.transformer)
13
15
  end
14
16
  def self.collection(resource_class, action, method, message, response_type)
15
17
  response = resource_class.client.request path: "#{resource_class.base_path}/#{action}",
16
18
  method: method,
17
19
  message: message,
18
20
  response_type: response_type
19
- nil == response ? nil : ::Protip::Wrapper.new(response, resource_class.converter)
21
+ nil == response ? nil : ::Protip::Decorator.new(response, resource_class.transformer)
20
22
  end
21
23
  end
22
24
  end
23
- end
25
+ end
@@ -10,10 +10,10 @@ namespace :protip do
10
10
  filename = args[:filename] || raise(ArgumentError.new 'filename argument is required')
11
11
 
12
12
  command = "protoc #{proto_path.map{|p| "--proto_path=#{Shellwords.escape p}"}.join ' '} --ruby_out=#{Shellwords.escape ruby_path} #{Shellwords.escape filename}"
13
- puts command # is there a better way to log this?
13
+ puts command
14
14
  system command
15
15
 
16
- ## hack around missing options in Ruby, remove when https://github.com/google/protobuf/issues/1198 is resolved
16
+ ## ridiculous hack around missing options in Ruby, remove when https://github.com/google/protobuf/issues/1198 is resolved
17
17
  package_match = File.read(filename).match(/package "?([a-zA-Z0-9\.]+)"?;/)
18
18
  package = (package_match ? package_match[1] : nil)
19
19
  ruby_file = filename.gsub(/^#{proto_path.first}\/?/, "#{ruby_path}/").gsub(/proto$/, 'rb') # Relies on a relative filename and proto path, which protoc requires anyway at this point
@@ -0,0 +1,14 @@
1
+ module Protip
2
+ # Interface for an object that converts between messages and more complex Ruby types. Resources and wrapped
3
+ # messages store one of these to transparently allow getting/setting of message fields as if they were
4
+ # Ruby types.
5
+ module Transformer
6
+ def to_object(message, field)
7
+ raise NotImplementedError.new('Must convert a message into a Ruby object')
8
+ end
9
+
10
+ def to_message(object, field)
11
+ raise NotImplementedError.new('Must convert a Ruby object into a message of the given type')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require 'protip/transformer'
2
+
3
+ module Protip
4
+ module Transformers
5
+ # Instantiable version of the +::Protip::Transformer+ concern,
6
+ # for when we need a placeholder transformer object.
7
+ class AbstractTransformer
8
+ include ::Protip::Transformer
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ require 'protip/transformer'
2
+ require 'protip/messages/active_support/time_with_zone'
3
+
4
+ module Protip
5
+ module Transformers
6
+ module ActiveSupport
7
+ module TimeWithZoneTransformer
8
+ include Protip::Transformer
9
+
10
+ def transformable?(message_class)
11
+ message_class == ::Protip::Messages::ActiveSupport::TimeWithZone
12
+ end
13
+
14
+ def to_object(message)
15
+ ActiveSupport::TimeWithZone.new(
16
+ Time.at(message.utc_timestamp).utc,
17
+ ActiveSupport::TimeZone.new(message.time_zone_name)
18
+ )
19
+ end
20
+
21
+ def to_message(value, message_class)
22
+ if !value.is_a?(::ActiveSupport::TimeWithZone) && (value.is_a?(Time) || value.is_a?(DateTime))
23
+ value = ::ActiveSupport::TimeWithZone.new(value.to_time.utc, ::ActiveSupport::TimeZone.new('UTC'))
24
+ end
25
+ raise ArgumentError unless value.is_a?(::ActiveSupport::TimeWithZone)
26
+
27
+ message_class.new(
28
+ utc_timestamp: value.to_i,
29
+ time_zone_name: value.time_zone.name,
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ require 'protip/decorator'
2
+ require 'protip/transformer'
3
+
4
+ module Protip
5
+ module Transformers
6
+ # A transformer which decorates all messages, passing in another
7
+ # transformer (generally a "parent" delegating transformer with
8
+ # this as its fallback) to the generated decorators. Allows
9
+ # cascading message decoration.
10
+ class DecoratingTransformer
11
+ include Protip::Transformer
12
+ def initialize(transformer)
13
+ @transformer = transformer
14
+ end
15
+
16
+ def to_object(message, field)
17
+ Protip::Decorator.new(message, @transformer)
18
+ end
19
+
20
+ def to_message(object, field)
21
+ if object.is_a?(Protip::Decorator)
22
+ object.message
23
+ elsif object.is_a?(Hash)
24
+ decorator = Protip::Decorator.new(field.subtype.msgclass.new, @transformer)
25
+ decorator.assign_attributes(object)
26
+ decorator.message
27
+ else
28
+ raise ArgumentError
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ require 'protip/transformers/decorating_transformer'
2
+ require 'protip/transformers/delegating_transformer'
3
+ require 'protip/transformers/deprecated_transformer'
4
+ require 'protip/transformers/enum_transformer'
5
+ require 'protip/transformers/primitives_transformer'
6
+
7
+ module Protip
8
+ module Transformers
9
+ # A transformer for our built-in types.
10
+ class DefaultTransformer < DelegatingTransformer
11
+ def initialize
12
+ # For message types that we don't recognize, just wrap them and pass
13
+ # ourself in as the transformer for their submessages.
14
+ super Protip::Transformers::DecoratingTransformer.new(self)
15
+
16
+ merge! Protip::Transformers::PrimitivesTransformer.new
17
+ merge! Protip::Transformers::EnumTransformer.new
18
+ merge! Protip::Transformers::DeprecatedTransformer.new
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ require 'protip/transformer'
2
+ require 'protip/transformers/abstract_transformer'
3
+
4
+ module Protip
5
+ module Transformers
6
+ # A transformer which forwards to other transformers based on the message type
7
+ # being converted.
8
+ class DelegatingTransformer
9
+ include Protip::Transformer
10
+ extend Forwardable
11
+
12
+ # @param [Protip::Transformer] fallback_transformer The transformer to use
13
+ # for messages that don't have a registered transformer already.
14
+ # @param [Hash<String, Protip::Transformer>] transformers A message_name => transformer
15
+ # hash specifying which transformers to use for which message types.
16
+ def initialize(fallback_transformer = AbstractTransformer.new, transformers = {})
17
+ @fallback_transformer = fallback_transformer
18
+ @transformers = transformers.dup
19
+ end
20
+
21
+ def_delegators :@transformers, :[]=, :[], :keys
22
+
23
+ def merge!(delegating_transformer)
24
+ delegating_transformer.keys.each do |key|
25
+ self[key] = delegating_transformer[key]
26
+ end
27
+ end
28
+
29
+ def to_object(message, field)
30
+ transformer_for(field.submsg_name).to_object(message, field)
31
+ end
32
+
33
+ def to_message(object, field)
34
+ transformer_for(field.submsg_name).to_message(object, field)
35
+ end
36
+
37
+ private
38
+
39
+ def transformer_for(message_name)
40
+ @transformers[message_name] || @fallback_transformer
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,90 @@
1
+ require 'protip/transformer'
2
+ require 'protip/transformers/delegating_transformer'
3
+
4
+ require 'active_support/time_with_zone'
5
+ require 'money'
6
+
7
+ # Temporary transformer for types that will be moved out to
8
+ # user extensions.
9
+ module Protip
10
+ module Transformers
11
+ class DeprecatedTransformer < DelegatingTransformer
12
+
13
+ def initialize
14
+ super
15
+ self['protip.messages.Currency'] = CurrencyTransformer.new
16
+ self['protip.messages.Money'] = MoneyTransformer.new
17
+ self['protip.messages.Date'] = DateTransformer.new
18
+ self['protip.messages.Range'] = RangeTransformer.new
19
+ self['protip.messages.ActiveSupport.TimeWithZone'] = TimeWithZoneTransformer.new
20
+ end
21
+
22
+ class CurrencyTransformer
23
+ include Protip::Transformer
24
+ def to_object(message, field)
25
+ message.currency_code
26
+ end
27
+ def to_message(object, field)
28
+ field.subtype.msgclass.new(currency_code: object)
29
+ end
30
+ end
31
+
32
+ class MoneyTransformer
33
+ include Protip::Transformer
34
+ def to_object(message, field)
35
+ ::Money.new(message.amount_cents, message.currency.currency_code)
36
+ end
37
+ def to_message(object, field)
38
+ raise ArgumentError unless object.is_a?(::Money)
39
+ currency = Protip::Messages::Currency.new(currency_code: object.currency.iso_code.to_sym)
40
+ field.subtype.msgclass.new(
41
+ amount_cents: object.fractional,
42
+ currency: currency,
43
+ )
44
+ end
45
+ end
46
+
47
+ class DateTransformer
48
+ include Protip::Transformer
49
+ def to_object(message, field)
50
+ ::Date.new(message.year, message.month, message.day)
51
+ end
52
+ def to_message(object, field)
53
+ raise ArgumentError unless object.is_a?(::Date)
54
+ field.subtype.msgclass.new(year: object.year, month: object.month, day: object.day)
55
+ end
56
+ end
57
+
58
+ class RangeTransformer
59
+ include Protip::Transformer
60
+ def to_object(message, field)
61
+ message.begin..message.end
62
+ end
63
+ def to_message(object, field)
64
+ field.subtype.msgclass.new(begin: object.begin.to_i, end: object.end.to_i)
65
+ end
66
+ end
67
+
68
+ class TimeWithZoneTransformer
69
+ include Protip::Transformer
70
+ def to_object(message, field)
71
+ ActiveSupport::TimeWithZone.new(
72
+ Time.at(message.utc_timestamp).utc,
73
+ ActiveSupport::TimeZone.new(message.time_zone_name)
74
+ )
75
+ end
76
+ def to_message(object, field)
77
+ if !object.is_a?(::ActiveSupport::TimeWithZone) && (object.is_a?(Time) || object.is_a?(DateTime))
78
+ object = ::ActiveSupport::TimeWithZone.new(object.to_time.utc, ::ActiveSupport::TimeZone.new('UTC'))
79
+ end
80
+ raise ArgumentError unless object.is_a?(::ActiveSupport::TimeWithZone)
81
+
82
+ field.subtype.msgclass.new(
83
+ utc_timestamp: object.to_i,
84
+ time_zone_name: object.time_zone.name,
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,93 @@
1
+ require 'protip/transformers/delegating_transformer'
2
+
3
+ module Protip
4
+ module Transformers
5
+ class EnumTransformer < DelegatingTransformer
6
+ def initialize
7
+ super
8
+ self['protip.messages.EnumValue'] = ScalarTransformer.new
9
+ self['protip.messages.RepeatedEnum'] = ArrayTransformer.new
10
+ end
11
+
12
+ def self.enum_for_field(field)
13
+ Google::Protobuf::DescriptorPool.generated_pool.lookup(field.instance_variable_get(:'@_protip_enum_value'))
14
+ end
15
+
16
+ # Internal helper classes - under the hood we use separate transformers for
17
+ # the scalar and repeated cases, and both of them share transformation logic
18
+ # by inheriting from +SingleMessageEnumTransformer+.
19
+ class SingleMessageEnumTransformer
20
+ include Protip::Transformer
21
+ private
22
+ # Instance-level cached version of the class method above
23
+ def enum_for_field(field)
24
+ @enum_for_field_cache ||= {}
25
+ name = EnumTransformer.enum_for_field(field)
26
+ @enum_for_field_cache[name] ||= EnumTransformer.enum_for_field(field)
27
+
28
+ @enum_for_field_cache[name] ||
29
+ raise("protip_enum missing or invalid for field '#{field.name}'")
30
+ end
31
+
32
+ # Matches the protobuf enum setter behavior.
33
+ # Convert +:VALUE+ or +5+ to their corresponding enum integer value.
34
+ # @example
35
+ # // foo.proto
36
+ # enum Foo {
37
+ # BAR = 0;
38
+ # BAZ = 1;
39
+ # }
40
+ # // ScalarTransformer.to_int(:BAZ) # => 1
41
+ # // ScalarTransformer.to_int(4) # => 4
42
+ def to_int(symbol_or_int, field)
43
+ if symbol_or_int.is_a?(Fixnum)
44
+ symbol_or_int
45
+ else
46
+ # Convert +.to_sym+ explicitly to allow strings (or other
47
+ # symobolizable objects) to be passed in to setters.
48
+ enum_for_field(field).lookup_name(symbol_or_int.to_sym) ||
49
+ raise(RangeError.new "unknown symbol value for field '#{field.name}'")
50
+ end
51
+ end
52
+
53
+ # Matches the protobuf enum getter behavior.
54
+ # Convert integers to their associated enum symbol, or pass them
55
+ # through if the
56
+ def to_symbol_or_int(int, field)
57
+ enum = EnumTransformer.enum_for_field(field) ||
58
+ raise("protip_enum missing or invalid for field '#{field.name}'")
59
+ enum.lookup_value(int) || int
60
+ end
61
+ end
62
+ private_constant :SingleMessageEnumTransformer
63
+
64
+ # For +protip.messages.EnumValue+
65
+ class ScalarTransformer < SingleMessageEnumTransformer
66
+ def to_object(message, field)
67
+ to_symbol_or_int(message.value, field)
68
+ end
69
+ def to_message(object, field)
70
+ field.subtype.msgclass.new(value: to_int(object, field))
71
+ end
72
+ end
73
+ private_constant :ScalarTransformer
74
+
75
+ # For +protip.messages.RepeatedEnum+
76
+ class ArrayTransformer < SingleMessageEnumTransformer
77
+ def to_object(message, field)
78
+ message.values.map do |value|
79
+ to_symbol_or_int(value, field)
80
+ end
81
+ end
82
+
83
+ def to_message(object, field)
84
+ values = (object.is_a?(::Enumerable) ? object : [object]).map do |value|
85
+ to_int(value, field)
86
+ end
87
+ field.subtype.msgclass.new(values: values)
88
+ end
89
+ end
90
+ private_constant :ArrayTransformer
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,78 @@
1
+ require 'protip/transformer'
2
+
3
+ require 'protip/transformers/delegating_transformer'
4
+
5
+ module Protip
6
+ module Transformers
7
+ # Transforms ints, strings, booleans, floats, and bytes to/from their
8
+ # well-known types (for scalars) and Protip repeated types.
9
+ class PrimitivesTransformer < DelegatingTransformer
10
+ TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON']
11
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF']
12
+
13
+ def initialize
14
+ super
15
+ {
16
+ Int64: :to_i.to_proc,
17
+ Int32: :to_i.to_proc,
18
+ UInt64: :to_i.to_proc,
19
+ UInt32: :to_i.to_proc,
20
+ Double: :to_f.to_proc,
21
+ Float: :to_f.to_proc,
22
+ String: :to_s.to_proc,
23
+ Bool: ->(object) { to_boolean(object) },
24
+ Bytes: ->(object) { object },
25
+ }.each do |type, transform|
26
+ self["google.protobuf.#{type}Value"] = ScalarTransformer.new(transform)
27
+ self["protip.messages.Repeated#{type}"] = ArrayTransformer.new(transform)
28
+ end
29
+ end
30
+
31
+ private
32
+ def to_boolean(object)
33
+ return true if TRUE_VALUES.include?(object)
34
+ return false if FALSE_VALUES.include?(object)
35
+
36
+ object
37
+ end
38
+
39
+ # Helper transfomer for scalar well-known types.
40
+ class ScalarTransformer
41
+ # @param [Proc] transform Proc to convert a Ruby object to the
42
+ # primitive type that we're transforming to.
43
+ def initialize(transform)
44
+ @transform = transform
45
+ end
46
+
47
+ def to_object(message, field)
48
+ message.value
49
+ end
50
+
51
+ def to_message(object, field)
52
+ value = @transform[object]
53
+ field.subtype.msgclass.new(value: value)
54
+ end
55
+ end
56
+ private_constant :ScalarTransformer
57
+
58
+ # Helper transformer for repeated types.
59
+ class ArrayTransformer
60
+ def initialize(transform)
61
+ @transform = transform
62
+ end
63
+
64
+ def to_object(message, field)
65
+ message.values.to_a.freeze
66
+ end
67
+
68
+ def to_message(object, field)
69
+ values = (object.is_a?(::Enumerable) ? object : [object]).map do |value|
70
+ @transform[value]
71
+ end
72
+ field.subtype.msgclass.new(values: values)
73
+ end
74
+ end
75
+ private_constant :ArrayTransformer
76
+ end
77
+ end
78
+ end