selene 0.3.1 → 0.3.4

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a94d8b8e9a90bf2658c322521edc6e6a89ccab65
4
+ data.tar.gz: d54aa750eab708bd7ea60ac21be9201073be09d0
5
+ SHA512:
6
+ metadata.gz: 525d85405335dcf90a579d1bde6d0a7c5472b96d91aa127606c0b81a19aabbc2cb2e33ab707201e884cb0d7a15c107616713f6c756a09d91763d7b2b1349529c
7
+ data.tar.gz: 70e1b7e2ce0a7651cb2c3526217107a53b092cc39dd9855d87d0358553615e56a038fe00c2ea40734aac2e3143e7a30ea9ad2c3b424b7bac261a462227a557e2
data/Gemfile CHANGED
@@ -1,2 +1,2 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
  gemspec
@@ -0,0 +1,4 @@
1
+ guard :minitest, all_on_start: false do
2
+ watch(%r|^test/(.*)\/?(.*)_test\.rb|)
3
+ watch(%r|^lib/(.*)\.rb|) { |m| "test/#{m[1]}_test.rb" }
4
+ end
data/Rakefile CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'bundler/gem_tasks'
2
- require 'debugger'
3
2
  require 'json'
4
3
  require 'pp'
5
4
  require 'rake/testtask'
@@ -2,9 +2,7 @@ require 'selene/parser'
2
2
  require 'selene/version'
3
3
 
4
4
  module Selene
5
-
6
5
  def self.parse(string)
7
6
  Parser.parse(string)
8
7
  end
9
-
10
8
  end
@@ -1,4 +1,7 @@
1
1
  module Selene
2
2
  class AlarmBuilder < ComponentBuilder
3
+ def initialize
4
+ super('valarm')
5
+ end
3
6
  end
4
7
  end
@@ -1,9 +1,15 @@
1
1
  module Selene
2
2
  class CalendarBuilder < ComponentBuilder
3
+ property 'prodid', required: true, :multiple => false
4
+ property 'version', :required => true, :multiple => false
3
5
 
4
- REQUIRED_PROPERTIES = %w(prodid version)
6
+ property 'calscale', :multiple => false
7
+ property 'method', :multiple => false
5
8
 
6
- DISTINCT_PROPERTIES = %w(prodid version calscale method)
9
+ # Custom properties: x-prop, iana-prop
7
10
 
11
+ def initialize
12
+ super('vcalendar')
13
+ end
8
14
  end
9
15
  end
@@ -1,46 +1,130 @@
1
1
  module Selene
2
+ # This is the base class for all component builders.
3
+ #
4
+ # Properties are specified one per property with optional rules, e.g.:
5
+ #
6
+ # property 'version', :required => true, :multiple => false
7
+ #
8
+ # If :required is truthy, a component is not valid without that property.
9
+ # If :multiple is falsy, a component can only have one of that property
10
+ #
11
+ # Custom rules can be implemented by overriding can_add?(property) or valid?
2
12
  class ComponentBuilder
3
- include ComponentRules
13
+ class ParseError < StandardError; end
4
14
 
5
- attr_accessor :component, :parent, :errors
15
+ @property_rules = {}
6
16
 
7
- REQUIRED_PROPERTIES = []
8
- DISTINCT_PROPERTIES = []
9
- EXCLUSIVE_PROPERTIES = []
17
+ class << self
18
+ attr_accessor :property_rules
19
+ end
10
20
 
11
- def add(name, builder)
12
- @component[name.downcase] << builder.component
13
- builder.parent = self
21
+ attr_accessor :component, :errors, :name, :parent
22
+
23
+ def self.property(name, rules = {})
24
+ property_rules[name] = rules
14
25
  end
15
26
 
16
- def initialize
27
+ def self.inherited(subclass)
28
+ subclass.instance_variable_set('@property_rules', @property_rules)
29
+ end
30
+
31
+ def initialize(name)
32
+ @name = name
17
33
  @component = Hash.new { |component, property| component[property] = [] }
18
- @errors = []
19
- @property_rules = property_rules(self)
20
- @component_rules = component_rules(self)
34
+ @errors = Hash.new { |errors, property| errors[property] = [] }
21
35
  end
22
36
 
23
- def parse(line)
24
- return unless @property_rules.all? { |message, rule| rule.call(name(line), line, message) }
25
- @component[name(line)] = value(line)
37
+ def add(name, builder)
38
+ @component[name] << builder.component
26
39
  end
27
40
 
28
- def name(line)
29
- line.name
41
+ def parse(*properties)
42
+ properties.each do |property|
43
+ case property
44
+ when Line
45
+ @component[name(property)] = value(property) if can_add?(property)
46
+ when String
47
+ Line.split(property).each { |line| parse(line) }
48
+ else
49
+ raise ParseError, "Cannot parse argument of type #{property.class}"
50
+ end
51
+ end
30
52
  end
31
53
 
32
- def value(line)
33
- line.value
54
+ def to_ical(component)
55
+ lines = []
56
+ component.each_pair do |key, value|
57
+ keys = []
58
+ values = []
59
+ keys << key.upcase
60
+ case value
61
+ when Array
62
+ if value[1].is_a?(Hash)
63
+ value[1].each_pair do |pkey, pvalue|
64
+ keys << [pkey.upcase, pvalue].join('=')
65
+ end
66
+ values << value[0]
67
+ else
68
+ values += value
69
+ end
70
+ when Hash
71
+ value.each_pair do |vkey, vvalue|
72
+ values << [vkey.upcase, vvalue].join('=')
73
+ end
74
+ end
75
+
76
+ lines << [keys.join(';'), values.join(';')].join(':')
77
+ end
78
+
79
+ lines.join("\n")
34
80
  end
35
81
 
36
- def a_component_rules
37
- @component_rules
82
+ def name(property)
83
+ property.name
38
84
  end
39
85
 
40
- def valid?
41
- @component_rules.each { |message, rule| rule.call(message) }
86
+ def value(property)
87
+ property.value
88
+ end
89
+
90
+ def contains_property?(property)
91
+ @component.key?(property.to_s)
92
+ end
93
+
94
+ def can_contain?(component_name)
95
+ true
96
+ end
97
+
98
+ def error(property, message)
99
+ @errors[property] << message
100
+ end
101
+
102
+ def properties
103
+ self.class.property_rules.keys
104
+ end
105
+
106
+ def required?(property)
107
+ self.class.property_rules[property][:required]
108
+ end
109
+
110
+ def multiple?(property)
111
+ self.class.property_rules[property][:multiple]
112
+ end
113
+
114
+ def can_add?(property)
115
+ if contains_property?(property.name) && !multiple?(property.name)
116
+ error(property.name, "property '%s' must not occur more than once" % property.name)
117
+ end
42
118
  @errors.empty?
43
119
  end
44
120
 
121
+ def valid?
122
+ properties.select { |property| required?(property) }.each do |property|
123
+ if !contains_property?(property)
124
+ error(property, "missing required property '%s'" % property)
125
+ end
126
+ end
127
+ @errors.empty?
128
+ end
45
129
  end
46
130
  end
@@ -0,0 +1,7 @@
1
+ module ComponentErrors
2
+
3
+ def containment_error(line)
4
+
5
+ end
6
+
7
+ end
@@ -1,5 +1,8 @@
1
1
  module Selene
2
2
  class DaylightSavingsTimeBuilder < ComponentBuilder
3
+ def initialize
4
+ super('daylight')
5
+ end
3
6
 
4
7
  def value(line)
5
8
  case line.name
@@ -9,6 +12,5 @@ module Selene
9
12
  super
10
13
  end
11
14
  end
12
-
13
15
  end
14
16
  end
@@ -1,29 +1,80 @@
1
1
  module Selene
2
2
  class EventBuilder < ComponentBuilder
3
3
 
4
- # These properties are required
5
- REQUIRED_PROPERTIES = %w(dtstamp uid)
4
+ # Required properties
5
+ property 'dtstamp', required: true, multiple: false
6
+ property 'uid', required: true, multiple: false
6
7
 
7
- # These properties must not occur more than once
8
- DISTINCT_PROPERTIES = %w(dtstamp uid dtstart class created description geo
9
- last-mod location organizer priority seq status summary
10
- transp url recurid)
8
+ # Optional properties
9
+ property 'class', multiple: false
10
+ property 'created', multiple: false
11
+ property 'description', multiple: false
12
+ property 'dtend', multiple: false
13
+ property 'dtstart', multiple: false
14
+ property 'duration', multiple: false
15
+ property 'geo', multiple: false
16
+ property 'last-mod', multiple: false
17
+ property 'location', multiple: false
18
+ property 'organizer', multiple: false
19
+ property 'priority', multiple: false
20
+ property 'recurid', multiple: false
21
+ property 'rrule' # The rrule property should not occur more than once (but can if necessary)
22
+ property 'seq', multiple: false
23
+ property 'status', multiple: false
24
+ property 'summary', multiple: false
25
+ property 'transp', multiple: false
26
+ property 'url', multiple: false
27
+ property 'attach'
28
+ property 'attendee'
29
+ property 'categories'
30
+ property 'comment'
31
+ property 'contact'
32
+ property 'exdate'
33
+ property 'rdate'
34
+ property 'related'
35
+ property 'related-to'
36
+ property 'resources'
37
+ property 'rstatus'
11
38
 
12
- # These properties must not occur together in the same component
13
- EXCLUSIVE_PROPERTIES = [
14
- %w(dtend duration)
15
- ]
39
+ # Custom properties: x-prop, iana-prop
16
40
 
17
- # single_property :rrule, :except => when?
18
- # if dtstart is a date, dtend has to be too
19
- # multi-day durations must be 'dur-day' or 'dur-week'
41
+ # TODO: if dtstart is a date, dtend must be as well
42
+ # TODO: multi-day durations must be 'dur-day' or 'dur-week'
20
43
 
21
- def value(line)
22
- case line.name
44
+ def self.update(component, properties)
45
+ new('vevent', component).update_properties(properties)
46
+ end
47
+
48
+ def initialize
49
+ super('vevent')
50
+ end
51
+
52
+ def update_properties(properties)
53
+ properties.inject(self.component) do |component, (name, value)|
54
+ component = update_property(component, name, value)
55
+ component
56
+ end
57
+ end
58
+
59
+ def update_property(component, name, value)
60
+ case name
61
+ when 'dtstart'
62
+ component.merge('dtstart' => [value.strftime('%Y%m%dT%H%M%S'), component['dtstart'][1]])
63
+ else
64
+ component
65
+ end
66
+ end
67
+
68
+ def value(property)
69
+ case property.name
23
70
  when 'dtstamp', 'dtstart', 'dtend'
24
- line.value_with_params
71
+ property.value_with_params
72
+ when 'exdate'
73
+ property.values_with_params
25
74
  when 'geo'
26
- line.values
75
+ property.values
76
+ when 'rrule'
77
+ property.rrule
27
78
  else
28
79
  super
29
80
  end
@@ -34,14 +85,20 @@ module Selene
34
85
  super(builder)
35
86
  end
36
87
 
37
- def component_rules(component)
38
- super(component).tap do |rules|
39
- rules["Property 'dtstart' required if the calendar does not specify a 'method' property"] = lambda do |message|
40
- return if @component.key?('dtstart') || parent && parent.component.key?('method')
41
- @errors << { :message => message }
42
- end
88
+ def can_add?(property)
89
+ if property.name == 'dtend' && contains_property?('duration')
90
+ error('dtend', "The 'dtend' property cannot be set if the 'duration' property already exists")
91
+ elsif property.name == 'duration' && contains_property?('dtend')
92
+ error('duration', "The 'duration' property cannot be set if the 'dtend' property already exists")
43
93
  end
94
+ super(property)
44
95
  end
45
96
 
97
+ def valid?
98
+ if !contains_property?('dtstart') && !parent.contains_property?('method')
99
+ error('dtstart', "The 'dtstart' property is required if the calendar does not have a 'method' property")
100
+ end
101
+ super
102
+ end
46
103
  end
47
104
  end
@@ -1,13 +1,15 @@
1
1
  module Selene
2
- class FeedBuilder < Builder
3
-
2
+ class FeedBuilder < ComponentBuilder
4
3
  def initialize
5
- @component = { 'vcalendar' => [] }
4
+ super('feed')
6
5
  end
7
6
 
8
- def component
9
- @component
7
+ def can_contain?(builder)
8
+ !%w(vevent vtimezone valarm standard daylight).include?(builder.name)
10
9
  end
11
10
 
11
+ def error(message)
12
+ @component['errors'] << message
13
+ end
12
14
  end
13
15
  end
@@ -1,5 +1,5 @@
1
1
  module Selene
2
- class Line < Struct.new(:name, :params, :value)
2
+ class Line < Struct.new(:name, :value, :params)
3
3
 
4
4
  # Match everything until we hit ';' (parameter separator) or ':' (value separator)
5
5
  NAME = /(?<name>[^:\;]+)/
@@ -13,7 +13,7 @@ module Selene
13
13
  # Match everything that is not ';' (parameter separator)
14
14
  PARAM = /[^\;]+/
15
15
 
16
- # Match everything before '='
16
+ # Match parameter key and value
17
17
  PARAM_KEY_VALUE = /(?<key>[^=]+)=(?<value>.*)/
18
18
 
19
19
  # Split a string into content lines
@@ -25,7 +25,7 @@ module Selene
25
25
  # Parse a content line into a line object
26
26
  def self.parse(content_line)
27
27
  content_line.match(/#{NAME}#{PARAMS}?:#{VALUE}/) do |match|
28
- return new(match[:name], parse_params(match[:params]), match[:value])
28
+ return new(match[:name], match[:value], parse_params(match[:params]))
29
29
  end
30
30
  end
31
31
 
@@ -41,35 +41,40 @@ module Selene
41
41
  end
42
42
  end
43
43
 
44
- def initialize(name, params, value)
45
- self.name = name.downcase
46
- self.params = params || {}
47
- self.value = value
44
+ def initialize(name, value, params = {})
45
+ super(name.downcase, value, params)
48
46
  end
49
47
 
50
48
  def begin_component?
51
49
  name == 'begin'
52
50
  end
53
51
 
52
+ def component_name
53
+ value.downcase
54
+ end
55
+
54
56
  def end_component?
55
57
  name == 'end'
56
58
  end
57
59
 
58
60
  def params?
59
- params && !params.empty?
61
+ !params.empty?
60
62
  end
61
63
 
62
64
  def value_with_params
63
65
  params? ? [value, params] : value
64
66
  end
65
67
 
68
+ def values_with_params
69
+ params? ? [values, params] : values
70
+ end
71
+
66
72
  def rrule
67
73
  Hash[values.map { |values| k, v = values.split('=', 2); [k.downcase, v] }]
68
74
  end
69
75
 
70
76
  def values
71
- value.split(';')
77
+ value.split(/[;,]/)
72
78
  end
73
-
74
79
  end
75
80
  end
@@ -1,36 +1,31 @@
1
1
  require 'selene/line'
2
- require 'selene/component_rules'
3
- require 'selene/component_builder'
4
2
 
3
+ require 'selene/component_builder'
5
4
  require 'selene/alarm_builder'
6
5
  require 'selene/calendar_builder'
7
6
  require 'selene/daylight_savings_time_builder'
8
7
  require 'selene/event_builder'
8
+ require 'selene/feed_builder'
9
9
  require 'selene/standard_time_builder'
10
10
  require 'selene/time_zone_builder'
11
11
 
12
12
  module Selene
13
- module Parser
13
+ class Parser
14
+ def self.parse(string)
15
+ new(string).parse
16
+ end
14
17
 
15
- def self.builder(component)
16
- case component.downcase
17
- when 'vcalendar' then CalendarBuilder
18
- when 'vtimezone' then TimeZoneBuilder
19
- when 'daylight' then DaylightSavingsTimeBuilder
20
- when 'standard' then StandardTimeBuilder
21
- when 'vevent' then EventBuilder
22
- when 'valarm' then AlarmBuilder
23
- else ComponentBuilder
24
- end
18
+ def initialize(string)
19
+ @string = string
25
20
  end
26
21
 
27
- def self.parse(string)
28
- stack = []
29
- stack << builder('feed').new
30
- Line.split(string).each do |line|
22
+ def parse
23
+ feed = FeedBuilder.new
24
+ stack = [feed]
25
+ Line.split(@string).each do |line|
31
26
  if line.begin_component?
32
- builder = builder(line.value).new
33
- stack[-1].add(line.value, builder) unless stack.empty?
27
+ builder = create_builder(line.component_name)
28
+ stack[-1].add(line.component_name, builder)
34
29
  stack << builder
35
30
  elsif line.end_component?
36
31
  stack.pop
@@ -38,8 +33,19 @@ module Selene
38
33
  stack[-1].parse(line)
39
34
  end
40
35
  end
41
- stack[-1].component
36
+ feed.component
42
37
  end
43
38
 
39
+ def create_builder(name)
40
+ case name
41
+ when 'daylight' then DaylightSavingsTimeBuilder.new
42
+ when 'standard' then StandardTimeBuilder.new
43
+ when 'valarm' then AlarmBuilder.new
44
+ when 'vcalendar' then CalendarBuilder.new
45
+ when 'vevent' then EventBuilder.new
46
+ when 'vtimezone' then TimeZoneBuilder.new
47
+ else ComponentBuilder.new(name)
48
+ end
49
+ end
44
50
  end
45
51
  end
@@ -1,4 +1,7 @@
1
1
  module Selene
2
2
  class StandardTimeBuilder < ComponentBuilder
3
+ def initialize
4
+ super('standard')
5
+ end
3
6
  end
4
7
  end
@@ -1,4 +1,7 @@
1
1
  module Selene
2
2
  class TimeZoneBuilder < ComponentBuilder
3
+ def initialize
4
+ super('vtimezone')
5
+ end
3
6
  end
4
7
  end
@@ -1,3 +1,3 @@
1
1
  module Selene
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -17,7 +17,11 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.add_development_dependency 'debugger'
21
- gem.add_development_dependency 'minitest-colorize'
20
+ gem.add_dependency 'ice_cube'
21
+
22
+ gem.add_development_dependency 'byebug'
23
+ gem.add_development_dependency 'minitest-reporters'
22
24
  gem.add_development_dependency 'rake'
25
+ gem.add_development_dependency 'guard'
26
+ gem.add_development_dependency 'guard-minitest'
23
27
  end
@@ -0,0 +1,6 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/reporters'
3
+
4
+ Minitest::Reporters.use!
5
+
6
+ require_relative 'support/fixtures_helper'
@@ -1,20 +1,17 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class AlarmBuilderTest < MiniTest::Unit::TestCase
4
+ class AlarmBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
- def setup
7
+ def test_parses_action
8
+ builder.parse(Line.new('ACTION', 'AUDIO'))
9
+ assert_equal builder.component['action'], 'AUDIO'
8
10
  end
9
11
 
12
+ private
10
13
  def builder
11
14
  @builder ||= AlarmBuilder.new
12
15
  end
13
-
14
- def test_parses_action
15
- parse_line 'ACTION', {}, 'AUDIO'
16
- assert_equal builder.component['action'], 'AUDIO'
17
- end
18
-
19
16
  end
20
17
  end
@@ -1,70 +1,36 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class CalendarBuilderTest < MiniTest::Unit::TestCase
4
+ class CalendarBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
- def builder
8
- @builder ||= CalendarBuilder.new
9
- end
10
-
11
- def event_builder
12
- @event_builder ||= EventBuilder.new.extend(Stubbable)
13
- end
14
-
15
- def time_zone_builder
16
- @time_zone_builder ||= TimeZoneBuilder.new.extend(Stubbable)
17
- end
18
-
19
- def test_parse_prodid
20
- parse_line('PRODID', {}, '-//Meetup//RemoteApi//EN')
21
- assert_equal builder.component['prodid'], '-//Meetup//RemoteApi//EN'
22
- end
23
-
24
- def test_parse_version
25
- parse_line('VERSION', {}, '2.0')
26
- assert_equal @builder.component['version'], '2.0'
27
- end
28
-
29
- def test_parse_calscale
30
- parse_line('CALSCALE', {}, 'Gregorian')
31
- assert_equal builder.component['calscale'], 'Gregorian'
32
- end
33
-
34
- def test_parse_method
35
- parse_line('METHOD', {}, 'Publish')
36
- assert_equal builder.component['method'], 'Publish'
37
- end
38
-
39
- def test_parse_x_prop
40
- parse_line('X-ORIGINAL-URL', {}, 'http://www.google.com')
41
- assert_equal builder.component['x-original-url'], 'http://www.google.com'
42
- end
43
-
44
7
  def test_append_event_builder
8
+ builder = CalendarBuilder.new
9
+ event_builder = EventBuilder.new.extend(Stubbable)
45
10
  event_builder.stub :component, { 'summary' => "Bluth's Best Party" }
46
11
  builder.add('vevent', event_builder)
47
12
  assert_equal builder.component['vevent'].first['summary'], "Bluth's Best Party"
48
13
  end
49
14
 
50
15
  def test_append_time_zone_builder
16
+ builder = CalendarBuilder.new
17
+ time_zone_builder = TimeZoneBuilder.new.extend(Stubbable)
51
18
  time_zone_builder.stub :component, { 'tzid' => 'America/Detroit' }
52
19
  builder.add('vtimezone', time_zone_builder)
53
20
  assert_equal builder.component['vtimezone'].first, { 'tzid' => 'America/Detroit' }
54
21
  end
55
22
 
56
- # Validation
57
-
23
+ # Test properties with :required => true
58
24
  %w(prodid version).each do |property|
59
25
  define_method "test_#{property}_required" do
60
- assert_required property
26
+ assert_required(CalendarBuilder.new, property)
61
27
  end
62
28
  end
63
29
 
30
+ # Test properties with :multiple => false
64
31
  %w(prodid version calscale method).each do |property|
65
- define_method "test_#{property}_cant_be_defined_more_than_once" do
66
- assert_single property
67
- assert_multiple_values_do_not_overwrite property
32
+ define_method "test_#{property}_cannot_be_defined_more_than_once" do
33
+ assert_single(CalendarBuilder.new, property)
68
34
  end
69
35
  end
70
36
 
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ module Selene
4
+ class ComponentBuilderTest < MiniTest::Test
5
+ def test_sets_property_rules
6
+ ComponentBuilder.property :version, :required => true
7
+ assert_equal({ :required => true }, ComponentBuilder.property_rules[:version])
8
+ end
9
+ end
10
+ end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class DaylightSavingsTimeBuilderTest < MiniTest::Unit::TestCase
4
+ class DaylightSavingsTimeBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
7
  def builder
@@ -9,9 +9,8 @@ module Selene
9
9
  end
10
10
 
11
11
  def test_parses_rrule
12
- parse_line('RRULE', '', 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU')
13
- assert_equal builder.component['rrule'], { 'freq' => 'YEARLY', 'bymonth' => '3', 'byday' => '2SU' }
12
+ builder.parse(Line.new('RRULE', 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU'))
13
+ assert_equal({ 'freq' => 'YEARLY', 'bymonth' => '3', 'byday' => '2SU' }, builder.component['rrule'])
14
14
  end
15
-
16
15
  end
17
16
  end
@@ -1,121 +1,73 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class EventBuilderTest < MiniTest::Unit::TestCase
4
+ class EventBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
- def builder
8
- @builder ||= EventBuilder.new
9
- end
10
-
11
- def builder_with_parent
12
- builder.tap { |b| b.parent = CalendarBuilder.new }
13
- end
14
-
15
- def test_parses_dtstart
16
- parse_line('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
17
- assert_equal builder.component['dtstart'],
18
- ['20130110T183000', { 'tzid' => 'America/New_York' }]
19
- end
20
-
21
- def test_parses_dtstamp
22
- parse_line('DTSTAMP', {}, '20121231T093631Z')
23
- assert_equal builder.component['dtstamp'], '20121231T093631Z'
24
- end
25
-
26
- def test_parses_dtstart_without_tzid
27
- parse_line('DTSTART', {}, '20130110T183000')
28
- assert_equal builder.component['dtstart'], '20130110T183000'
29
- end
30
-
31
- def test_parses_dtend
32
- parse_line('DTEND', { 'tzid' => 'America/New_York' }, '20130110T183000')
33
- assert_equal builder.component['dtend'], ['20130110T183000', { 'tzid' => 'America/New_York' }]
34
- end
35
-
36
- def test_parses_dtstart_without_tzid
37
- parse_line('DTEND', {}, '20130110T183000')
38
- assert_equal builder.component['dtend'], '20130110T183000'
39
- end
40
-
41
- def test_parses_status
42
- parse_line('STATUS', {}, 'CONFIRMED')
43
- assert_equal builder.component['status'], 'CONFIRMED'
44
- end
45
-
46
- def test_parses_summary
47
- parse_line('SUMMARY', {}, 'DetroitRuby: 2012 Plan + Lightning talks')
48
- assert_equal builder.component['summary'], 'DetroitRuby: 2012 Plan + Lightning talks'
49
- end
50
-
51
- def test_parses_description
52
- parse_line('DESCRIPTION', {}, 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n')
53
- assert_equal builder.component['description'], 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n'
54
- end
55
-
56
- def test_parses_class
57
- parse_line('CLASS', {}, 'PUBLIC')
58
- assert_equal builder.component['class'], 'PUBLIC'
59
- end
60
-
61
- def test_parses_created
62
- parse_line('CREATED', {}, '20120106T161509Z')
63
- assert_equal builder.component['created'], '20120106T161509Z'
7
+ # Test properties that can contain property parameters
8
+ %w(dtstamp dtstart dtend).each do |property|
9
+ define_method "test_parses_#{property}_with_property_parameters" do
10
+ builder = EventBuilder.new
11
+ builder.parse(Line.new(property.upcase, '20130110T183000', 'tzid' => 'America/New_York'))
12
+ assert_equal ['20130110T183000', { 'tzid' => 'America/New_York' }],
13
+ builder.component[property]
14
+ end
64
15
  end
65
16
 
66
- def test_parses_geo
67
- parse_line('GEO', {}, '42.33;-83.05')
17
+ def test_parses_geo_with_multiple_values
18
+ builder = EventBuilder.new
19
+ builder.parse(Line.new('GEO', '42.33;-83.05'))
68
20
  assert_equal builder.component['geo'], ['42.33', '-83.05']
69
21
  end
70
22
 
71
- def test_parses_location
72
- parse_line('LOCATION', {}, 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)')
73
- assert_equal builder.component['location'], 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)'
74
- end
75
-
76
- def test_parses_url
77
- parse_line('URL', {}, 'http://www.meetup.com/DetroitRuby/events/93346412/')
78
- assert_equal builder.component['url'], 'http://www.meetup.com/DetroitRuby/events/93346412/'
79
- end
80
-
81
- def test_parses_last_modified
82
- parse_line('LAST-MODIFIED', {}, '20120106T161509Z')
83
- assert_equal builder.component['last-modified'], '20120106T161509Z'
84
- end
85
-
86
- def test_parses_uid
87
- parse_line('UID', {}, 'event_qgkxkcyrcbnb@meetup.com')
88
- assert_equal builder.component['uid'], 'event_qgkxkcyrcbnb@meetup.com'
89
- end
90
-
91
- def test_sets_parent_calendar
92
- assert builder_with_parent.parent.is_a?(CalendarBuilder), "Must be able to set the parent calendar builder"
93
- end
94
-
95
- # Validation
96
-
23
+ # Test properties with :required => true
97
24
  %w(dtstamp uid).each do |property|
98
25
  define_method "test_#{property}_required" do
99
- assert_required property
26
+ builder = EventBuilder.new
27
+ builder.parent = CalendarBuilder.new
28
+ assert_required(builder, property)
100
29
  end
101
30
  end
102
31
 
103
- %w(dtstamp uid dtstart class created description geo last-mod location organizer priority seq status summary transp url recurid).each do |property|
104
- define_method "test_#{property}_cant_be_defined_more_than_once" do
105
- assert_single property
106
- assert_multiple_values_do_not_overwrite property
32
+ # Test properties with :multiple => false
33
+ %w(dtstamp uid dtstart class created description geo last-mod location
34
+ organizer priority seq status summary transp url recurid).each do |property|
35
+ define_method "test_#{property}_cannot_be_defined_more_than_once" do
36
+ assert_single(EventBuilder.new, property)
107
37
  end
108
38
  end
109
39
 
110
- def test_adding_to_non_calendar_raises_exception
40
+ def test_invalid_parent
111
41
  assert_raises Exception do
112
- builder.parent = TimeZoneBuilder.new
42
+ EventBuilder.new.parent = TimeZoneBuilder.new
113
43
  end
114
44
  end
115
45
 
116
46
  def test_dtstart_required_if_no_calendar_method
117
- builder_with_parent.valid?
118
- assert builder.errors.any? { |e| e[:message] == "Property 'dtstart' required if the calendar does not specify a 'method' property" }
47
+ builder = EventBuilder.new
48
+ builder.parent = CalendarBuilder.new
49
+ builder.valid?
50
+ assert_error builder, 'dtstart', "The 'dtstart' property is required if the calendar does not have a 'method' property"
51
+ end
52
+
53
+ def test_dtend_invalid_if_duration
54
+ builder = EventBuilder.new
55
+ builder.parse(Line.new('DURATION', {}, 'PT15M'))
56
+ builder.parse(Line.new('DTEND', {}, '19970903T190000Z'))
57
+ assert_error builder, 'dtend', "The 'dtend' property cannot be set if the 'duration' property already exists"
58
+ end
59
+
60
+ def test_duration_invalid_if_dtend
61
+ builder = EventBuilder.new
62
+ builder.parse(Line.new('DTEND', {}, '19970903T190000Z'))
63
+ builder.parse(Line.new('DURATION', {}, 'PT15M'))
64
+ assert_error builder, 'duration', "The 'duration' property cannot be set if the 'dtend' property already exists"
65
+ end
66
+
67
+ def test_exdate
68
+ builder = EventBuilder.new
69
+ builder.parse("EXDATE;VALUE=DATE-TIME;TZID=America/Detroit:19960402T010000,19960403T010000,19960404T010000")
70
+ assert_equal builder.component['exdate'], [['19960402T010000','19960403T010000','19960404T010000'], { 'value' => 'DATE-TIME', 'tzid' => 'America/Detroit' }]
119
71
  end
120
72
  end
121
73
  end
@@ -1,24 +1,24 @@
1
1
  require 'test_helper'
2
+ require 'selene/line'
2
3
 
3
4
  module Selene
4
- class LineTest < MiniTest::Unit::TestCase
5
-
5
+ class LineTest < MiniTest::Test
6
6
  def test_lines_are_unfolded_before_splitting
7
- assert_equal Line.split("TEST:This is a\r\n test").first.value, "This is a test"
7
+ assert_equal Line.new('WHAT', "Say it ain't so"), Line.split("WHAT:Say it\r\n ain't so").first
8
8
  end
9
9
 
10
10
  def test_parses_content_line
11
- assert_equal Line.parse('VERSION:2.0'), Line.new('VERSION', {}, '2.0')
11
+ assert_equal Line.new('VERSION', '2.0'), Line.parse('VERSION:2.0')
12
12
  end
13
13
 
14
14
  def test_parses_url
15
- assert_equal Line.parse('TZURL:http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/'),
16
- Line.new('TZURL', {}, 'http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/')
15
+ assert_equal Line.new('TZURL', 'https://townstage.com'),
16
+ Line.parse('TZURL:https://townstage.com')
17
17
  end
18
18
 
19
19
  def test_parses_params
20
20
  assert_equal Line.parse('DTSTART;TZID=America/New_York:20130110T183000'),
21
- Line.new('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
21
+ Line.new('DTSTART', '20130110T183000', 'tzid' => 'America/New_York')
22
22
  end
23
23
  end
24
24
  end
@@ -1,22 +1,29 @@
1
- require 'test_helper'
1
+ require_relative '../test_helper'
2
2
  require 'json'
3
+ require 'selene/parser'
3
4
 
4
5
  module Selene
5
- class ParserTest < MiniTest::Unit::TestCase
6
+ class ParserTest < MiniTest::Test
6
7
  include FixtureHelper
7
8
 
8
9
  def test_parses_blank_string
9
- assert_equal Selene::Parser.parse(""), {}
10
+ assert_equal Parser.parse(""), {}
10
11
  end
11
12
 
12
13
  def test_parses_simple_calendar
13
- assert_equal Selene::Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"),
14
+ assert_equal Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"),
14
15
  { 'vcalendar' => [{ 'summary' => 'Meetups' }] }
15
16
  end
16
17
 
17
18
  def test_parses_meetup_calendar
18
- assert_equal Selene::Parser.parse(fixture('meetup.ics')),
19
+ assert_equal Parser.parse(fixture('meetup.ics')),
19
20
  JSON.parse(fixture('meetup.json'))
20
21
  end
22
+
23
+ def test_contain_error
24
+ skip
25
+ ical = Parser.parse("BEGIN:VEVENT\r\nEND:VEVENT")
26
+ assert ical['errors'].any? { |e| e.include? "can't contain" }, "Feed can't contain a vevent"
27
+ end
21
28
  end
22
29
  end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class TimeZoneBuilderTest < MiniTest::Unit::TestCase
4
+ class TimeZoneBuilderTest < MiniTest::Test
5
5
 
6
6
  def builder
7
7
  @builder ||= TimeZoneBuilder.new
@@ -1,10 +1,11 @@
1
1
  require 'bundler/setup'
2
- require 'debugger'
3
2
  require 'minitest/autorun'
4
- require 'minitest/colorize'
3
+ require 'minitest/reporters'
5
4
  require 'minitest/mock'
6
5
  require 'selene'
7
6
 
7
+ Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
8
+
8
9
  module Selene
9
10
  module Stubbable
10
11
 
@@ -34,32 +35,21 @@ module Selene
34
35
 
35
36
  module BuilderTestHelper
36
37
 
37
- # This is a helper method that takes a name, params and a value, turns them into a proper line hash,
38
- # and passes them to the builder to be parsed.
39
- def parse_line(name, params, value)
40
- builder.parse(Line.new(name, params, value))
41
- end
42
-
43
- def assert_required property
38
+ def assert_required builder, property
44
39
  builder.valid?
45
- message = "missing required property '#{property}'"
46
- assert builder.errors.any? { |e| e[:message] =~ /#{message}/ }, message
40
+ assert_error builder, property, "missing required property '#{property}'"
47
41
  end
48
42
 
49
- def assert_single property
50
- parse_line(property, {}, 'Some Value')
51
- n = builder.errors.count
52
- parse_line(property, {}, 'Another Value')
53
- n = builder.errors.count - n
54
- assert_equal n, 1, "Cannot have more than one #{property}"
43
+ def assert_single builder, property
44
+ builder.parse(Line.new(property, 'First Value'))
45
+ original_value = builder.component[property]
46
+ builder.parse(Line.new(property, 'Second Value'))
47
+ assert_error builder, property, "property '#{property}' must not occur more than once"
48
+ assert_equal original_value, builder.component[property]
55
49
  end
56
50
 
57
- def assert_multiple_values_do_not_overwrite property
58
- parse_line(property, {}, 'Some Value')
59
- value = builder.component[property].dup
60
- parse_line(property, {}, 'Another Value')
61
- assert_equal builder.component[property], value
51
+ def assert_error builder, property, message
52
+ assert builder.errors[property].any? { |e| e =~ /#{message}/ }, "#{builder.class.name}: #{message}"
62
53
  end
63
-
64
54
  end
65
55
  end
metadata CHANGED
@@ -1,62 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: selene
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
5
- prerelease:
4
+ version: 0.3.4
6
5
  platform: ruby
7
6
  authors:
8
7
  - Cory Kaufman-Schofield
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-15 00:00:00.000000000 Z
11
+ date: 2015-03-08 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: debugger
14
+ name: ice_cube
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
20
32
  - !ruby/object:Gem::Version
21
33
  version: '0'
22
34
  type: :development
23
35
  prerelease: false
24
36
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
37
  requirements:
27
- - - ! '>='
38
+ - - '>='
28
39
  - !ruby/object:Gem::Version
29
40
  version: '0'
30
41
  - !ruby/object:Gem::Dependency
31
- name: minitest-colorize
42
+ name: minitest-reporters
32
43
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
44
  requirements:
35
- - - ! '>='
45
+ - - '>='
36
46
  - !ruby/object:Gem::Version
37
47
  version: '0'
38
48
  type: :development
39
49
  prerelease: false
40
50
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
51
  requirements:
43
- - - ! '>='
52
+ - - '>='
44
53
  - !ruby/object:Gem::Version
45
54
  version: '0'
46
55
  - !ruby/object:Gem::Dependency
47
56
  name: rake
48
57
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
58
  requirements:
51
- - - ! '>='
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
52
88
  - !ruby/object:Gem::Version
53
89
  version: '0'
54
90
  type: :development
55
91
  prerelease: false
56
92
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
93
  requirements:
59
- - - ! '>='
94
+ - - '>='
60
95
  - !ruby/object:Gem::Version
61
96
  version: '0'
62
97
  description: Selene is an iCalendar parser for Ruby
@@ -68,8 +103,8 @@ extra_rdoc_files: []
68
103
  files:
69
104
  - .gitattributes
70
105
  - .gitignore
71
- - .rspec
72
106
  - Gemfile
107
+ - Guardfile
73
108
  - LICENSE.txt
74
109
  - README.md
75
110
  - Rakefile
@@ -77,7 +112,7 @@ files:
77
112
  - lib/selene/alarm_builder.rb
78
113
  - lib/selene/calendar_builder.rb
79
114
  - lib/selene/component_builder.rb
80
- - lib/selene/component_rules.rb
115
+ - lib/selene/component_errors.rb
81
116
  - lib/selene/daylight_savings_time_builder.rb
82
117
  - lib/selene/event_builder.rb
83
118
  - lib/selene/feed_builder.rb
@@ -89,8 +124,10 @@ files:
89
124
  - selene.gemspec
90
125
  - test/fixtures/meetup.ics
91
126
  - test/fixtures/meetup.json
127
+ - test/helper.rb
92
128
  - test/selene/alarm_builder_test.rb
93
129
  - test/selene/calendar_builder_test.rb
130
+ - test/selene/component_builder_test.rb
94
131
  - test/selene/daylight_savings_time_builder_test.rb
95
132
  - test/selene/event_builder_test.rb
96
133
  - test/selene/line_test.rb
@@ -99,39 +136,34 @@ files:
99
136
  - test/test_helper.rb
100
137
  homepage: https://github.com/allspiritseve/selene
101
138
  licenses: []
139
+ metadata: {}
102
140
  post_install_message:
103
141
  rdoc_options: []
104
142
  require_paths:
105
143
  - lib
106
144
  required_ruby_version: !ruby/object:Gem::Requirement
107
- none: false
108
145
  requirements:
109
- - - ! '>='
146
+ - - '>='
110
147
  - !ruby/object:Gem::Version
111
148
  version: '0'
112
- segments:
113
- - 0
114
- hash: 500821961400754664
115
149
  required_rubygems_version: !ruby/object:Gem::Requirement
116
- none: false
117
150
  requirements:
118
- - - ! '>='
151
+ - - '>='
119
152
  - !ruby/object:Gem::Version
120
153
  version: '0'
121
- segments:
122
- - 0
123
- hash: 500821961400754664
124
154
  requirements: []
125
155
  rubyforge_project:
126
- rubygems_version: 1.8.24
156
+ rubygems_version: 2.4.2
127
157
  signing_key:
128
- specification_version: 3
158
+ specification_version: 4
129
159
  summary: Selene is an iCalendar parser for Ruby
130
160
  test_files:
131
161
  - test/fixtures/meetup.ics
132
162
  - test/fixtures/meetup.json
163
+ - test/helper.rb
133
164
  - test/selene/alarm_builder_test.rb
134
165
  - test/selene/calendar_builder_test.rb
166
+ - test/selene/component_builder_test.rb
135
167
  - test/selene/daylight_savings_time_builder_test.rb
136
168
  - test/selene/event_builder_test.rb
137
169
  - test/selene/line_test.rb
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --format progress
@@ -1,38 +0,0 @@
1
- module Selene
2
- module ComponentRules
3
-
4
- def property_rules(component)
5
- {}.tap do |rules|
6
- rules["property %s must not occur more than once"] = lambda do |property, line, message|
7
- invalid = @component.key?(property) && component.class::DISTINCT_PROPERTIES.include?(property)
8
- !invalid.tap { @errors << { :message => message % property } if invalid }
9
- end
10
-
11
- rules["properties '%s' and '%s' cannot occur in the same component"] = lambda do |property, line, message|
12
- passed = true
13
- component.class::EXCLUSIVE_PROPERTIES.each do |properties|
14
- other_property = properties.find { |p| @component.key?(p) && p != property }
15
- next unless other_property
16
- passed = false
17
- @errors << { :message => message % [property, other_property] }
18
- end
19
- end
20
- end
21
- end
22
-
23
- def component_rules(component)
24
- {}.tap do |rules|
25
- rules["missing required property '%s'"] = lambda do |message|
26
- passed = true
27
- component.class::REQUIRED_PROPERTIES.each do |required|
28
- next if @component.key?(required)
29
- passed = false
30
- @errors << { :message => message % required }
31
- end
32
- passed
33
- end
34
- end
35
- end
36
-
37
- end
38
- end