json_mapper 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/json_mapper.gemspec +3 -2
- data/lib/json_mapper.rb +117 -17
- data/lib/json_mapper/attribute.rb +24 -6
- data/lib/json_mapper/attribute_list.rb +7 -2
- data/lib/json_mapper/parser.rb +11 -0
- data/test/fixtures/complex.json +3 -0
- data/test/fixtures/simple.json +3 -1
- data/test/json_mapper_test.rb +50 -0
- data/test/support/models.rb +7 -0
- metadata +6 -5
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/json_mapper.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{json_mapper}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Trond Arve Nordheim"]
|
12
|
-
s.date = %q{2010-08-
|
12
|
+
s.date = %q{2010-08-09}
|
13
13
|
s.email = %q{tanordheim@gmail.com}
|
14
14
|
s.extra_rdoc_files = [
|
15
15
|
"LICENSE",
|
@@ -28,6 +28,7 @@ Gem::Specification.new do |s|
|
|
28
28
|
"lib/json_mapper.rb",
|
29
29
|
"lib/json_mapper/attribute.rb",
|
30
30
|
"lib/json_mapper/attribute_list.rb",
|
31
|
+
"lib/json_mapper/parser.rb",
|
31
32
|
"test.rb",
|
32
33
|
"test/fixtures/complex.json",
|
33
34
|
"test/fixtures/simple.json",
|
data/lib/json_mapper.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require "json"
|
2
2
|
|
3
3
|
class Boolean; end
|
4
|
+
class DelimitedString; end
|
4
5
|
|
5
6
|
module JSONMapper
|
6
7
|
|
7
8
|
def self.included(base)
|
8
9
|
base.instance_variable_set("@attributes", {})
|
10
|
+
base.instance_variable_set("@json_data", {})
|
9
11
|
base.extend ClassMethods
|
10
12
|
end
|
11
13
|
|
@@ -13,8 +15,8 @@ module JSONMapper
|
|
13
15
|
|
14
16
|
def json_attribute(name, *args)
|
15
17
|
|
16
|
-
source_attributes, type = extract_attribute_data(name, *args)
|
17
|
-
attribute = Attribute.new(name, source_attributes, type)
|
18
|
+
source_attributes, type, options = extract_attribute_data(name, *args)
|
19
|
+
attribute = Attribute.new(name, source_attributes, type, options)
|
18
20
|
@attributes[to_s] ||= []
|
19
21
|
@attributes[to_s] << attribute
|
20
22
|
|
@@ -24,8 +26,8 @@ module JSONMapper
|
|
24
26
|
|
25
27
|
def json_attributes(name, *args)
|
26
28
|
|
27
|
-
source_attributes, type = extract_attribute_data(name, *args)
|
28
|
-
attribute = AttributeList.new(name, source_attributes, type)
|
29
|
+
source_attributes, type, options = extract_attribute_data(name, *args)
|
30
|
+
attribute = AttributeList.new(name, source_attributes, type, options)
|
29
31
|
@attributes[to_s] ||= []
|
30
32
|
@attributes[to_s] << attribute
|
31
33
|
|
@@ -37,17 +39,30 @@ module JSONMapper
|
|
37
39
|
@attributes[to_s] || []
|
38
40
|
end
|
39
41
|
|
40
|
-
def
|
42
|
+
def json_data
|
43
|
+
@json_data[to_s] || []
|
44
|
+
end
|
41
45
|
|
42
|
-
|
43
|
-
json = JSON.parse(data, { :symbolize_names => true })
|
46
|
+
def parse(data, options = {})
|
44
47
|
|
45
|
-
|
48
|
+
return nil if data.nil? || data == ""
|
49
|
+
json = get_json_structure(data, options)
|
46
50
|
parse_json(json)
|
47
51
|
|
48
52
|
end
|
49
53
|
|
54
|
+
def parse_collection(data, options = {})
|
55
|
+
|
56
|
+
return [] if data.nil? || data == ""
|
57
|
+
json = get_json_structure(data, options)
|
58
|
+
parse_json_collection(json)
|
59
|
+
|
60
|
+
end
|
61
|
+
|
50
62
|
def parse_json(json)
|
63
|
+
|
64
|
+
# Set the JSON data for this instance
|
65
|
+
@json_data[to_s] = json
|
51
66
|
|
52
67
|
# Create a new instance of ourselves
|
53
68
|
instance = new
|
@@ -67,7 +82,19 @@ module JSONMapper
|
|
67
82
|
if attribute.is_a?(AttributeList)
|
68
83
|
value = [ value ] unless value.is_a?(Array)
|
69
84
|
value.each do |v|
|
70
|
-
|
85
|
+
|
86
|
+
list_attribute = build_attribute(attribute.name, attribute.type, attribute.options)
|
87
|
+
list_attribute_value = list_attribute.typecast(v)
|
88
|
+
|
89
|
+
# Some times typecasting a value for a list will produce another list, in the case of
|
90
|
+
# for instance DelimitedString. If this is the case, we concat that array to the list.
|
91
|
+
# Otherwise, we just append the value.
|
92
|
+
if list_attribute_value.is_a?(Array)
|
93
|
+
instance.send("#{attribute.name}").concat(list_attribute_value)
|
94
|
+
else
|
95
|
+
instance.send("#{attribute.name}") << list_attribute_value
|
96
|
+
end
|
97
|
+
|
71
98
|
end
|
72
99
|
else
|
73
100
|
instance.send("#{attribute.name}=".to_sym, attribute.typecast(value))
|
@@ -79,10 +106,43 @@ module JSONMapper
|
|
79
106
|
|
80
107
|
end
|
81
108
|
|
109
|
+
def parse_json_collection(json)
|
110
|
+
|
111
|
+
collection = []
|
112
|
+
|
113
|
+
if json.is_a?(Array)
|
114
|
+
json.each do |element|
|
115
|
+
collection << parse_json(element)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
collection
|
120
|
+
|
121
|
+
end
|
122
|
+
|
82
123
|
private
|
83
124
|
|
84
|
-
def
|
85
|
-
|
125
|
+
def get_json_structure(data, options = {})
|
126
|
+
|
127
|
+
# Parse the data into a hash
|
128
|
+
json = Parser.parse(data)
|
129
|
+
|
130
|
+
# If we need to shift the structure, do that now
|
131
|
+
shift = options.delete(:shift)
|
132
|
+
unless shift.nil?
|
133
|
+
shift = [ shift ] unless shift.is_a?(Array)
|
134
|
+
shift.each do |s|
|
135
|
+
break unless json.key?(s) # Break out if we can't find the element we're looking for
|
136
|
+
json = json[s]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
json
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_attribute(name, type, options)
|
145
|
+
Attribute.new(name, name, type, options)
|
86
146
|
end
|
87
147
|
|
88
148
|
def extract_attribute_data(name, *args)
|
@@ -94,10 +154,10 @@ module JSONMapper
|
|
94
154
|
raise ArgumentError.new("Type parameter is required")
|
95
155
|
end
|
96
156
|
|
97
|
-
# If the first argument is a symbol or an array, that's
|
157
|
+
# If the first argument is a symbol, string or an array, that's
|
98
158
|
# a specific source attribute mapping. If not, use the
|
99
159
|
# specified name as the source attribute name.
|
100
|
-
if args[0].is_a?(Symbol) || args[0].is_a?(Array)
|
160
|
+
if args[0].is_a?(Symbol) || args[0].is_a?(Array) || args[0].is_a?(String) || args[0].is_a?(Hash)
|
101
161
|
source_attributes = args.delete_at(0)
|
102
162
|
else
|
103
163
|
source_attributes = name
|
@@ -105,19 +165,40 @@ module JSONMapper
|
|
105
165
|
|
106
166
|
# The remaining first argument must be a valid data type
|
107
167
|
if args[0].is_a?(Class)
|
108
|
-
type = args
|
168
|
+
type = args.delete_at(0)
|
109
169
|
else
|
110
170
|
raise ArgumentError.new("Invalid type parameter specified")
|
111
171
|
end
|
112
172
|
|
113
|
-
|
173
|
+
# If we have anything remaining, and it's a hash, use it as our options
|
174
|
+
options = {}
|
175
|
+
if !args.empty? && args.first.is_a?(Hash)
|
176
|
+
options = args.delete_at(0)
|
177
|
+
end
|
178
|
+
|
179
|
+
return source_attributes, type, options
|
114
180
|
|
115
181
|
end
|
116
182
|
|
117
183
|
def is_mapped?(attribute, json)
|
118
184
|
|
185
|
+
# Just return true if this attribute is potentially self-referencing
|
186
|
+
return true if attribute.self_referential?
|
187
|
+
|
188
|
+
# Return false if our JSON isn't a hash or an array
|
189
|
+
return false unless json.is_a?(Hash) || json.is_a?(Array)
|
190
|
+
|
119
191
|
attribute.source_attributes.each do |source_attribute|
|
120
|
-
|
192
|
+
|
193
|
+
# If the source attribute is a hash, do a key/value lookup on the json data
|
194
|
+
if source_attribute.is_a?(Hash)
|
195
|
+
|
196
|
+
source_key = source_attribute.keys.first
|
197
|
+
if json.key?(source_key) && json[source_key].is_a?(Hash) && json[source_key].key?(source_attribute[source_key])
|
198
|
+
return true
|
199
|
+
end
|
200
|
+
|
201
|
+
elsif json.key?(source_attribute)
|
121
202
|
return true
|
122
203
|
end
|
123
204
|
end
|
@@ -127,11 +208,29 @@ module JSONMapper
|
|
127
208
|
|
128
209
|
def mapping_value(attribute, json)
|
129
210
|
|
211
|
+
# Return nil if our JSON isn't a hash or an array
|
212
|
+
return nil unless json.is_a?(Hash) || json.is_a?(Array)
|
213
|
+
|
130
214
|
attribute.source_attributes.each do |source_attribute|
|
131
|
-
|
215
|
+
|
216
|
+
# If the source attribute is a hash, do a key/value lookup on the json data
|
217
|
+
if source_attribute.is_a?(Hash)
|
218
|
+
|
219
|
+
source_key = source_attribute.keys.first
|
220
|
+
if json.key?(source_key) && json[source_key].key?(source_attribute[source_key])
|
221
|
+
return json[source_key][source_attribute[source_key]]
|
222
|
+
end
|
223
|
+
|
224
|
+
elsif json.key?(source_attribute)
|
132
225
|
return json[source_attribute]
|
133
226
|
end
|
227
|
+
|
134
228
|
end
|
229
|
+
|
230
|
+
# If no mapping could be found and this attribute is potentially
|
231
|
+
# self-referencing, return the current JSON data as the mapped value
|
232
|
+
return json_data if attribute.self_referential?
|
233
|
+
|
135
234
|
return nil
|
136
235
|
|
137
236
|
end
|
@@ -140,5 +239,6 @@ module JSONMapper
|
|
140
239
|
|
141
240
|
end
|
142
241
|
|
242
|
+
require "json_mapper/parser"
|
143
243
|
require "json_mapper/attribute"
|
144
244
|
require "json_mapper/attribute_list"
|
@@ -1,14 +1,13 @@
|
|
1
1
|
class Attribute
|
2
2
|
|
3
|
-
attr_accessor :name, :source_attributes, :type
|
3
|
+
attr_accessor :name, :source_attributes, :type, :options
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(name, source_attributes, type)
|
5
|
+
def initialize(name, source_attributes, type, options = {})
|
8
6
|
|
9
7
|
self.name = name
|
10
8
|
self.source_attributes = source_attributes.is_a?(Array) ? source_attributes : [ source_attributes ]
|
11
9
|
self.type = type
|
10
|
+
self.options = options
|
12
11
|
|
13
12
|
end
|
14
13
|
|
@@ -16,13 +15,32 @@ class Attribute
|
|
16
15
|
@method_name ||= self.name.to_s.tr("-", "_")
|
17
16
|
end
|
18
17
|
|
18
|
+
def self_referential?
|
19
|
+
self.source_attributes.include?("self")
|
20
|
+
end
|
21
|
+
|
19
22
|
def typecast(value)
|
20
23
|
|
21
24
|
return value if value.nil?
|
22
25
|
|
23
26
|
if self.type == String then return value.to_s
|
24
|
-
elsif self.type ==
|
25
|
-
|
27
|
+
elsif self.type == DelimitedString
|
28
|
+
self.options[:delimiter] ||= ","
|
29
|
+
return value.split(self.options[:delimiter])
|
30
|
+
elsif self.type == Integer
|
31
|
+
begin
|
32
|
+
return value.to_i
|
33
|
+
rescue
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
elsif self.type == Float
|
37
|
+
begin
|
38
|
+
return value.to_f
|
39
|
+
rescue
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
elsif self.type == Boolean then return %w(true t 1).include?(value.to_s.downcase)
|
43
|
+
elsif self.type == DateTime then return Date.parse(value.to_s)
|
26
44
|
else
|
27
45
|
|
28
46
|
# If our type is a JSONMapper instance, delegate the
|
@@ -1,12 +1,13 @@
|
|
1
1
|
class AttributeList < ::Array
|
2
2
|
|
3
|
-
attr_accessor :name, :source_attributes, :type
|
3
|
+
attr_accessor :name, :source_attributes, :type, :options
|
4
4
|
|
5
|
-
def initialize(name, source_attributes, type)
|
5
|
+
def initialize(name, source_attributes, type, options = {})
|
6
6
|
|
7
7
|
self.name = name
|
8
8
|
self.source_attributes = source_attributes.is_a?(Array) ? source_attributes : [ source_attributes ]
|
9
9
|
self.type = type
|
10
|
+
self.options = options
|
10
11
|
|
11
12
|
end
|
12
13
|
|
@@ -16,5 +17,9 @@ class AttributeList < ::Array
|
|
16
17
|
|
17
18
|
def typecast
|
18
19
|
end
|
20
|
+
|
21
|
+
def self_referential?
|
22
|
+
false # Attribute lists can't be self referential
|
23
|
+
end
|
19
24
|
|
20
25
|
end
|
data/test/fixtures/complex.json
CHANGED
data/test/fixtures/simple.json
CHANGED
data/test/json_mapper_test.rb
CHANGED
@@ -42,8 +42,10 @@ class JSONMapperTest < Test::Unit::TestCase
|
|
42
42
|
should "parse simple json structure into a ruby object" do
|
43
43
|
model = SimpleModel.parse(fixture_file("simple.json"))
|
44
44
|
model.id.should == 1
|
45
|
+
model.money.should == 125.50
|
45
46
|
model.title.should == "Simple JSON title"
|
46
47
|
model.boolean.should == true
|
48
|
+
model.datetime.should == Date.parse("2010-10-08 17:59:46")
|
47
49
|
end
|
48
50
|
|
49
51
|
should "assign value from different sources into an attribute" do
|
@@ -92,6 +94,7 @@ class JSONMapperTest < Test::Unit::TestCase
|
|
92
94
|
model.model_title.should == "Complex JSON title"
|
93
95
|
model.simple.id.should == 1
|
94
96
|
model.simple.title.should == "Simple JSON title"
|
97
|
+
model.nested_test.should == "foo bar"
|
95
98
|
model.simples.size.should == 2
|
96
99
|
model.simples.first.id.should == 1
|
97
100
|
model.simples.first.title.should == "Simple JSON title #1"
|
@@ -99,6 +102,53 @@ class JSONMapperTest < Test::Unit::TestCase
|
|
99
102
|
model.simples.last.title.should == "Simple JSON title #2"
|
100
103
|
end
|
101
104
|
|
105
|
+
should "be able to shift into a data structure to find the root element" do
|
106
|
+
|
107
|
+
json = '{ "foo": { "id": 1 } }'
|
108
|
+
model = SimpleModel.parse(json, :shift => :foo)
|
109
|
+
model.id.should == 1
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
should "be able to shift deep into a data structure to find the root element" do
|
114
|
+
|
115
|
+
json = '{ "foo": { "bar": { "id": 1 } } }'
|
116
|
+
model = SimpleModel.parse(json, :shift => [ :foo, :bar ])
|
117
|
+
model.id.should == 1
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
should "generate a collection of objects from an array" do
|
122
|
+
|
123
|
+
json = '[ { "id": 1 }, { "id": 2 } ]'
|
124
|
+
models = SimpleModel.parse_collection(json)
|
125
|
+
models.size.should == 2
|
126
|
+
|
127
|
+
models.first.id.should == 1
|
128
|
+
models.last.id.should == 2
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
should "be able to use a delimited string as an array" do
|
133
|
+
|
134
|
+
json = '{ "delimited": "foo,bar,baz" }'
|
135
|
+
model = ComplexModel.parse(json)
|
136
|
+
model.delimited.size.should == 3
|
137
|
+
model.delimited[0].should == "foo"
|
138
|
+
model.delimited[1].should == "bar"
|
139
|
+
model.delimited[2].should == "baz"
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
should "be able to map an object to self" do
|
144
|
+
|
145
|
+
json = '{ "id": 1 }'
|
146
|
+
model = ComplexModel.parse(json)
|
147
|
+
model.id.should == 1
|
148
|
+
model.self_referential.id.should == 1
|
149
|
+
|
150
|
+
end
|
151
|
+
|
102
152
|
end
|
103
153
|
|
104
154
|
end
|
data/test/support/models.rb
CHANGED
@@ -3,8 +3,10 @@ class SimpleModel
|
|
3
3
|
include JSONMapper
|
4
4
|
|
5
5
|
json_attribute :id, Integer
|
6
|
+
json_attribute :money, Float
|
6
7
|
json_attribute :title, String
|
7
8
|
json_attribute :boolean, Boolean
|
9
|
+
json_attribute :datetime, DateTime
|
8
10
|
|
9
11
|
end
|
10
12
|
|
@@ -14,8 +16,13 @@ class ComplexModel
|
|
14
16
|
|
15
17
|
json_attribute :id, [ :id, :attribute_id ], Integer
|
16
18
|
json_attribute :model_title, :title, String
|
19
|
+
json_attribute :datetime, DateTime
|
17
20
|
json_attribute :simple, SimpleModel
|
21
|
+
json_attribute :nested_test, { :nested => :test }, String
|
18
22
|
json_attributes :simples, SimpleModel
|
19
23
|
json_attributes :integers, Integer
|
24
|
+
json_attributes :delimited, DelimitedString, :delimiter => ","
|
25
|
+
|
26
|
+
json_attribute :self_referential, "self", SimpleModel
|
20
27
|
|
21
28
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_mapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Trond Arve Nordheim
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-08-
|
18
|
+
date: 2010-08-09 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -104,6 +104,7 @@ files:
|
|
104
104
|
- lib/json_mapper.rb
|
105
105
|
- lib/json_mapper/attribute.rb
|
106
106
|
- lib/json_mapper/attribute_list.rb
|
107
|
+
- lib/json_mapper/parser.rb
|
107
108
|
- test.rb
|
108
109
|
- test/fixtures/complex.json
|
109
110
|
- test/fixtures/simple.json
|