plist4r 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,17 +1,128 @@
1
1
  = plist4r
2
2
 
3
- Description goes here.
3
+ Welcome to Plist4r, a ruby library for reading and writing plist files.
4
4
 
5
- == Note on Patches/Pull Requests
5
+ Current project status: in alpha
6
+
7
+ == Installation
8
+
9
+ gem install plist4r
10
+
11
+ == Quick Start
12
+
13
+ Plist4r::Config.default_dir "/Library/LaunchDaemons"
14
+ filename = "com.github.myservice.plist"
15
+ p = Plist4r.open(filename)
16
+
17
+ p.plist_type
18
+ # => :launchd
19
+
20
+ p.file_format
21
+ # => :xml
22
+
23
+ p.<< do
24
+ ProgramArguments ["/usr/local/bin/myservice"]
25
+ end
26
+
27
+ p.edit do
28
+ WatchPaths ["/var/db/myservice"]
29
+ end
30
+
31
+ p.save
32
+
33
+ == Plist 'Types'
34
+
35
+ A Plist type can be one of `%w[info launchd]`, and is the data type for the whole plist file. A plist data type can provide convenience methods to set Type-specific plist structures. For example "Sockets" in a launchd plist.
36
+
37
+ Plist types are also useful to disallow keys which arent recognized or supported by that format. Flicking `:unsupported_keys` the Plist4r config will enable this:
38
+
39
+ ::Plist4r::Config[:unsupported_keys] = false
40
+
41
+ Or individually, per plist object with
42
+
43
+ plist.unsupported_keys false
44
+
45
+ Default is true, which allows editing of any plist keys. We think thats a good choice, since unsupported keys can already be present in existing plist files, which are loadable by Plist4r.
46
+
47
+ == Plist4r Backends
48
+
49
+ There are now a number of ruby libraries which can read / write plist files. The aim of plist4r is to utilize the individual best features from all of those libraries, as a series of "backends". And hide those behind a "frontend" that is easy to work with.
50
+
51
+ Backends often only need to be a single ruby file, which implements the Plist4r API methods and calls out to other (existing) ruby code. No single backend has to provide the whole API. Instead, Plist4r simply iterates over all of the backends it knows about, and then calls the first backend that can responds to the API method.
52
+
53
+ There are just 6 supported API methods
54
+
55
+ ApiMethods = %w[from_string to_xml to_binary to_next_step open save]
56
+
57
+ And (as above) the 3 supported Plist file formats are
58
+
59
+ FileFormats = %w[binary xml next_step]
60
+
61
+ We believe thats allright for most uses, and decided to include `next_step` for completeness. `NextStep` is also known by other names such as `OpenStep` and (more updated version) `GNU Step`. For example the apple `defaults` command on Mac OS-X will still return `NextStep` formatted plist data.
62
+
63
+ == More Examples
64
+
65
+ module ::Plist4r::Backend::MyPlistReaderWriter
66
+ # implement some plist4r api calls here
67
+ end
68
+
69
+ # append my backend to the end of the list
70
+ Plist4r::Config[:backends] << :my_plist_reader_writer
71
+
72
+ # or to the front of the list (executes first)
73
+ Plist4r::Config[:backends].insert 0 :my_plist_reader_writer
74
+
75
+ # The default directory to load / save files from
76
+ Plist4r::Config.default_path "/Library/Cars"
77
+
78
+ car = Plist4r.new("car.plist")
79
+
80
+ car.load
81
+
82
+ car.file_format :binary
83
+ # car.plist_type :car # not implemented *yet*
84
+
85
+ car.save
86
+
87
+ car.<< do
88
+ road_legal true
89
+ brake_light_color "red"
90
+ end
91
+
92
+ car.save_as("car2.plist", :binary => true)
93
+
94
+ car.<< do
95
+ eyes "blue"
96
+ end
97
+ # => Exception, invalid plist key name "Eyes"
98
+
99
+ car.<< do
100
+ tyres "Pirelli"
101
+ end
102
+
103
+ car.to_xml
104
+ # => xml string
105
+
106
+ == Remaining Work
107
+
108
+ Plist4r is currently alpha - quality software. Yet to be completed...
109
+
110
+ * Regression Tests (rspec)
111
+ * Test harness for the backends
112
+ * Testing of the individual backends
113
+ * Tests for Plist Types
114
+ * RDoc Documentation
115
+ * Script for embedding / inlining Plist4r into Homebrew
116
+ * A Plist Type for Info.plist
117
+ * Command line interface (hopefully mixlib-cli)
118
+
119
+ == Notes on Patches/Pull Requests
6
120
 
7
- * Fork the project.
121
+ * Fork the project, and create a topic branch as per {these instructions}[http://wiki.opscode.com/display/opscode/Working+with+Git].
8
122
  * Make your feature addition or bug fix.
9
- * Add tests for it. This is important so I don't break it in a
10
- future version unintentionally.
11
- * Commit, do not mess with rakefile, version, or history.
12
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
- * Send me a pull request. Bonus points for topic branches.
123
+ * Include documentation for it.
124
+ * Include a regression test for it. So I don't break it in a future version unintentionally.
14
125
 
15
126
  == Copyright
16
127
 
17
- Copyright (c) 2010 dreamcat4. See LICENSE for details.
128
+ Copyright (c) 2010 Dreamcat4. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
data/lib/plist4r.rb CHANGED
@@ -0,0 +1,54 @@
1
+
2
+ $:.unshift(File.dirname(__FILE__)) unless
3
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
4
+
5
+ require 'plist4r/plist'
6
+
7
+ module Plist4r
8
+ class << self
9
+ def new *args, &blk
10
+ puts args.inspect
11
+ return Plist.new *args, &blk
12
+ end
13
+
14
+ def open filename, *args, &blk
15
+ puts args.inspect
16
+ return Plist.new filename, *args, &blk
17
+ end
18
+
19
+ def string_detect_format string
20
+ s.strip!
21
+ case s[0,1]
22
+ when "{","("
23
+ :next_step
24
+ when "b"
25
+ if s =~ /^bplist/
26
+ :binary
27
+ else
28
+ nil
29
+ end
30
+ when "<"
31
+ if s =~ /^\<\?xml/ && s =~ /\<\!DOCTYPE plist/
32
+ :xml
33
+ else
34
+ nil
35
+ end
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ def file_detect_format filename
42
+ string_detect_format File.read(filename)
43
+ end
44
+ end
45
+ end
46
+
47
+ class String
48
+ def to_plist
49
+ return ::Plist4r.new(:from_string => self)
50
+ end
51
+ end
52
+
53
+
54
+
@@ -0,0 +1,52 @@
1
+
2
+ require 'plist4r/config'
3
+ require 'plist4r/mixin/ordered_hash'
4
+
5
+ class Plist4r::Backend
6
+ ApiMethods = %w[from_string to_xml to_binary to_next_step open save]
7
+
8
+ def initialize plist, *args, &blk
9
+ @plist = plist
10
+ @backends = Config[:backends].collect do |b|
11
+ case b
12
+ when Module
13
+ b
14
+ when Symbol, String
15
+ eval "::Plist4r::Backend::#{b.to_s.camelcase}"
16
+ else
17
+ raise "Backend #{b.inspect} is of unsupported type: #{b.class}"
18
+ end
19
+ end
20
+ end
21
+
22
+ def call method_sym, *args, &blk
23
+ raise "Unsupported api call #{method_sym.inspect}" unless ApiMethods.include? method_sym.to_s
24
+ exceptions = []
25
+ @backends.each do |backend|
26
+ if backend.respond_to? method_sym
27
+ begin
28
+ return backend.send(method_sym, @plist, *args, &blk)
29
+ rescue LoadError
30
+ exceptions << $!
31
+ rescue
32
+ exceptions << $!
33
+ end
34
+ end
35
+ if Config[:raise_any_failure] && exceptions.first
36
+ raise exceptions.first
37
+ end
38
+ end
39
+ if exceptions.empty?
40
+ raise "Plist4r: No backend found to handle method #{method_sym.inspect}. Could not execute method #{method_sym.inspect} on plist #{@plist.inspect}"
41
+ else
42
+ $stderr.puts "Couldn't execute method #{method_sym.inspect} on plist #{@plist.inspect}."
43
+ exceptions.each do |e|
44
+ $stderr.puts e.inspect
45
+ end
46
+ raise exceptions.first
47
+ end
48
+ end
49
+ end
50
+
51
+
52
+
@@ -0,0 +1,69 @@
1
+
2
+ require 'plist4r/backend'
3
+
4
+ module Plist4r::Backend::Example
5
+ class << self
6
+ def from_string plist, string
7
+ plist_format = Plist4r.string_detect_format string
8
+ unless [:supported_fmt1,:supported_fmt2].include? plist_format
9
+ raise "#{self} - cant convert string of format #{plist_format}"
10
+ end
11
+ hash = ::ActiveSupport::OrderedHash.new
12
+ # import / convert plist data into ruby ordered hash
13
+ plist.import_hash hash
14
+ plist.file_format = plist_format
15
+ return plist
16
+ end
17
+
18
+ def to_xml plist
19
+ hash = plist.to_hash
20
+ xml_string = "Convert the plists's nested ruby hash into xml here"
21
+ return xml_string
22
+ end
23
+
24
+ def to_binary
25
+ hash = plist.to_hash
26
+ binary_string = "Convert the plists's nested ruby hash into binary format here"
27
+ return binary_string
28
+ end
29
+
30
+ def to_next_step
31
+ hash = plist.to_hash
32
+ next_step_string = "Convert the plists's nested ruby hash into next_step format here"
33
+ return next_step_string
34
+ end
35
+
36
+ def open plist
37
+ filename = plist.filename_path
38
+ file_format = Plist4r.file_detect_format filename
39
+ unless [:supported_fmt1,:supported_fmt2].include? file_format
40
+ raise "#{self} - cant load file of format #{file_format}"
41
+ end
42
+ plist_file_as_string = File.read(filename)
43
+ hash = ::ActiveSupport::OrderedHash.new
44
+ # import / convert plist data into ruby ordered hash
45
+ plist.import_hash hash
46
+ plist.file_format = file_format
47
+ return plist
48
+ end
49
+
50
+ def save plist
51
+ filename = plist.filename_path
52
+ file_format = plist.file_format || Config[:default_format]
53
+ unless [:xml,:binary].include? file_format
54
+ raise "#{self} - cant save file of format #{file_format}"
55
+ end
56
+ hash = plist.to_hash
57
+ output_string = String.new
58
+ # convert plist's @hash representation into an output_string,
59
+ # and formatted to your supported plist file_format(s)
60
+ File.open(filename,'w') do |out|
61
+ out << output_string
62
+ end
63
+ return true
64
+ end
65
+ end
66
+ end
67
+
68
+
69
+
@@ -0,0 +1,74 @@
1
+
2
+ require 'plist4r/backend'
3
+
4
+ module Plist4r::Backend::HamlXmlWriter
5
+ class << self
6
+ def to_xml_haml
7
+ @to_xml_haml ||= <<-'EOC'
8
+ !!! XML UTF-8
9
+ <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
10
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
11
+ %plist{ :version => '1.0' }
12
+ %dict
13
+ - p = Proc.new do |data, block|
14
+ - data.each_pair do |k,v|
15
+ - raise "Invalid input. Hash: #{data.inspect} can only contain keys of type Symbol or String. Found key #{k.inspect}, of type: \"#{k.class}\"" unless [Symbol,String].include? k.class
16
+ - case v
17
+ - when TrueClass, FalseClass
18
+ %key #{k}
19
+ <#{v}/>
20
+ - when String
21
+ %key #{k}
22
+ %string #{v}
23
+ - when Fixnum
24
+ %key #{k}
25
+ %integer #{v}
26
+ - when Array
27
+ %key #{k}
28
+ %array
29
+ - v.compact.each do |e|
30
+ - case e
31
+ - when TrueClass, FalseClass
32
+ <#{v}/>
33
+ - when String
34
+ %string #{e}
35
+ - when Fixnum
36
+ %integer #{v}
37
+ - when Hash
38
+ %dict
39
+ - tab_up ; block.call(e, block) ; tab_down
40
+ - else
41
+ - raise "Invalid input. Array: #{v.inspect} can only contain elements of type String (<string>) or Hash (<dict>). Found element: #{e.inspect} of type: \"#{e.class}\""
42
+ - when Hash
43
+ %key #{k}
44
+ %dict
45
+ - tab_up ; block.call(v, block) ; tab_down
46
+ - else
47
+ - raise "Invalid input. Hash: #{data.inspect} can only contain values of type true, false, String (<string>) Fixnum (<integer>) Array (<array>) or Hash (<dict>). Found value: #{v.inspect} of type: \"#{v.class}\""
48
+ - p.call( @hash, p)
49
+ EOC
50
+ end
51
+
52
+ def to_xml plist
53
+ require 'haml'
54
+ # engine = Haml::Engine.new File.read("launchd_plist.haml")
55
+ engine = Haml::Engine.new to_xml_haml
56
+ rendered_xml_output = engine.render self
57
+ File.open(@filename,'w') do |o|
58
+ o << rendered_xml_output
59
+ end
60
+ end
61
+
62
+ def save plist
63
+ file_format = plist.file_format || Config[:default_format]
64
+ raise "#{self} - cant save file format #{file_format}" unless file_format == :xml
65
+
66
+ hash = plist.to_hash
67
+ filename = plist.filename_path
68
+ File.open(filename,'w') do |out|
69
+ out << to_xml plist
70
+ end
71
+ end
72
+ end
73
+ end
74
+
@@ -0,0 +1,80 @@
1
+
2
+ require 'plist4r/backend'
3
+
4
+ module Plist4r::Backend::Libxml4rXmlReader
5
+ class << self
6
+ def tree_hash n
7
+ hash = ::ActiveSupport::OrderedHash.new
8
+ n_xml_keys = n.nodes["key"]
9
+ n_xml_keys.each do |n|
10
+ k = n.inner_xml
11
+ vnode = n.next
12
+ case vnode.name
13
+ when "true", "false"
14
+ hash[k] = eval(vnode.name)
15
+ when "string"
16
+ hash[k] = vnode.inner_xml
17
+ when "integer"
18
+ hash[k] = vnode.inner_xml.to_i
19
+ when "array"
20
+ hash[k] = tree_array(vnode)
21
+ when "dict"
22
+ hash[k] = tree_hash(vnode)
23
+ else
24
+ raise "Unsupported / not recognized plist key: #{vnode.name}"
25
+ end
26
+ end
27
+ return hash
28
+ end
29
+
30
+ def tree_array n
31
+ array = []
32
+ n.children.each do |node|
33
+ case node.name
34
+ when "true", "false"
35
+ array << eval(node.name)
36
+ when "string"
37
+ array << node.inner_xml
38
+ when "integer"
39
+ array << node.inner_xml.to_i
40
+ when "array"
41
+ array << tree_array(node)
42
+ when "dict"
43
+ array << tree_hash(node)
44
+ else
45
+ raise "Unsupported / not recognized plist key: #{vnode.name}"
46
+ end
47
+ end
48
+ return array
49
+ end
50
+
51
+ def parse_plist_xml string
52
+ require 'libxml4r'
53
+ ::LibXML::XML.default_keep_blanks = false
54
+ doc = string.to_xmldoc
55
+ doc.strip!
56
+ root = doc.node["/plist/dict"]
57
+ ordered_hash = tree_hash root
58
+ end
59
+
60
+ def from_string plist, string
61
+ plist_format = Plist4r.string_detect_format string
62
+ raise "#{self} - cant convert string of format #{plist_format}" unless plist_format == :xml
63
+
64
+ hash = parse_plist_xml string
65
+ plist.import_hash hash
66
+ plist.file_format = file_format
67
+ return plist
68
+ end
69
+
70
+ def open plist
71
+ filename = plist.filename_path
72
+ file_format = Plist4r.file_detect_format filename
73
+ raise "#{self} - cant load file of format #{file_format}" unless file_format == :xml
74
+
75
+ return from_string plist, File.read(filename)
76
+ end
77
+ end
78
+ end
79
+
80
+