model_xml 1.0.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/.gitignore +1 -0
- data/README.md +104 -0
- data/lib/model_xml/block_parser.rb +34 -0
- data/lib/model_xml/generator.rb +116 -0
- data/lib/model_xml.rb +45 -0
- data/test/test_block_parser.rb +16 -0
- data/test/test_model_xml.rb +87 -0
- metadata +102 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.sw*
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
Introduction
|
2
|
+
============
|
3
|
+
|
4
|
+
model_xml is a small gem which helps in the conversion of ruby objects to xml. It is designed with ActiveRecord objects in mind, but should work with any Ruby object.
|
5
|
+
|
6
|
+
Installation
|
7
|
+
============
|
8
|
+
|
9
|
+
gem 'model_xml' in your gemfile, or gem install model_xml
|
10
|
+
|
11
|
+
If you are using Rails, just require model_xml any time after ActiveRecord has loaded - the bottom of your environment.rb file is fine. Otherwise, require model_xml, then include ModelXML in your object.
|
12
|
+
|
13
|
+
Usage
|
14
|
+
=====
|
15
|
+
|
16
|
+
Take a look at ActiveRecord's baked in to_xml method first. If it meets your requirements, obviously just use that. If not, read on.
|
17
|
+
|
18
|
+
The simplest usage method is just to declare the list of fields you want in your model's xml representation, like this:
|
19
|
+
|
20
|
+
class User < ActiveRecord::Base
|
21
|
+
model_xml :first_name, :last_name, :dob
|
22
|
+
end
|
23
|
+
|
24
|
+
Then user.to_xml gives:
|
25
|
+
|
26
|
+
<user>
|
27
|
+
<first_name>John</first_name>
|
28
|
+
<last_name>Jones</last_name>
|
29
|
+
<dob>1970-12-23</dob>
|
30
|
+
</user>
|
31
|
+
|
32
|
+
Note that (unlike ActiveRecords's to_xml) the field names can be any method in your object, not just database columns - eg
|
33
|
+
|
34
|
+
class User < ActiveRecord::Base
|
35
|
+
model_xml :full_name, :dob
|
36
|
+
|
37
|
+
def full_name
|
38
|
+
"#{first_name} {last+name}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
behaves as expected:
|
43
|
+
|
44
|
+
<user>
|
45
|
+
<full_name>John Jones</full_name>
|
46
|
+
<dob>1970-12-23</dob>
|
47
|
+
</user>
|
48
|
+
|
49
|
+
You can even declare a formatting proc on the fly, like this:
|
50
|
+
|
51
|
+
class User < ActiveRecord::Base
|
52
|
+
model_xml [:full_name, proc {|u| "#{u.first_name} #{u.last_name}"], :dob
|
53
|
+
end
|
54
|
+
|
55
|
+
which would give the same result.
|
56
|
+
|
57
|
+
For more complicated setups, you can use block notation like this:
|
58
|
+
|
59
|
+
class User < ActiveRecord::Base
|
60
|
+
model_xml do
|
61
|
+
full_name proc {|u| "#{u.first_name} #{u.last_name}"}
|
62
|
+
dob
|
63
|
+
password
|
64
|
+
last_logged_in
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Finally, you can declare named blocks using block notation like this:
|
69
|
+
|
70
|
+
class User < ActiveRecord::Base
|
71
|
+
model_xml :first_name, :last_name
|
72
|
+
model_xml :personal_details do
|
73
|
+
dob
|
74
|
+
password
|
75
|
+
last_logged_in
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
By default named blocks are excluded from the xml, so user.to_xml gives
|
80
|
+
|
81
|
+
<user>
|
82
|
+
<first_name>John</first_name>
|
83
|
+
<last_name>Jones</last_name>
|
84
|
+
</user>
|
85
|
+
|
86
|
+
but can be included explicitly - so user.to_xml(:personal_details => true) gives
|
87
|
+
|
88
|
+
<user>
|
89
|
+
<first_name>John</first_name>
|
90
|
+
<last_name>Jones</last_name>
|
91
|
+
<dob>1970-12-23</dob>
|
92
|
+
<password>foo</password>
|
93
|
+
<last_logged_in>2012-04-10</last_logged_in>
|
94
|
+
</user>
|
95
|
+
|
96
|
+
Source
|
97
|
+
======
|
98
|
+
|
99
|
+
Source is on github https://github.com/rob-anderson/model_xml
|
100
|
+
|
101
|
+
Bugs
|
102
|
+
====
|
103
|
+
|
104
|
+
Send bugs or comments to me rob.anderson@paymentcardsolutions.co.uk
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ModelXML
|
2
|
+
class BlockParser
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def parse &block
|
7
|
+
raise "block required" unless block_given?
|
8
|
+
parser = self.new
|
9
|
+
parser.instance_eval &block
|
10
|
+
parser.field_set
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :field_set
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@field_set = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing *args
|
22
|
+
|
23
|
+
# if the method is called without arguments, add it as a member of the field set
|
24
|
+
if args.map(&:class) == [Symbol]
|
25
|
+
@field_set << args[0]
|
26
|
+
|
27
|
+
# otherwise add it as an array
|
28
|
+
else
|
29
|
+
@field_set << args
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'builder'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'model_xml/block_parser'
|
5
|
+
|
6
|
+
module ModelXML
|
7
|
+
class Generator
|
8
|
+
|
9
|
+
attr_reader :field_sets
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@field_sets ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
# three types of argument list are expected:
|
16
|
+
#
|
17
|
+
# 1. an unnamed fieldset expressed as any number of symbols eg
|
18
|
+
# model_xml :id, :first_name, :last_name, [:embossed_name, :getter => proc {|u| "#{u.first_name} #{u.last_name}".upcase}]
|
19
|
+
#
|
20
|
+
# 2. a named fieldset in block notation eg
|
21
|
+
# model_xml :personal_data do
|
22
|
+
# :first_name
|
23
|
+
# :last_name
|
24
|
+
# :embossed_name proc {|u| "#{u.first_name} #{u.last_name}".upcase}
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# 3. an unnamed fieldset in block notation eg
|
28
|
+
# model_xml do
|
29
|
+
# :first_name
|
30
|
+
# last_name
|
31
|
+
# end
|
32
|
+
|
33
|
+
def add_field_set *args, &block
|
34
|
+
|
35
|
+
# if the argument list is empty and we have a block, it is an unnamed block
|
36
|
+
if args.empty? && block_given?
|
37
|
+
@field_sets << ModelXML::BlockParser.parse(&block)
|
38
|
+
|
39
|
+
# otherwise if the argument list is a symbol and we have a block, it is a named block
|
40
|
+
elsif args.map(&:class) == [Symbol] && block_given?
|
41
|
+
name = args[0]
|
42
|
+
@field_sets << {name => ModelXML::BlockParser.parse(&block)}
|
43
|
+
|
44
|
+
# otherwise assume it is a simple fieldset expressed as symbols
|
45
|
+
else
|
46
|
+
@field_sets << args
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# apply any options to the default field sets to generate a single array of fields
|
51
|
+
def generate_field_list options={}
|
52
|
+
|
53
|
+
field_list = []
|
54
|
+
@field_sets.each do |field_set|
|
55
|
+
|
56
|
+
# if the field set is a hash then it is a hash of conditional field sets
|
57
|
+
# which should only be included if the conditions are present as options
|
58
|
+
if field_set.is_a?(Hash)
|
59
|
+
field_set.each do |k, v|
|
60
|
+
field_list += v if options[k]
|
61
|
+
end
|
62
|
+
|
63
|
+
# otherwise, always include
|
64
|
+
else
|
65
|
+
field_list += field_set
|
66
|
+
end
|
67
|
+
end
|
68
|
+
field_list
|
69
|
+
end
|
70
|
+
|
71
|
+
def generate_xml! object, options={}
|
72
|
+
|
73
|
+
field_list = generate_field_list(options)
|
74
|
+
|
75
|
+
xml = Builder::XmlMarkup.new
|
76
|
+
unless options[:skip_instruct]
|
77
|
+
xml.instruct!
|
78
|
+
end
|
79
|
+
|
80
|
+
xml.tag! object.class.to_s.downcase do
|
81
|
+
|
82
|
+
field_list.each do |field|
|
83
|
+
|
84
|
+
# if the field is a symbol, treat it as the field name and the getter method
|
85
|
+
if field.is_a?(Symbol)
|
86
|
+
tag = field
|
87
|
+
content = object.send(field)
|
88
|
+
|
89
|
+
# if the field is an array of a symbol followed by a proc, assume the symbol is the field name and the proc is the getter
|
90
|
+
elsif field.is_a?(Array) && field.map(&:class) == [Symbol, Proc]
|
91
|
+
tag = field[0]
|
92
|
+
content = field[1].call(object)
|
93
|
+
|
94
|
+
# otherwise we have garbage
|
95
|
+
else
|
96
|
+
raise "ModelXML unable to parse #{field.inspect}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# if the content implements a to_xml method which returns a non-nil value, insert as raw xml
|
100
|
+
if content.respond_to?(:to_xml) && inline_content = content.to_xml(:skip_instruct => true, :skip_types => true)
|
101
|
+
xml << inline_content
|
102
|
+
|
103
|
+
# otherwise create the tag normally
|
104
|
+
else
|
105
|
+
xml.tag! tag, content
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# builder will screw up the indentation of embedded xml objects, so use nokogiri to format it nicely
|
111
|
+
output = xml.target!.gsub("\n","").gsub(" ","")
|
112
|
+
Nokogiri.parse(output).to_s
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/model_xml.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
$:.unshift File.dirname __FILE__
|
2
|
+
require 'model_xml/generator'
|
3
|
+
|
4
|
+
module ModelXML
|
5
|
+
|
6
|
+
def self.included base
|
7
|
+
|
8
|
+
base.instance_eval do
|
9
|
+
|
10
|
+
def model_xml *args, &block
|
11
|
+
|
12
|
+
@model_xml_generator ||= ModelXML::Generator.new
|
13
|
+
if block_given?
|
14
|
+
@model_xml_generator.add_field_set(*args, &block)
|
15
|
+
else
|
16
|
+
@model_xml_generator.add_field_set *args
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
# this is probably only ever required for testing
|
22
|
+
def model_xml_reset!
|
23
|
+
@model_xml_generator = ModelXML::Generator.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def model_xml_generator
|
27
|
+
@model_xml_generator
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_xml options={}
|
34
|
+
|
35
|
+
# if no generator is defined, pass straight through to the parent to_xml method, which may or may not exist
|
36
|
+
if generator = self.class.model_xml_generator
|
37
|
+
generator.generate_xml! self, options
|
38
|
+
else
|
39
|
+
super options
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
ActiveRecord::Base.send :include, ModelXML if defined?(ActiveRecord::Base)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
$:.unshift File.dirname(__FILE__) + "/../lib/"
|
3
|
+
require 'model_xml/block_parser'
|
4
|
+
|
5
|
+
class BlockParserTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_simple_parse
|
8
|
+
block = Proc.new do
|
9
|
+
foo
|
10
|
+
bar 3, 4
|
11
|
+
end
|
12
|
+
assert_equal [:foo, [:bar, 3, 4]], ModelXML::BlockParser.parse(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'ostruct'
|
3
|
+
$:.unshift File.dirname(__FILE__) + "/../lib/"
|
4
|
+
require 'model_xml'
|
5
|
+
|
6
|
+
class TestStruct < OpenStruct
|
7
|
+
include ModelXML
|
8
|
+
end
|
9
|
+
|
10
|
+
class ModelXMLTest < Test::Unit::TestCase
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@t = TestStruct.new :foo => 1, :bar => 2
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_class_simple
|
17
|
+
TestStruct.instance_eval do
|
18
|
+
model_xml_reset!
|
19
|
+
model_xml :foo
|
20
|
+
end
|
21
|
+
|
22
|
+
assert_equal [[:foo]], TestStruct.model_xml_generator.field_sets
|
23
|
+
|
24
|
+
res = '<?xml version="1.0" encoding="UTF-8"?>
|
25
|
+
<teststruct>
|
26
|
+
<foo>1</foo>
|
27
|
+
</teststruct>
|
28
|
+
'
|
29
|
+
assert_equal res, @t.to_xml
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_block_notation
|
33
|
+
TestStruct.instance_eval do
|
34
|
+
model_xml_reset!
|
35
|
+
model_xml do
|
36
|
+
foo
|
37
|
+
bar
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
assert_equal [[:foo, :bar]], TestStruct.model_xml_generator.field_sets
|
42
|
+
|
43
|
+
res = '<?xml version="1.0" encoding="UTF-8"?>
|
44
|
+
<teststruct>
|
45
|
+
<foo>1</foo>
|
46
|
+
<bar>2</bar>
|
47
|
+
</teststruct>
|
48
|
+
'
|
49
|
+
assert_equal res, @t.to_xml
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_inline_procs
|
53
|
+
TestStruct.instance_eval do
|
54
|
+
model_xml_reset!
|
55
|
+
model_xml do
|
56
|
+
foo
|
57
|
+
bar
|
58
|
+
foobar Proc.new {|obj| obj.foo + obj.bar}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
res = '<?xml version="1.0" encoding="UTF-8"?>
|
63
|
+
<teststruct>
|
64
|
+
<foo>1</foo>
|
65
|
+
<bar>2</bar>
|
66
|
+
<foobar>3</foobar>
|
67
|
+
</teststruct>
|
68
|
+
'
|
69
|
+
assert_equal res, @t.to_xml
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_skip_instruct
|
73
|
+
TestStruct.instance_eval do
|
74
|
+
model_xml_reset!
|
75
|
+
model_xml :foo, :bar
|
76
|
+
end
|
77
|
+
|
78
|
+
res = '<?xml version="1.0"?>
|
79
|
+
<teststruct>
|
80
|
+
<foo>1</foo>
|
81
|
+
<bar>2</bar>
|
82
|
+
</teststruct>
|
83
|
+
'
|
84
|
+
assert_equal res, @t.to_xml(:skip_instruct => true)
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: model_xml
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Rob Anderson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-04-10 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: builder
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 15
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 1
|
32
|
+
- 2
|
33
|
+
version: 2.1.2
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: nokogiri
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 4
|
48
|
+
- 2
|
49
|
+
version: 1.4.2
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
description: Simple replacement for ActiveRecord's default to_xml
|
53
|
+
email: rob.anderson@paymentcardsolutions.co.uk
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files: []
|
59
|
+
|
60
|
+
files:
|
61
|
+
- .gitignore
|
62
|
+
- README.md
|
63
|
+
- lib/model_xml.rb
|
64
|
+
- lib/model_xml/block_parser.rb
|
65
|
+
- lib/model_xml/generator.rb
|
66
|
+
- test/test_block_parser.rb
|
67
|
+
- test/test_model_xml.rb
|
68
|
+
homepage:
|
69
|
+
licenses: []
|
70
|
+
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
version: "0"
|
94
|
+
requirements: []
|
95
|
+
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.8.16
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: Ruby object to xml converter
|
101
|
+
test_files: []
|
102
|
+
|