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 CHANGED
@@ -1 +1 @@
1
- 0.1.1
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.1.1"
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-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 parse(data)
42
+ def json_data
43
+ @json_data[to_s] || []
44
+ end
41
45
 
42
- # Parse the data into a hash
43
- json = JSON.parse(data, { :symbolize_names => true })
46
+ def parse(data, options = {})
44
47
 
45
- # Parse the JSON data structure
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
- instance.send("#{attribute.name}") << build_attribute(attribute.name, attribute.type).typecast(v)
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 build_attribute(name, type)
85
- Attribute.new(name, name, type)
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[0]
168
+ type = args.delete_at(0)
109
169
  else
110
170
  raise ArgumentError.new("Invalid type parameter specified")
111
171
  end
112
172
 
113
- return source_attributes, type
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
- if json.key?(source_attribute)
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
- if json.key?(source_attribute)
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
- Types = [ String, Integer, Boolean ]
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 == Integer then return value.to_i
25
- elsif self.type == Boolean then return value.to_s == "true"
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
@@ -0,0 +1,11 @@
1
+ module JSONMapper
2
+ class Parser
3
+
4
+ # Parse the JSON string into a Hash
5
+ def self.parse(data)
6
+ return nil if data.nil? || data == ""
7
+ JSON.parse(data, { :symbolize_names => true })
8
+ end
9
+
10
+ end
11
+ end
@@ -6,6 +6,9 @@
6
6
  "title": "Simple JSON title",
7
7
  "boolean": true
8
8
  },
9
+ "nested": {
10
+ "test": "foo bar"
11
+ },
9
12
  "simples": [
10
13
  {
11
14
  "id": 1,
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "id": 1,
3
+ "money": 125.50,
3
4
  "title": "Simple JSON title",
4
- "boolean": true
5
+ "boolean": true,
6
+ "datetime": "2010-10-08 17:59:46"
5
7
  }
@@ -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
@@ -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: 25
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
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-08 00:00:00 +02:00
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