protojson 0.1.0 → 0.2.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.
@@ -0,0 +1,7 @@
1
+ package examples;
2
+
3
+ message Simple {
4
+ required string foo = 1;
5
+ required int32 bar = 2;
6
+ optional string baz = 3;
7
+ }
data/lib/protojson.rb CHANGED
@@ -1,6 +1,84 @@
1
- require 'protobuf/message/message'
2
- require 'extensions/protobuf/message'
1
+ %W(binary json hash json_indexed json_tag_map).each { |codec|
2
+ require "protojson/codec/#{codec}"
3
+ }
3
4
 
4
5
  module Protojson
5
- # Your code goes here...
6
+
7
+ class << self
8
+
9
+ # Key name - Codec value
10
+ def []=(codec_name = nil, codec = nil)
11
+ codec_name.is_a?(String) and codec_name.downcase!
12
+
13
+ unless codec_name.nil? # default codec
14
+ unless codec_name.is_a?(Symbol)
15
+ if codec_name.respond_to?(:to_sym)
16
+ codec_name = codec_name.to_sym
17
+ else
18
+ raise Exception, "Only symbols allowed as codec names"
19
+ end
20
+ end
21
+ end
22
+
23
+ if codec.nil? # unregister codec
24
+ codecs.delete(codec_name)
25
+ else # register codec
26
+ !codec.is_a? Protojson::Codec::CodecInterface and raise Exception, "codec should include Protojson::Codec::CodecInterface"
27
+ codecs[codec_name] = codec
28
+ end
29
+ end
30
+
31
+ # Returns the codec specified or the default one if no attribute is defined
32
+ def [](codec = nil)
33
+ codec.is_a?(Protojson::Codec::CodecInterface) and return codec
34
+ codec.is_a?(Symbol) or codec.nil? and return codecs[codec]
35
+ raise "Invalid codec #{codec}. It should either implement Protojson::Codec::CodecInterface or be a symbol"
36
+ end
37
+
38
+ # Initializes the codecs Hash
39
+ def codecs
40
+ @codecs ||= (
41
+ # While accessing the Hash, if the key does not exist, throws an exception
42
+ h = Hash.new { |hash, key|
43
+ hash.has_key?(key) and return hash[key]
44
+ !key.nil? and raise Exception, "Undefined codec #{key}"
45
+ h[nil] = Protojson::Codec::Binary
46
+ }
47
+ # default value is Binary codec
48
+ h[nil] = Protojson::Codec::Binary
49
+ h[:hash] = Protojson::Codec::Hash
50
+ h[:indexed] = Protojson::Codec::JsonIndexed
51
+ h[:json] = Protojson::Codec::Json
52
+ h[:tag_map] = Protojson::Codec::JsonTagMap
53
+ h
54
+ )
55
+ end
56
+
57
+ def set_default_codec(codec)
58
+ # fetch codec from Hash if codec name received
59
+ if codec.is_a?(Symbol) or codec.is_a?(String)
60
+ codec = Protojson[codec]
61
+ end
62
+
63
+ # set default codec
64
+ puts codec.is_a?(Protojson::Codec::CodecInterface)
65
+ if codec.is_a?(Protojson::Codec::CodecInterface)
66
+ codecs[nil] = codec
67
+ else
68
+ raise Exception, 'Codec must implement Protojson::Codec::CodecInterface'
69
+ end
70
+ end
71
+
72
+ def encode(message, codec = nil)
73
+ codec = send("[]".to_sym, codec)
74
+ codec.encode(message)
75
+ end
76
+
77
+ def decode(message, data, codec = nil)
78
+ codec = send("[]".to_sym, codec) # fetch default codec if none given
79
+ codec.decode(message, data)
80
+ end
81
+
82
+ end
83
+
6
84
  end
@@ -0,0 +1,21 @@
1
+ require 'protojson/codec/codec_interface'
2
+
3
+ module Protojson
4
+ module Codec
5
+ class Binary
6
+ extend Protojson::Codec::CodecInterface
7
+
8
+ class << self
9
+
10
+ def encode(message)
11
+ message.serialize_to_string
12
+ end
13
+
14
+ def decode(message, data)
15
+ message.new.parse_from_string(data)
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module Protojson
2
+ module Codec
3
+ module CodecInterface
4
+
5
+ def encode(message)
6
+ raise NotImplementedError, "#{self.class} should implement encode method"
7
+ end
8
+
9
+ def decode(message, data)
10
+ raise NotImplementedError, "#{self.class} should implement decode method"
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,121 @@
1
+ require 'protobuf/message/message'
2
+ require 'protobuf/message/field'
3
+ require 'protobuf/message/enum'
4
+
5
+ module Protojson
6
+ module Codec
7
+ class Hash
8
+ extend Protojson::Codec::CodecInterface
9
+
10
+ class << self
11
+
12
+ # Encode the message to a hash object defined as a collection for key/values
13
+ # where each element has:
14
+ # - key: field tag
15
+ # - value:
16
+ # - if field.is_a? message_field => field.value.serialized_to_hash
17
+ # - if field.is_a? enum_field => field.value.value
18
+ # - else => field.value
19
+ #
20
+ # @return [Hash] a specific Message encoded in an Hash object
21
+ def encode(message, encoding_key = :name)
22
+ raise NotInitializedError unless message.initialized?
23
+ # var to store the encoded message fields
24
+ result = {}
25
+
26
+ # lambda function that extract the field tag and value
27
+ # it's called recursively if value is a MessageField
28
+ field_value_to_string = lambda { |field, value|
29
+ field_value = \
30
+ if field.optional? && !message.has_field?(field.name)
31
+ ''
32
+ else
33
+ case field
34
+ when Protobuf::Field::MessageField
35
+ if value.nil?
36
+ nil
37
+ else
38
+ encode(value, encoding_key)
39
+ end
40
+ when Protobuf::Field::EnumField
41
+ if value.is_a?(Protobuf::EnumValue)
42
+ value.value
43
+ else
44
+ value.to_i
45
+ end
46
+ else
47
+ value
48
+ end
49
+ end
50
+ return field.send(encoding_key), field_value
51
+ }
52
+
53
+ # per each message field create a new element in result var with
54
+ # key = field.tag and value = field.value
55
+ message.each_field do |field, value|
56
+ # create a vector if field is repeated
57
+ if field.repeated? && !value.empty?
58
+ key_value = []
59
+ key = nil
60
+ value.each do |v|
61
+ key, val = field_value_to_string.call(field, v) # always return the same key
62
+ key_value.push val
63
+ end
64
+ # field is not repeated but is not empty
65
+ elsif !field.repeated?
66
+ key, key_value = field_value_to_string.call(field, value)
67
+ # empty field, discard
68
+ else
69
+ next
70
+ end
71
+ # new element in result Hash
72
+ unless key_value.nil? or (key_value.respond_to?(:empty?) and key_value.empty?)
73
+ result[key.to_s] = key_value
74
+ end
75
+ end
76
+ result
77
+ end
78
+
79
+ # This method parses a JSON encoded message to the message object
80
+ def decode(message, data, decoding_key = :name)
81
+
82
+ message = message.new
83
+
84
+ # per each hash element:
85
+ data.each_pair { |key, value|
86
+ key = decoding_key.eql?(:tag) ? key.to_i : key.to_s
87
+
88
+ # get the field object using the key (field tag)
89
+ #field = message.get_field_by_tag(key.to_i)
90
+ field = message.send("get_field_by_#{decoding_key}".to_sym, key)
91
+
92
+ if field.nil?
93
+ # ignore unknown field
94
+ elsif field.repeated?
95
+ # create the element
96
+ array = message.__send__(field.name)
97
+ value.each { |val|
98
+ # if value is a complex field, create the object and parse the content
99
+ if field.is_a?(Protobuf::Field::MessageField)
100
+ instance = field.type
101
+ val = decode(instance, val, decoding_key)
102
+ end
103
+ # include each element in the parent element field
104
+ array.push(val)
105
+ }
106
+ else
107
+ # if value is a complex field, create the object and parse the content
108
+ if field.is_a?(Protobuf::Field::MessageField)
109
+ instance = field.type
110
+ value = decode(instance, value, decoding_key)
111
+ end
112
+ # set the message field
113
+ message.__send__("#{field.name}=", value)
114
+ end
115
+ }
116
+ message
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support/core_ext'
2
+
3
+ require 'protojson/codec/codec_interface'
4
+
5
+ module Protojson
6
+ module Codec
7
+ class Json
8
+ extend Protojson::Codec::CodecInterface
9
+
10
+ class << self
11
+
12
+ def encode(message)
13
+ Protojson::Codec::Hash.encode(message).to_json
14
+ end
15
+
16
+ def decode(message, data)
17
+ data.is_a?(String) and data = ActiveSupport::JSON.decode(data)
18
+ Protojson::Codec::Hash.decode(message, data)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,85 @@
1
+ require 'active_support/core_ext'
2
+
3
+ require 'protojson/codec/codec_interface'
4
+
5
+ module Protojson
6
+ module Codec
7
+ class JsonIndexed
8
+ extend Protojson::Codec::CodecInterface
9
+
10
+ class << self
11
+
12
+ def encode(message)
13
+ data = Protojson::Codec::Hash.encode(message, :tag)
14
+ serialize_hash_to_indexed(data)
15
+ end
16
+
17
+ def decode(message, data)
18
+ data.is_a?(String) and data = ActiveSupport::JSON.decode(data)
19
+
20
+ values = parse_indexed_to_hash(message.new, data)
21
+ Protojson::Codec::Hash.decode(message, values, :tag)
22
+ end
23
+
24
+ # This class method serializes a Hash
25
+ def serialize_hash_to_indexed(value)
26
+ !value.is_a?(::Hash) and raise ArgumentError, "value must be a hash"
27
+
28
+ # index value
29
+ index = ""
30
+ index.respond_to?(:force_encoding) and index.force_encoding("UTF-8") # 1.9.2
31
+ # field values
32
+ result = []
33
+ value.each_pair { |key, value|
34
+ index << key.to_i+48 # ASCII integer. 1 => 49, ...
35
+ # recursive call if value is a Hash
36
+ if value.is_a?(::Hash)
37
+ value = serialize_hash_to_indexed(value)
38
+ # array => serializes each element
39
+ elsif value.is_a?(Array)
40
+ value.map! { |val|
41
+ if val.is_a?(::Hash)
42
+ serialize_hash_to_indexed(val)
43
+ else
44
+ val
45
+ end
46
+ }
47
+ end
48
+ # insert encoded value in Array
49
+ result.push value
50
+ }
51
+ # include index as first element
52
+ result.unshift(index)
53
+ end
54
+
55
+ # This method parses a INDEXED encoded message to a hash using
56
+ # message message as model.
57
+ # We need to know the specific message to decode to differenciate between
58
+ # vectors and message fields
59
+ def parse_indexed_to_hash(message, data)
60
+ values = {}
61
+ index = data.shift
62
+ index.each_codepoint { |tag|
63
+ tag = tag-48
64
+ field = message.get_field_by_tag(tag)
65
+ val = data.shift
66
+
67
+ if val.is_a?(Array) && field.is_a?(Protobuf::Field::MessageField)
68
+ if field.repeated?
69
+ val.map! { |v|
70
+ v = parse_indexed_to_hash(field.type, v)
71
+ }
72
+ else
73
+ val = parse_indexed_to_hash(field.type, val)
74
+ end
75
+ end
76
+ values[tag.to_s] = val
77
+ }
78
+ values
79
+ end
80
+ end
81
+
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/core_ext'
2
+
3
+ require 'protojson/codec/codec_interface'
4
+
5
+ module Protojson
6
+ module Codec
7
+ class JsonTagMap
8
+ extend Protojson::Codec::CodecInterface
9
+
10
+ class << self
11
+
12
+ def encode(message)
13
+ Protojson::Codec::Hash.encode(message, :tag).to_json
14
+ end
15
+
16
+ def decode(message, data)
17
+ data.is_a?(String) and data = ActiveSupport::JSON.decode(data)
18
+ Protojson::Codec::Hash.decode(message, data, :tag)
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module Protojson
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/protojson.gemspec CHANGED
@@ -3,13 +3,13 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
  require "protojson/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = "protojson"
7
- s.version = Protojson::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.authors = ["Juan de Bravo", "Ivan -DrSlump- Montes"]
10
- s.email = ["juandebravo@gmail.com", "drslump@pollinimini.net"]
11
- s.homepage = ""
12
- s.summary = %q{Ruby extension to ruby-protobuf to enable three new encodings}
6
+ s.name = "protojson"
7
+ s.version = Protojson::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Juan de Bravo", "Ivan -DrSlump- Montes"]
10
+ s.email = ["juandebravo@gmail.com", "drslump@pollinimini.net"]
11
+ s.homepage = ""
12
+ s.summary = %q{Ruby extension to ruby-protobuf to enable three new encodings}
13
13
  s.description = %q{A Ruby gem for Google's Protocol Buffers messages using three different encodings JSON
14
14
  based syntax instead of the original binary protocol. Supported formats
15
15
 
@@ -25,10 +25,17 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.rubyforge_project = "protojson"
27
27
 
28
- s.files = `git ls-files`.split("\n")
28
+ s.files = `git ls-files`.split("\n")
29
29
  s.files.delete("Gemfile.lock")
30
30
  s.files.delete(".gitignore")
31
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
31
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
33
33
  s.require_paths = ["lib"]
34
+
35
+ s.add_dependency('ruby_protobuf', '~>0.4.10')
36
+ s.add_dependency('activesupport', '~>3.0.5')
37
+ s.add_dependency('i18n', '~>0.5.0')
38
+
39
+ s.add_development_dependency("rspec")
40
+
34
41
  end
@@ -0,0 +1,150 @@
1
+ # -*- coding: UTF-8 -*-
2
+
3
+ #$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+ $:.unshift File.join(File.dirname(__FILE__), '..')
5
+
6
+ require 'protojson'
7
+
8
+ require 'examples/simple.pb'
9
+ require 'examples/repeated.pb'
10
+ require 'examples/addressbook.pb'
11
+
12
+ describe Protojson::Codec::Binary do
13
+ before(:all) do
14
+ Protojson.set_default_codec(Protojson::Codec::Binary)
15
+ end
16
+
17
+ context "simple message" do
18
+
19
+ it "should encode properly when two required attributes provided" do
20
+ simple = Examples::Simple.new
21
+ simple.foo = "Foo"
22
+ simple.bar = 1000
23
+ decoded = Protojson.decode(Examples::Simple, Protojson.encode(simple))
24
+ decoded.foo.should eql("Foo")
25
+ decoded.bar.should eql(1000)
26
+ end
27
+
28
+ it "should encode properly when two required attributes and one optional attribute provided" do
29
+ simple = Examples::Simple.new
30
+ simple.foo = "Foo"
31
+ simple.bar = 1000
32
+ simple.baz = "Bazz"
33
+ decoded = Protojson.decode(Examples::Simple, Protojson.encode(simple))
34
+ decoded.foo.should eql("Foo")
35
+ decoded.bar.should eql(1000)
36
+ decoded.baz.should eql("Bazz")
37
+ end
38
+ end
39
+
40
+ context "repeated message" do
41
+
42
+ it "should encode properly when the string field has more than one value" do
43
+ repeated = Examples::Repeated.new
44
+ repeated.string << "one"
45
+ repeated.string << "two"
46
+ repeated.string << "three"
47
+ decoded = Protojson.decode(Examples::Repeated, Protojson.encode(repeated))
48
+ decoded.string.length.should eql(3)
49
+ decoded.string[0].should eql("one")
50
+ decoded.string[1].should eql("two")
51
+ decoded.string[2].should eql("three")
52
+ end
53
+
54
+ it "should encode properly when the int field has more than one value" do
55
+ repeated = Examples::Repeated.new
56
+ repeated.int << 1
57
+ repeated.int << 2
58
+ repeated.int << 3
59
+ decoded = Protojson.decode(Examples::Repeated, Protojson.encode(repeated))
60
+ decoded.int.length.should eql(3)
61
+ (0..2).each{|i|
62
+ decoded.int[i].should eql(i+1)
63
+ }
64
+ end
65
+
66
+ it "should encode properly when both int and string fields has more than one value" do
67
+ repeated = Examples::Repeated.new
68
+ repeated.string << "one"
69
+ repeated.string << "two"
70
+ repeated.string << "three"
71
+ repeated.int << 1
72
+ repeated.int << 2
73
+ repeated.int << 3
74
+ decoded = Protojson.decode(Examples::Repeated, Protojson.encode(repeated))
75
+ decoded.string.length.should eql(3)
76
+ decoded.string[0].should eql("one")
77
+ decoded.string[1].should eql("two")
78
+ decoded.string[2].should eql("three")
79
+ decoded.int.length.should eql(3)
80
+ (0..2).each{|i|
81
+ decoded.int[i].should eql(i+1)
82
+ }
83
+ end
84
+
85
+ it "should encode properly when the nested field has more than one value" do
86
+ repeated = Examples::Repeated.new
87
+ (1..3).each { |id|
88
+ nested = Examples::Repeated::Nested.new
89
+ nested.id = id
90
+ repeated.nested << nested
91
+ }
92
+ decoded = Protojson.decode(Examples::Repeated, Protojson.encode(repeated))
93
+ decoded.nested.length.should eql(3)
94
+ (0..2).each{|i|
95
+ decoded.nested[i].should be_instance_of Examples::Repeated::Nested
96
+ decoded.nested[i].id.should eql(i+1)
97
+ }
98
+ end
99
+
100
+ end
101
+
102
+ context "complex message" do
103
+ it "should work!! :)" do
104
+ book = Examples::AddressBook.new
105
+ person = Examples::Person.new
106
+ person.name = 'John Doe'
107
+ person.id = 2051
108
+ person.email = "john.doe@gmail.com"
109
+ phone = Examples::Person::PhoneNumber.new
110
+ phone.number = '1231231212'
111
+ phone.type = Examples::Person::PhoneType::HOME
112
+ person.phone << phone
113
+ phone = Examples::Person::PhoneNumber.new
114
+ phone.number = '55512321312'
115
+ phone.type = Examples::Person::PhoneType::MOBILE
116
+ person.phone << phone
117
+ book.person << person
118
+
119
+ person = Examples::Person.new
120
+ person.name = "Ivan Montes"
121
+ person.id = 23
122
+ person.email = "drslump@pollinimini.net"
123
+ phone = Examples::Person::PhoneNumber.new
124
+ phone.number = '3493123123'
125
+ phone.type = Examples::Person::PhoneType::WORK
126
+ person.phone << phone
127
+ book.person << person
128
+
129
+ person = Examples::Person.new
130
+ person.name = "Juan de Bravo"
131
+ person.id = 24
132
+ person.email = "juan@pollinimini.net"
133
+ phone = Examples::Person::PhoneNumber.new
134
+ phone.number = '3493123124'
135
+ phone.type = Examples::Person::PhoneType::WORK
136
+ person.phone << phone
137
+ book.person << person
138
+
139
+ decoded = Protojson.decode(Examples::AddressBook, Protojson.encode(book))
140
+
141
+ decoded.person.length.should eql 3
142
+ decoded.person[0].name.should eql "John Doe"
143
+ decoded.person[1].name.should eql "Ivan Montes"
144
+ decoded.person[2].name.should eql "Juan de Bravo"
145
+ decoded.person[0].phone[1].number.should eql "55512321312"
146
+ end
147
+
148
+ end
149
+
150
+ end