json_mapper 0.1.1 → 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.
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