mhc 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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/COPYRIGHT +28 -0
  6. data/Gemfile +8 -0
  7. data/README.org +209 -0
  8. data/Rakefile +13 -0
  9. data/bin/mhc +312 -0
  10. data/emacs/Cask +25 -0
  11. data/emacs/Makefile +58 -0
  12. data/emacs/mhc-calendar.el +1723 -0
  13. data/emacs/mhc-calfw.el +135 -0
  14. data/emacs/mhc-compat.el +90 -0
  15. data/emacs/mhc-date.el +642 -0
  16. data/emacs/mhc-day.el +149 -0
  17. data/emacs/mhc-db.el +158 -0
  18. data/emacs/mhc-draft.el +211 -0
  19. data/emacs/mhc-e21.el +167 -0
  20. data/emacs/mhc-face.el +236 -0
  21. data/emacs/mhc-file.el +224 -0
  22. data/emacs/mhc-guess.el +648 -0
  23. data/emacs/mhc-header.el +176 -0
  24. data/emacs/mhc-logic.el +563 -0
  25. data/emacs/mhc-message.el +130 -0
  26. data/emacs/mhc-minibuf.el +466 -0
  27. data/emacs/mhc-misc.el +248 -0
  28. data/emacs/mhc-mua.el +260 -0
  29. data/emacs/mhc-parse.el +286 -0
  30. data/emacs/mhc-process.el +35 -0
  31. data/emacs/mhc-ps.el +1174 -0
  32. data/emacs/mhc-record.el +201 -0
  33. data/emacs/mhc-schedule.el +202 -0
  34. data/emacs/mhc-summary.el +763 -0
  35. data/emacs/mhc-sync.el +158 -0
  36. data/emacs/mhc-vars.el +149 -0
  37. data/emacs/mhc.el +1114 -0
  38. data/icons/Anniversary.xbm +6 -0
  39. data/icons/Anniversary.xpm +27 -0
  40. data/icons/Birthday.xbm +6 -0
  41. data/icons/Birthday.xpm +25 -0
  42. data/icons/Business.xbm +6 -0
  43. data/icons/Business.xpm +24 -0
  44. data/icons/CheckBox.xbm +6 -0
  45. data/icons/CheckBox.xpm +24 -0
  46. data/icons/CheckedBox.xbm +6 -0
  47. data/icons/CheckedBox.xpm +25 -0
  48. data/icons/Conflict.xbm +6 -0
  49. data/icons/Conflict.xpm +22 -0
  50. data/icons/Date.xbm +6 -0
  51. data/icons/Date.xpm +29 -0
  52. data/icons/Holiday.xbm +6 -0
  53. data/icons/Holiday.xpm +25 -0
  54. data/icons/Link.xbm +6 -0
  55. data/icons/Link.xpm +25 -0
  56. data/icons/Other.xbm +6 -0
  57. data/icons/Other.xpm +28 -0
  58. data/icons/Party.xbm +6 -0
  59. data/icons/Party.xpm +23 -0
  60. data/icons/Private.xbm +6 -0
  61. data/icons/Private.xpm +26 -0
  62. data/icons/Recurrence.xbm +6 -0
  63. data/icons/Recurrence.xpm +98 -0
  64. data/icons/Vacation.xbm +6 -0
  65. data/icons/Vacation.xpm +26 -0
  66. data/lib/mhc.rb +45 -0
  67. data/lib/mhc/builder.rb +64 -0
  68. data/lib/mhc/caldav.rb +304 -0
  69. data/lib/mhc/calendar.rb +106 -0
  70. data/lib/mhc/command.rb +13 -0
  71. data/lib/mhc/command/cache.rb +14 -0
  72. data/lib/mhc/command/completions.rb +108 -0
  73. data/lib/mhc/command/init.rb +133 -0
  74. data/lib/mhc/command/scan.rb +33 -0
  75. data/lib/mhc/command/sync.rb +22 -0
  76. data/lib/mhc/config.rb +229 -0
  77. data/lib/mhc/converter.rb +330 -0
  78. data/lib/mhc/datastore.rb +164 -0
  79. data/lib/mhc/date_enumerator.rb +274 -0
  80. data/lib/mhc/date_frame.rb +124 -0
  81. data/lib/mhc/date_helper.rb +49 -0
  82. data/lib/mhc/etag.rb +68 -0
  83. data/lib/mhc/event.rb +396 -0
  84. data/lib/mhc/formatter.rb +312 -0
  85. data/lib/mhc/logger.rb +94 -0
  86. data/lib/mhc/modifier.rb +149 -0
  87. data/lib/mhc/occurrence.rb +94 -0
  88. data/lib/mhc/occurrence_enumerator.rb +113 -0
  89. data/lib/mhc/property_value.rb +33 -0
  90. data/lib/mhc/property_value/date.rb +190 -0
  91. data/lib/mhc/property_value/integer.rb +15 -0
  92. data/lib/mhc/property_value/list.rb +41 -0
  93. data/lib/mhc/property_value/period.rb +49 -0
  94. data/lib/mhc/property_value/range.rb +100 -0
  95. data/lib/mhc/property_value/recurrence_condition.rb +272 -0
  96. data/lib/mhc/property_value/text.rb +11 -0
  97. data/lib/mhc/property_value/time.rb +45 -0
  98. data/lib/mhc/query.rb +210 -0
  99. data/lib/mhc/sync.rb +46 -0
  100. data/lib/mhc/sync/driver.rb +108 -0
  101. data/lib/mhc/sync/status.rb +70 -0
  102. data/lib/mhc/sync/status_manager.rb +142 -0
  103. data/lib/mhc/sync/strategy.rb +233 -0
  104. data/lib/mhc/sync/syncinfo.rb +98 -0
  105. data/lib/mhc/templates/config.yml.erb +142 -0
  106. data/lib/mhc/version.rb +4 -0
  107. data/lib/mhc/webdav.rb +319 -0
  108. data/mhc.gemspec +24 -0
  109. data/samples/DOT.mhc-config.yml +116 -0
  110. data/samples/japanese-holidays.mhcc +153 -0
  111. data/samples/mhc-completions.zsh +11 -0
  112. data/spec/mhc_spec.rb +682 -0
  113. data/spec/spec_helper.rb +9 -0
  114. data/xpm/close.xpm +18 -0
  115. data/xpm/delete.xpm +19 -0
  116. data/xpm/exit.xpm +18 -0
  117. data/xpm/month.xpm +18 -0
  118. data/xpm/next.xpm +18 -0
  119. data/xpm/next2.xpm +18 -0
  120. data/xpm/next_year.xpm +18 -0
  121. data/xpm/open.xpm +19 -0
  122. data/xpm/prev.xpm +18 -0
  123. data/xpm/prev2.xpm +18 -0
  124. data/xpm/prev_year.xpm +18 -0
  125. data/xpm/save.xpm +19 -0
  126. data/xpm/today.xpm +18 -0
  127. metadata +214 -0
@@ -0,0 +1,106 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Mhc
3
+ class Calendar
4
+
5
+ def initialize(datastore, modifiers = [], &default_scope)
6
+ @datastore = datastore
7
+ @modifiers = modifiers || []
8
+ @logger = @datastore.logger
9
+ @default_scope = default_scope
10
+ end
11
+
12
+ def find(uid:)
13
+ if event = @datastore.find_by_uid(uid)
14
+ decorate_event(event)
15
+ end
16
+ end
17
+
18
+ def events(date_range = nil, &scope_block)
19
+ occurrences(date_range, &scope_block).map(&:event).uniq
20
+ end
21
+
22
+ def occurrences(date_range, &scope_block)
23
+ ocs = []
24
+ @datastore.entries(date_range).each do |event|
25
+ event = decorate_event(event)
26
+ event.occurrences(range:date_range).each do |oc|
27
+ ocs << oc if in_scope?(oc, &scope_block)
28
+ end
29
+ end
30
+ return ocs.sort
31
+ end
32
+
33
+ ################################################################
34
+ ## for sync manager
35
+
36
+ def report_etags(uid = nil)
37
+ return find(uid) if uid
38
+ date_range = (Mhc::PropertyValue::Date.today - 90)..
39
+ (Mhc::PropertyValue::Date.today + 90)
40
+ events(date_range)
41
+ end
42
+
43
+ def get_with_etag(uid)
44
+ find(uid: uid)
45
+ end
46
+
47
+ def put_if_match(uid, ics_string, expected_etag)
48
+ STDERR.print "Mhc::Calendar#put_if_match(uid:#{uid}, expected_etag:#{expected_etag})..."
49
+ if ev = find(uid: uid) and ev.etag != expected_etag
50
+ STDERR.print "failed: etag not match #{ev.etag} != #{expected_etag}\n"
51
+ return nil
52
+ end
53
+ if expected_etag and (not ev)
54
+ STDERR.print "failed: etag not match #{expected_etag} != nil\n"
55
+ end
56
+ begin
57
+ ev = Mhc::Event.new_from_ics(ics_string)
58
+ @datastore.update(ev)
59
+ STDERR.print "succeeded #{ev.etag}\n"
60
+ return true
61
+ rescue Exception => e
62
+ STDERR.print "failed: #{e.to_s}\n"
63
+ STDERR.print "#{e.backtrace.first}\n" if $MHC_DEBUG
64
+ STDERR.print "#{ics_string}\n" if $MHC_DEBUG
65
+ return nil
66
+ end
67
+ end
68
+
69
+ def delete_if_match(uid, expected_etag)
70
+ STDERR.print "Mhc::Calendar#delete_if_match(uid:#{uid}, expected_etag:#{expected_etag})..."
71
+ unless ev = find(uid: uid)
72
+ STDERR.print "failed: uid #{uid} not found\n"
73
+ return nil
74
+ end
75
+ if expected_etag && ev.etag != expected_etag
76
+ STDERR.print "failed: etag not match #{ev.etag} != #{expected_etag}\n"
77
+ return nil
78
+ end
79
+ begin
80
+ @datastore.delete(ev)
81
+ STDERR.print "succeeded: #{ev.etag}\n"
82
+ return ev
83
+ rescue Exception => e
84
+ STDERR.print "failed: #{e.to_s}\n"
85
+ return nil
86
+ end
87
+ end
88
+
89
+ ################################################################
90
+ private
91
+ ################################################################
92
+
93
+ def decorate_event(event)
94
+ @modifiers.each do |deco|
95
+ event = deco.decorate(event)
96
+ end
97
+ return event
98
+ end
99
+
100
+ def in_scope?(oc, &scope_block)
101
+ (!@default_scope || @default_scope.call(oc)) &&
102
+ (!scope_block || scope_block.call(oc))
103
+ end
104
+
105
+ end # class Calendar
106
+ end # module Mhc
@@ -0,0 +1,13 @@
1
+ module Mhc
2
+ module Command
3
+
4
+ dir = File.dirname(__FILE__) + "/command"
5
+
6
+ autoload :Completions, "#{dir}/completions.rb"
7
+ autoload :Cache, "#{dir}/cache.rb"
8
+ autoload :Init, "#{dir}/init.rb"
9
+ autoload :Scan, "#{dir}/scan.rb"
10
+ autoload :Sync, "#{dir}/sync.rb"
11
+
12
+ end # module Command
13
+ end # module Mhc
@@ -0,0 +1,14 @@
1
+ module Mhc
2
+ module Command
3
+ class Cache
4
+
5
+ def initialize(calendar)
6
+ calendar.events.each do |event|
7
+ range = event.range
8
+ puts "#{range.min.to_mhc_string},#{range.max.to_mhc_string},#{event.uid},#{event.subject}"
9
+ end
10
+ end
11
+
12
+ end # class Cache
13
+ end # module Command
14
+ end # module Mhc
@@ -0,0 +1,108 @@
1
+ module Mhc
2
+ module Command
3
+ class Completions
4
+
5
+ def initialize(help, global_options, arguments, config = nil)
6
+ @help, @global_options, @arguments, @config = help, global_options, arguments, config
7
+ command_name = arguments.first
8
+
9
+ if command_name and help[command_name]
10
+ option_arguments(help, global_options, command_name)
11
+ else
12
+ command_arguments
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def command_arguments
19
+ print "_arguments\n"
20
+ print "1:Possible commands\\::"
21
+ print possible_commands + "\n"
22
+ end
23
+
24
+ def option_arguments(help, global_options, command_name)
25
+ command_arguments
26
+ print arguments(help, command_name)
27
+ print options(help[command_name], global_options)
28
+ end
29
+
30
+ # make normal argument completion setting from usage string such as: "scan REPOSITORY"
31
+ def arguments(help, command_name, position = 2)
32
+ str = ""
33
+ help[command_name].usage.split(/\s+/)[1..-1].each do |arg|
34
+ pos = position
35
+
36
+ if /^\[(.*)\]/ =~ arg
37
+ arg = $1
38
+ end
39
+
40
+ multi = ""
41
+ if /(.*)\.\.\.$/ =~ arg
42
+ arg = $1
43
+ pos = "*"
44
+ multi = ":"
45
+ end
46
+
47
+ str << "#{pos}:#{arg}\\::#{possible_values(arg)}#{multi}\n"
48
+ position += 1
49
+ end
50
+ return str
51
+ end
52
+
53
+ # make option argument completion setting from usage options help
54
+ def options(command_help, global_options, position = 2)
55
+ str = ""
56
+ options = command_help.options.merge(global_options)
57
+
58
+ options.each do |name, opt|
59
+ name = name.to_s.gsub("_", "-")
60
+
61
+ if opt.type == :boolean
62
+ str << "(--#{name})--#{name}[#{opt.description}]\n"
63
+ else
64
+ str << "(--#{name})--#{name}=-[#{opt.description}]:#{opt.banner}:#{possible_values_for_opt(opt)}\n"
65
+ end
66
+ end
67
+ return str
68
+ end
69
+
70
+ def possible_commands
71
+ str = "(("
72
+ @help.each_value do |cmd|
73
+ next if cmd.name == "completions"
74
+ str << " #{cmd.name}\\:"
75
+ str << cmd.description.gsub(/([()\s"';&|#\\])/, '\\\\\1')
76
+ end
77
+ str << "))"
78
+ end
79
+
80
+ def possible_values_for_opt(option)
81
+ return "(" + option.enum.join(" ") + ")" if option.enum
82
+ return possible_values(option.banner)
83
+ end
84
+
85
+ def possible_values(banner)
86
+ case banner
87
+ when /^CALENDAR/
88
+ "(" + @config.calendars.select{|cal| cal.type == "mhc"}.map(&:name).join(' ') + ")"
89
+ when /^SYNC_CHANNEL/
90
+ "(" + @config.sync_channels.map(&:name).join(' ') + ")"
91
+ when /^(FILE|CONF)/
92
+ "_files"
93
+ when /^DIR/
94
+ "_files -/"
95
+ when "COMMAND"
96
+ possible_commands
97
+ when "RANGE"
98
+ "(today tomorrow thismonth nextmonth)"
99
+ when /^NUM/
100
+ "_guard '[0-9]#' 'Number'"
101
+ else
102
+ ""
103
+ end
104
+ end
105
+
106
+ end # class Completions
107
+ end # module Command
108
+ end # module Mhc
@@ -0,0 +1,133 @@
1
+ module Mhc
2
+ module Command
3
+ class Init
4
+ SUB_DIRS = %w(draft inbox presets spool trash
5
+ status/cache status/log status/sync_channels)
6
+
7
+ TEMPLATE_DIR = File.expand_path("../../templates", __FILE__)
8
+
9
+ def initialize(top_dir, config_path, tzid = nil, template_dir = nil)
10
+ @shell = Thor.new
11
+ @status = {green: 0, yellow: 0, red: 0}
12
+ @config = {}
13
+
14
+ # guess teimzone
15
+ say "Guessing current local timezone ..."
16
+
17
+ if @config[:tzid] = find_current_tzid
18
+ say_status "ok", "guess timezone ... #{@config[:tzid]}", :green
19
+ tzid = find_current_tzid
20
+ else
21
+ say_status "failed", "guess timezone... Unknown", :red
22
+ end
23
+
24
+ # mkdir
25
+ say "Making directries under #{top_dir} ..."
26
+ SUB_DIRS.each do |ent|
27
+ mkdir_p(File.expand_path(ent, top_dir))
28
+ end
29
+
30
+ # make config file from tamplate
31
+ say "Copying config file(s) into #{config_path} ..."
32
+ src = File.expand_path("config.yml.erb", TEMPLATE_DIR)
33
+ dst = File.expand_path(config_path)
34
+ @config[:topdir] = top_dir
35
+ expand_template(src, dst)
36
+
37
+ say_status_report
38
+ end
39
+
40
+ private
41
+
42
+ def say(message, color = nil)
43
+ @shell.say(message, color)
44
+ end
45
+
46
+ def say_status(status, message, log_status = nil)
47
+ @status[log_status] += 1
48
+ @shell.say_status(status, message, log_status)
49
+ end
50
+
51
+ def say_status_report
52
+ if (errors = @status[:red]) > 0
53
+ say "#{errors} error(s) were occurred.", :red
54
+ else
55
+ say "Done."
56
+ end
57
+ end
58
+
59
+ def expand_template(template_path, dest_path)
60
+ require "erb"
61
+ template = ERB.new(File.open(template_path).read, nil, "-")
62
+
63
+ if File.exists?(dest_path)
64
+ say_status "exist", "Ignore #{dest_path}", :yellow
65
+ return
66
+ end
67
+
68
+ begin
69
+ mkdir_p(File.expand_path("..", dest_path))
70
+ File.open(dest_path, "w", 0600) do |file|
71
+ file.write(template.result(binding))
72
+ end
73
+ say_status "ok", "copy #{dest_path}", :green
74
+ rescue StandardError => e
75
+ say_status "failed", "#{e.message.split(' @').first} #{dest_path}", :red
76
+ end
77
+ end
78
+
79
+ def mkdir_p(path)
80
+ path = File.expand_path(path)
81
+
82
+ if File.directory?(path)
83
+ say_status "exist", "Ignore #{path}", :yellow
84
+ return
85
+ end
86
+
87
+ begin
88
+ FileUtils.mkdir_p(path)
89
+ say_status "create", "#{path}", :green
90
+ rescue StandardError => e
91
+ say_status "failed", "#{e.message.split(' @').first} #{path}", :red
92
+ end
93
+ end
94
+
95
+ def find_current_tzid
96
+ require "digest/md5"
97
+
98
+ # Debian
99
+ if File.exists?("/etc/timezone")
100
+ return File.open("/etc/timezone").read.chomp
101
+ end
102
+
103
+ # Mac
104
+ if File.symlink?("/etc/localtime") &&
105
+ /([^\/]+\/[^\/]+)$/ =~ File.readlink("/etc/localtime")
106
+ return $1
107
+ end
108
+
109
+ # Red Had / CentOS
110
+ if File.exists?("/etc/sysconfig/clock") &&
111
+ /ZONE=["']?([^"']+)/ =~ File.open("/etc/sysconfig/clock").read.chomp
112
+ return $1
113
+ end
114
+
115
+ # generic including FreeBSD
116
+ if File.exists?("/etc/localtime")
117
+ localtime = Digest::MD5.file("/etc/localtime")
118
+ candidates = Dir.chdir("/usr/share/zoneinfo") do
119
+ Dir.glob("**/*").select do |fn|
120
+ File.file?(fn) && Digest::MD5.file(fn) == localtime
121
+ end
122
+ end
123
+ unless candidates.empty?
124
+ # take the most descriptive (has long name) one
125
+ return candidates.sort {|a,b| b.length <=> a.length}.first
126
+ end
127
+ end
128
+ return "Unknown"
129
+ end
130
+
131
+ end # class Init
132
+ end # module Command
133
+ end # module Mhc
@@ -0,0 +1,33 @@
1
+ module Mhc
2
+ module Command
3
+ class Scan
4
+ Encoding.default_external = "UTF-8"
5
+
6
+ def initialize(calendar, range_string, format: :text, search: nil, **options)
7
+ date_range = Mhc::PropertyValue::Date.parse_range(range_string)
8
+ formatter = Mhc::Formatter.build(formatter: format, date_range: date_range, **options)
9
+ format_range(calendar, formatter, date_range, search: search, **options)
10
+ end
11
+
12
+ def format_range(calendar, formatter, date_range, search: nil, **options)
13
+ if options[:category] and not search
14
+ search = "category:\"#{options[:category]}\""
15
+ end
16
+
17
+ if search
18
+ begin
19
+ search_proc = Mhc::Query.new(search).to_proc
20
+ rescue Mhc::Query::ParseError => e
21
+ STDERR.print "Error: " + e.message.capitalize + " in search string\n"
22
+ exit 1
23
+ end
24
+ end
25
+
26
+ calendar.occurrences(date_range, &search_proc).each do |oc|
27
+ formatter << oc
28
+ end
29
+ print formatter.to_s
30
+ end
31
+ end # class Scan
32
+ end # module Command
33
+ end # module Mhc
@@ -0,0 +1,22 @@
1
+ module Mhc
2
+ module Command
3
+ class Sync
4
+ Encoding.default_external = "UTF-8"
5
+
6
+ def initialize(channel_name, config)
7
+ channel = config.sync_channels[channel_name]
8
+
9
+ unless channel
10
+ STDERR.print "Error: Not found '#{channel_name}' in ~/.mhc/config.yml\n"
11
+ exit 1
12
+ end
13
+
14
+ builder = Mhc::Builder.new(config)
15
+
16
+ driver = builder.sync_driver(channel.name, channel.strategy)
17
+ driver.sync_all
18
+ end
19
+
20
+ end # class Sync
21
+ end # module Command
22
+ end # module Mhc
data/lib/mhc/config.rb ADDED
@@ -0,0 +1,229 @@
1
+ require 'yaml'
2
+ require 'pp'
3
+
4
+ module Mhc
5
+ class Config
6
+ # Syntax table manipulation
7
+ class Syntax
8
+ def initialize(syntax_config)
9
+ @syntax_config = syntax_config
10
+ end
11
+
12
+ def keyword_symbols
13
+ @syntax_config.keys
14
+ end
15
+
16
+ def keywords
17
+ keyword_symbols.map {|sym| sym.to_s.upcase }
18
+ end
19
+
20
+ def keyword?(word)
21
+ if word.is_a?(Symbol)
22
+ keyword_symbols.member?(word)
23
+ else
24
+ # String
25
+ keywords.member?(word)
26
+ end
27
+ end
28
+
29
+ def instance_variable_name(word)
30
+ return nil unless keyword?(word)
31
+ return '@' + as_symbol(word).to_s
32
+ end
33
+
34
+ def item_class(word)
35
+ return nil unless keyword?(word)
36
+ @syntax_config[as_symbol(word)]
37
+ end
38
+
39
+ private
40
+ def as_symbol(word)
41
+ word.to_s.downcase.sub(/^@+/, "").to_sym
42
+ end
43
+ end # class Syntax
44
+
45
+ # Parse Key-Value object in YAML
46
+ class Base
47
+ # attr_accessor :name
48
+
49
+ def self.create_from_yaml_file(yaml_file)
50
+ yaml_string = File.open(File.expand_path(yaml_file)).read
51
+ return create_from_yaml_string(yaml_string, yaml_file)
52
+ end
53
+
54
+ def self.create_from_yaml_string(yaml_string, filename = nil)
55
+ hash = YAML.load(yaml_string, filename) || {}
56
+ return new(hash)
57
+ end
58
+
59
+ def self.define_syntax(config)
60
+ @syntax = Syntax.new(config)
61
+ @syntax.keyword_symbols.each do |sym|
62
+ attr_accessor sym # XXX: attr_reader is enough?
63
+ end
64
+ end
65
+
66
+ def self.syntax
67
+ return @syntax
68
+ end
69
+
70
+ def initialize(hash = {})
71
+ @original_hash = hash
72
+ (hash || {}).each do |key, val|
73
+ raise Mhc::ConfigurationError, "config syntax error (#{key})" unless syntax.keyword?(key)
74
+ var = syntax.instance_variable_name(key)
75
+ obj = create_subnode(key, val)
76
+ instance_variable_set(var, obj)
77
+ end
78
+ end
79
+
80
+ attr_reader :original_hash
81
+
82
+ def get_value(dot_separated_string = nil)
83
+ if dot_separated_string.to_s == ""
84
+ return original_hash
85
+ end
86
+
87
+ key, subkey = dot_separated_string.to_s.upcase.split(".", 2)
88
+ subnode = get_subnode(key)
89
+
90
+ if subnode.respond_to?(:get_value)
91
+ return subnode.get_value(subkey)
92
+ else
93
+ return subnode.to_s
94
+ end
95
+ end
96
+
97
+ def to_yaml
98
+ return self.to_hash.to_yaml
99
+ end
100
+
101
+ def to_hash
102
+ hash = {}
103
+ syntax.keywords.each do |key|
104
+ var = syntax.instance_variable_name(key)
105
+ obj = instance_variable_get(var)
106
+ obj = obj.respond_to?(:to_hash) ? obj.to_hash : obj.to_s
107
+ hash[key] = obj
108
+ end
109
+ return hash
110
+ end
111
+
112
+ private
113
+ def syntax
114
+ self.class.syntax
115
+ end
116
+
117
+ def get_subnode(key)
118
+ raise Mhc::ConfigurationError, "Invalid key: #{key}" unless syntax.keyword?(key)
119
+ return instance_variable_get(syntax.instance_variable_name(key))
120
+ end
121
+
122
+ def create_subnode(keyword, value)
123
+ item_class = syntax.item_class(keyword)
124
+ if item_class.is_a?(Array)
125
+ return List.new(item_class.first, value)
126
+ elsif item_class == String
127
+ return value.to_s
128
+ else
129
+ return item_class.new(value)
130
+ end
131
+ end
132
+
133
+ end # class Base
134
+
135
+ # Parse Array object in YAML
136
+ class List < Base
137
+ include Enumerable
138
+
139
+ def initialize(item_class, array = [])
140
+ @original_hash = array
141
+ @configs = []
142
+ (array || []).each do |value|
143
+ item = item_class.new(value)
144
+ @configs << item
145
+ end
146
+ end
147
+
148
+ def [](key)
149
+ @configs.find {|c| c.name == key}
150
+ end
151
+
152
+ alias_method :get_subnode, :[]
153
+
154
+ def <<(conf)
155
+ @configs << conf
156
+ end
157
+
158
+ def to_hash # XXX: actually, it returns a Array
159
+ return @configs.map {|c| c.respond_to?(:to_hash) ? c.to_hash : c.to_s}
160
+ end
161
+
162
+ def each
163
+ @configs.each do |conf|
164
+ yield conf
165
+ end
166
+ end
167
+ end # List
168
+
169
+ ## concrete config classes
170
+
171
+ class General < Base
172
+ define_syntax :tzid => String,
173
+ :repository => String
174
+ end # class General
175
+
176
+ class SyncChannel < Base
177
+ define_syntax :name => String,
178
+ :calendar1 => String,
179
+ :calendar2 => String,
180
+ :strategy => String
181
+ end # class SyncChannel
182
+
183
+ class Calendar < Base
184
+ define_syntax :name => String,
185
+ :type => String,
186
+ :user => String,
187
+ :password => String,
188
+ :url => String,
189
+ :filter => Mhc::Query,
190
+ :modifiers => [Mhc::Modifier]
191
+ end # class Calendar
192
+
193
+ # Top-Level Config
194
+ class Top < Base
195
+ define_syntax :general => General,
196
+ :sync_channels => [SyncChannel],
197
+ :calendars => [Calendar]
198
+
199
+ def embed_values
200
+ super hash
201
+ self.sync_channels.each do |ch|
202
+ # String -> Calendar
203
+ ch.calendar1 = calendars[ch.calendar1] if calendars[ch.calendar1]
204
+ ch.calendar2 = calendars[ch.calendar2] if calendars[ch.calendar2]
205
+ end
206
+ end
207
+ end # class Top
208
+
209
+ def self.create_from_file(file_name)
210
+ unless File.exists?(File.expand_path(file_name))
211
+ raise Mhc::ConfigurationError, "config file '#{file_name}' not found"
212
+ end
213
+ begin
214
+ return Top.create_from_yaml_file(file_name)
215
+ rescue Psych::SyntaxError, Mhc::Query::ParseError, Mhc::Modifier::ParseError => e
216
+ raise Mhc::ConfigurationError, e.message
217
+ end
218
+ end
219
+
220
+ def self.create_from_string(string)
221
+ begin
222
+ return Top.create_from_yaml_string(string)
223
+ rescue Psych::SyntaxError, Mhc::Query::ParseError, Mhc::Modifier::ParseError => e
224
+ raise Mhc::ConfigurationError, e.message
225
+ end
226
+ end
227
+
228
+ end # class Config
229
+ end # module Mhc