valerie 0.0.6 → 0.0.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4852cdd21e92043de8fce8c61d0772ca050173629b83f4ca937d470e6f8a2704
4
- data.tar.gz: b012526eb2ed715eee6c6a306fcdc6bfae0b4a0f3d5990bdf4f73519f6eb1b6f
3
+ metadata.gz: ea7e19804b751742d2949d006db9f884f3baa7a17d0a0a1f6a6db00196a6c194
4
+ data.tar.gz: 49741e0b54e5c6ee460654d4f7134aae2912e74539625fe8c3b37e8621a23fa6
5
5
  SHA512:
6
- metadata.gz: c045564a3aa04ad66f18fac276a4cd777106f9d43e49f48dde04d1e71132bacba2cc173ad97fcfaf3ad2d8c4c1e3819c01aa9c3c18b46eec0d708dfd48cef7be
7
- data.tar.gz: d412316626501ab8917501cb03eb4b9ba01588fc8714ac9fceb24a3424b8e429cc7a11143ee4b9281f5a816cfce14872e512993f9861cc72eb8036aacd8ad629
6
+ metadata.gz: d2518a65329eb490ad21ce723bcab923c09db8b7b8f02afb678aac41469a6799203fa92cd03f530b6d11d451034d540fa3ce58f5eb7b80f214c2b9ddab75ec98
7
+ data.tar.gz: fa001df7282f3af9a1c1cc2aa3aaf6957b79158640808e45b11e1958e086bbe4ec52b5fb59cc2812b7cf25aa0ec0f8c010df0f81c9d9381d7a8e43572f42cdc6
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # VCard parser and generator
2
+
3
+ This is a simple VCard parser and generator extracted from Hellotext.
4
+ It implements the VCard 3.0 specification. It supports all of the cases we want to use in Hellotext.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'valerie'
12
+ ```
13
+
14
+ Or install it yourself as:
15
+
16
+ ```bash
17
+ gem install valerie
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Parsing a VCard is as simple as passing a VCard string, like
23
+
24
+ ```ruby
25
+ data = "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Hellotext www.hellotext.com//EN\r\nN:Rosenbaum;Shira;;;\r\nTEL;:+598 00 000 00\r\nEND:VCARD"
26
+ Valerie::Card.parse(data)
27
+ ```
28
+
29
+ ## Generating a VCard
30
+
31
+ You usually start by initializing a new Card object.
32
+
33
+ ```ruby
34
+ card = Valerie::Card.new
35
+ ```
36
+
37
+ ### Types of Properties
38
+
39
+ According to the VCard 3.0 specification, some types of properties can have multiple values. From the specs, Valerie supports the following properties:
40
+
41
+ - `EMAIL`: for email addresses
42
+ - `TEL`: for telephone numbers
43
+ - `ADR`: for addresses
44
+
45
+ Aside from these, every other support property supports a single value.
46
+
47
+
48
+ ### Single value properties
49
+
50
+ #### Name
51
+
52
+ To set the name property for the VCard, you can use the `Card#name=` setter. It accepts two types of arguments,
53
+
54
+ A hash that contains the following
55
+
56
+ ```ruby
57
+ {
58
+ first_name: 'Shira',
59
+ last_name: 'Rosenbaum',
60
+ middle_name: 'M',
61
+ prefix: 'Ms.',
62
+ suffix: 'PhD'
63
+ }
64
+ ```
65
+
66
+ Or an array with the following order `[first_name, last_name, middle_name, prefix, suffix]`.
67
+
68
+ ```ruby
69
+ card.name = %w[Shira Rosenbaum M Ms. PhD]
70
+ ```
71
+
72
+ #### Gender
73
+
74
+ To set the Gender of the card you can, set it value `Card#gender=`, this accepts one of the following constants:
75
+ `male`, `female`, `other`, `none` and `unknown`. Passing another value raises an `ArgumentError`.
76
+
77
+ ```ruby
78
+ card.gender = 'female'
79
+ ```
80
+
81
+ #### Birthday
82
+
83
+ The birthday can be set via `Card#birthday=`. It accepts any object that responds to `strftime`, otherwise it stores the `to_s` version of the value.
84
+
85
+ ```ruby
86
+ card.birthday = Date.new(1999, 11, 4)
87
+ ```
88
+
89
+ #### Organization
90
+
91
+ The organization can be an tuple of `[organization_name, department]`, or a hash of `{organization: 'organization_name', department: 'department'}`.
92
+ When a string value is passed, only the `organization_name` is set.
93
+
94
+ ```ruby
95
+ card.organization = %w[Hellotext Engineering]
96
+ card.organization = { organization: 'Hellotext', department: 'Engineering' }
97
+ card.organization = 'Hellotext'
98
+ ```
99
+
100
+ ### Collections
101
+
102
+ The card exposes methods to adding the respective collectable properties. Email, telephone and address.
103
+
104
+ Adding an email,
105
+
106
+ ```ruby
107
+ card.emails.add('user@domain.com')
108
+ ```
109
+
110
+ Or a telephone number,
111
+
112
+ ```ruby
113
+ card.phones.add('+598 00 000 00')
114
+ ```
115
+
116
+ Or an address,
117
+
118
+ ```ruby
119
+ card.addresses.add(
120
+ {
121
+ post_office_box: 'PO Box 123',
122
+ extended_address: 'Suite 123',
123
+ street_address: '123 Main St',
124
+ locality: 'Anytown',
125
+ region: 'CA',
126
+ postal_code: '12345',
127
+ country: 'United States'
128
+ }
129
+ )
130
+ ```
131
+
132
+ #### Positions
133
+
134
+ Emails, phones and addresses are ordered. They can include an optional `position` argument
135
+ to specify their order when the profile has multiple values. Whenever you add a new value,
136
+ a default position argument is picked and the value is appended as the last element.
137
+
138
+ To specify the position, you can pass the `position` argument as a keyword argument. Unlike arrays
139
+ the position is 1-based and not 0-based.
140
+
141
+ ```ruby
142
+ card.emails.add('user@domain.com')
143
+ card.emails.add('user@domain2.com', position: 1) # inserts the email as the first element
144
+ ```
145
+
146
+ #### Types
147
+
148
+ Emails, phones and addresses can have types. The types are defined in the VCard 3.0 specification.
149
+ To specify the type of one of these properties, you can pass the `types` argument as a keyword argument.
150
+
151
+ ```ruby
152
+ card.emails.add('user@domain.com', type: 'work')
153
+ card.emails.add('user@domain.com', type: %w[work internet])
154
+ ```
155
+
156
+ ### Configuration
157
+
158
+ You can configure the following properties of the card.
159
+
160
+ - `prodid`: The product id of the card. This defaults to 'Valerie www.hellotext.com'.
161
+ - `version`: The version of the card. This defaults to '3.0'.
162
+ - `language`: The language of the card. This defaults to 'en'.
163
+
164
+ ```ruby
165
+ Valerie.configure do |config|
166
+ config.product = 'Valerie www.hellotext.com'
167
+ config.version = '3.0'
168
+ config.language = 'en'
169
+ end
170
+ ```
171
+
172
+ ### Licence
173
+
174
+ This code is released under the MIT License. See the LICENSE file for more information.
175
+
176
+ ### Acknowledgements
177
+
178
+ - [VCardigan](https://github.com/brewster/vcardigan)
179
+
180
+ ### Contributing
181
+
182
+ Contributions are welcome. Please follow the steps below to contribute.
183
+
184
+ 1. Fork it
185
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
186
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
187
+ 4. Push to the branch (`git push origin my-new-feature`)
188
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |task|
4
+ task.pattern = 'test/**/*_test.rb'
5
+ end
6
+
7
+ desc 'Run tests'
8
+ task default: :test
@@ -0,0 +1,70 @@
1
+ require_relative 'ordered'
2
+
3
+ module Valerie
4
+ class Address
5
+ include Ordered
6
+
7
+ def self.from_s(data)
8
+ data = data[data.index("ADR;")..] unless data.start_with?("ADR;")
9
+ identifier = data.split(":").last.split(";")
10
+ options = data.gsub("ADR", "").split(":").first.split(";").compact.filter { _1.to_s.include?('=')}.map { _1.downcase.split("=") }.to_h
11
+
12
+ new(
13
+ post_office_box: identifier[0],
14
+ extended_address: identifier[1],
15
+ street_address: identifier[2],
16
+ locality: identifier[3],
17
+ region: identifier[4],
18
+ postal_code: identifier[5],
19
+ country: identifier[6],
20
+ **options
21
+ )
22
+ end
23
+
24
+ def initialize(post_office_box:, extended_address:, street_address:, locality:, region:, postal_code:, country:, **options)
25
+ @post_office_box = post_office_box
26
+ @extended_address = extended_address
27
+ @street_address = street_address
28
+ @locality = locality
29
+ @region = region
30
+ @postal_code = postal_code
31
+ @country = country
32
+ @options = options
33
+
34
+ raise ArgumentError, 'Invalid Position' if invalid_position?
35
+ end
36
+
37
+ def [](key)
38
+ @options[key]
39
+ end
40
+
41
+ def to_s
42
+ parts = ['ADR']
43
+
44
+ parts << "PERF=#{position}" if position?
45
+
46
+ @options.map do |key, value|
47
+ next if key == :position
48
+ parts << "#{key}=#{value}"
49
+ end
50
+
51
+ parts.join(';') + ":#{identifier}"
52
+ end
53
+
54
+ def identifier
55
+ [@post_office_box, @extended_address, @street_address, @locality, @region, @postal_code, @country].join(';')
56
+ end
57
+
58
+ def to_h
59
+ {
60
+ post_office_box: @post_office_box,
61
+ extended_address: @extended_address,
62
+ street_address: @street_address,
63
+ locality: @locality,
64
+ region: @region,
65
+ postal_code: @postal_code,
66
+ country: @country
67
+ }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,46 @@
1
+ require 'date'
2
+
3
+ module Valerie
4
+ class Birthday
5
+ def self.from_s(data)
6
+ new data.gsub("BDAY:", "")
7
+ end
8
+
9
+ def initialize(date)
10
+ if date.respond_to?(:strftime)
11
+ @date = date.strftime("%Y-%m-%d")
12
+ else
13
+ @date = date
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ "BDAY:#{@date}"
19
+ end
20
+
21
+ def to_date
22
+ Date.new(year, month, day)
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ day:,
28
+ month:,
29
+ year:
30
+ }
31
+ end
32
+
33
+ private
34
+ def year
35
+ @year ||= @date.split('-')[0].to_i
36
+ end
37
+
38
+ def month
39
+ @month ||= @date.split('-')[1].to_i
40
+ end
41
+
42
+ def day
43
+ @day ||= @date.split('-')[-1].to_i
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ require_relative '../valerie'
2
+ require_relative 'core/parser'
3
+
4
+ module Valerie
5
+ class Card
6
+ extend Core::Parser
7
+
8
+ attr_reader :name, :formatted_name, :organization, :gender, :birthday
9
+
10
+ def name=(parts)
11
+ if parts.instance_of?(Name)
12
+ @name = parts
13
+ else
14
+ @name = Name.new parts
15
+ end
16
+
17
+ if (@name.first_name || @name.last_name) && @formatted_name.nil?
18
+ @formatted_name = "#{@name.first_name} #{@name.last_name}".strip
19
+ end
20
+
21
+ @name
22
+ end
23
+
24
+ def organization=(*args)
25
+ if args.first.instance_of?(Organization)
26
+ @organization = args.flatten.first
27
+ else
28
+ @organization = Organization.new(*args)
29
+ end
30
+ end
31
+
32
+ def gender=(value)
33
+ @gender = value.is_a?(Gender) ? value : Gender.new(value)
34
+ end
35
+
36
+ def birthday=(value)
37
+ @birthday = value.is_a?(Birthday) ? value : Birthday.new(value)
38
+ end
39
+
40
+ def emails
41
+ @emails ||= Collection::EmailCollection.new
42
+ end
43
+
44
+ def addresses
45
+ @addresses ||= Collection::AddressCollection.new
46
+ end
47
+
48
+ def phones
49
+ @phones ||= Collection::PhoneCollection.new
50
+ end
51
+
52
+ def to_s
53
+ to_a.join("\r\n")
54
+ end
55
+
56
+ def to_a
57
+ parts = [
58
+ 'BEGIN:VCARD',
59
+ "VERSION:#{Valerie.configuration.version}",
60
+ "PRODID:-//#{Valerie.configuration.product}//#{Valerie.configuration.language}",
61
+ ]
62
+
63
+ parts << @name.to_s if @name
64
+ parts << @organization.to_s if @organization
65
+ parts << @birthday.to_s if @birthday
66
+ parts << @gender.to_s if @gender
67
+
68
+ emails.each do |email|
69
+ parts << email.to_s
70
+ end
71
+
72
+ phones.each do |phone|
73
+ parts << phone.to_s
74
+ end
75
+
76
+ addresses.each do |address|
77
+ parts << address.to_s
78
+ end
79
+
80
+ parts << 'END:VCARD'
81
+ parts
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'collection'
2
+ require_relative '../address'
3
+
4
+ module Valerie
5
+ module Collection
6
+ class AddressCollection < Base
7
+ def add(address, **options)
8
+ @item = if address.is_a?(Address)
9
+ address.dup { _1.position = options[:position] || @items.size + 1 }
10
+ else
11
+ Address.new(**address, **options)
12
+ end
13
+
14
+ @items << @item
15
+ @items = @items.sort_by(&:position)
16
+
17
+ @item
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Valerie
2
+ module Collection
3
+ class Base
4
+ include Enumerable
5
+
6
+ attr_reader :items
7
+
8
+ def initialize
9
+ @items = []
10
+ end
11
+
12
+ def each(&block)
13
+ items.each(&block)
14
+ end
15
+
16
+ def add
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def blank?
21
+ @items.empty?
22
+ end
23
+
24
+ def present?
25
+ !blank?
26
+ end
27
+
28
+ def size
29
+ @items.size
30
+ end
31
+
32
+ def count
33
+ @items.count
34
+ end
35
+
36
+ def length
37
+ @items.length
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'collection'
2
+ require_relative '../email'
3
+
4
+ module Valerie
5
+ module Collection
6
+ class EmailCollection < Base
7
+ def add(email, **options)
8
+ @item = if email.is_a?(Email)
9
+ email.dup.tap { _1.position = options[:position] || @items.size + 1 }
10
+ elsif email.is_a?(Hash)
11
+ Email.new(address: email[:address], **email, **options, position: options[:position] || @items.size + 1)
12
+ else
13
+ Email.new(address: email, **options, position: options[:position] || @items.size + 1)
14
+ end
15
+
16
+ @items << @item
17
+ @items = @items.sort_by(&:position)
18
+
19
+ @item
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'collection'
2
+ require_relative '../phone'
3
+
4
+ module Valerie
5
+ module Collection
6
+ class PhoneCollection < Base
7
+ def add(phone, **options)
8
+ @item = if phone.is_a?(Phone)
9
+ phone.dup.tap { _1.position = options[:position] || @items.size + 1 }
10
+ else
11
+ Phone.new(phone, **options, position: options[:position] || @items.size + 1)
12
+ end
13
+
14
+ @items << @item
15
+ @items = @items.sort_by(&:position)
16
+
17
+ @item
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,90 @@
1
+ require_relative '../../valerie'
2
+
3
+ module Valerie
4
+ module Core
5
+ module Parser
6
+ def parse(data)
7
+ if data.instance_of?(String)
8
+ from_s(data)
9
+ elsif data.instance_of?(Array)
10
+ from_a(data)
11
+ else
12
+ raise ArgumentError, "Expected String or Array, got #{data.class}"
13
+ end
14
+ end
15
+
16
+ private
17
+ def from_a(data)
18
+ Card.new.tap do |vcard|
19
+ if (name = data.find { _1.start_with?("N:") })
20
+ vcard.name = Name.from_s name.split(":").last
21
+ end
22
+
23
+ if (organization = data.find { _1.start_with?("ORG:") })
24
+ vcard.organization = Organization.from_s(organization)
25
+ end
26
+
27
+ if (birthday = data.find { _1.start_with?("BDAY:") })
28
+ vcard.birthday = Birthday.from_s(birthday)
29
+ end
30
+
31
+ if (gender = data.find { _1.start_with?("GENDER:") })
32
+ vcard.gender = gender.split(":").last
33
+ end
34
+
35
+ data.select { _1.include?("TEL;") }.each do |phone|
36
+ vcard.phones.add(Phone.from_s(phone))
37
+ end
38
+
39
+ data.select { _1.start_with?("EMAIL;") }.each do |email|
40
+ vcard.emails.add(Email.from_s(email))
41
+ end
42
+ end
43
+ end
44
+
45
+ def from_s(str)
46
+ vcards = []
47
+ vcard = []
48
+
49
+ unfold(str).each do |entry|
50
+ if entry.include?("VERSION:")
51
+ vcards << from_a(vcard) unless vcard.empty?
52
+ vcard = []
53
+ else
54
+ vcard << entry
55
+ end
56
+ end
57
+
58
+ vcards << from_a(vcard)
59
+
60
+ vcards
61
+ end
62
+
63
+ UNTERMINATED_QUOTED_PRINTABLE = /ENCODING=QUOTED-PRINTABLE:.*=$/
64
+
65
+ def unfold(vcard)
66
+ unfolded = []
67
+
68
+ prior_line = nil
69
+ vcard.lines do |line|
70
+ line.chomp!
71
+ # If it's a continuation line, add it to the last.
72
+ # If it's an empty line, drop it from the input.
73
+ if line =~ /^[ \t]/
74
+ unfolded[-1] << line[1, line.size - 1]
75
+ elsif line =~ /(^BEGIN:VCARD$)|(^END:VCARD$)/
76
+ elsif prior_line && (prior_line =~ UNTERMINATED_QUOTED_PRINTABLE)
77
+ # Strip the trailing = off prior line, then append current line
78
+ unfolded[-1] = prior_line[0, prior_line.length - 1] + line
79
+ elsif line =~ /^$/
80
+ else
81
+ unfolded << line
82
+ end
83
+ prior_line = unfolded[-1]
84
+ end
85
+
86
+ unfolded
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'ordered'
2
+
3
+ module Valerie
4
+ class Email
5
+ include Ordered
6
+
7
+ def self.from_s(data)
8
+ data = data[data.index("EMAIL;")..] unless data.start_with?("EMAIL;")
9
+ identifier = data.split(":").last.split(";")
10
+ options = data.gsub("EMAIL", "").split(":").first.split(";").compact.filter { _1.to_s.include?('=')}.map { _1.downcase.split("=") }.to_h
11
+
12
+ new(
13
+ address: identifier[0],
14
+ **options
15
+ )
16
+ end
17
+
18
+ attr_reader :address, :options
19
+
20
+ def initialize(address:, **options)
21
+ @address = address
22
+ @options = options
23
+
24
+ raise ArgumentError, 'Invalid Position' if invalid_position?
25
+ end
26
+
27
+ def [](key)
28
+ @options[key]
29
+ end
30
+
31
+ def to_s
32
+ parts = ['EMAIL']
33
+
34
+ parts << "PERF=#{@options[:position]}" if position?
35
+ parts << type_to_s if type?
36
+
37
+ parts.join(';') + ":#{@address}"
38
+ end
39
+
40
+ def to_h
41
+ {
42
+ address: @address,
43
+ position: @options[:position],
44
+ type: @options[:type],
45
+ }
46
+ end
47
+
48
+ private
49
+ def type?
50
+ @options[:type]
51
+ end
52
+
53
+ def type_to_s
54
+ if @options[:type].is_a?(Array)
55
+ "TYPE=\"#{@options[:type].join(',')}\""
56
+ else
57
+ "TYPE=#{@options[:type]}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ module Valerie
2
+ class Gender
3
+ SEX_COMPONENTS = {
4
+ male: 'M',
5
+ female: 'F',
6
+ other: 'O',
7
+ unknown: 'U',
8
+ none: 'N'
9
+ }
10
+
11
+ attr_reader :identifier
12
+
13
+ def self.from_s(data)
14
+ new(data.split(":").last)
15
+ end
16
+
17
+ def initialize(identifier)
18
+ if SEX_COMPONENTS.keys.include?(identifier.to_s.downcase.to_sym)
19
+ @identifier = SEX_COMPONENTS[identifier.to_s.downcase.to_sym]
20
+ else
21
+ @identifier = identifier
22
+ end
23
+
24
+ raise ArgumentError, 'Invalid Sex identifier' unless SEX_COMPONENTS.values.include?(@identifier)
25
+ end
26
+
27
+ def to_s
28
+ "GENDER:#{@identifier}"
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ identifier: @identifier,
34
+ sex:,
35
+ }
36
+ end
37
+
38
+ def sex
39
+ @sex ||= SEX_COMPONENTS.key(@identifier)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ module Valerie
2
+ class Name
3
+ class << self
4
+ def new(parts)
5
+ if parts.is_a?(Hash)
6
+ super **parts
7
+ else
8
+ first_name, last_name = parts.split(" ")
9
+
10
+ super first_name:, last_name:
11
+ end
12
+ end
13
+
14
+ def from_s(data)
15
+ parts = data.gsub("N:", "").split(";")
16
+ new first_name: parts[1], last_name: parts[0], middle_name: parts[2], prefix: parts[3], suffix: parts[4]
17
+ end
18
+ end
19
+
20
+ attr_reader :first_name, :last_name
21
+
22
+ def initialize(first_name: nil, last_name: nil, middle_name: nil, prefix: nil, suffix: nil)
23
+ @first_name = first_name
24
+ @last_name = last_name
25
+ @middle_name = middle_name
26
+ @prefix = prefix
27
+ @suffix = suffix
28
+ end
29
+
30
+ def to_s
31
+ "N:#{[@last_name, @first_name, @middle_name, @prefix, @suffix].join(";")}"
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ first_name: @first_name,
37
+ last_name: @last_name,
38
+ middle_name: @middle_name,
39
+ prefix: @prefix,
40
+ suffix: @suffix,
41
+ }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ module Valerie
2
+ module Ordered
3
+ def invalid_position?
4
+ position? && position.to_i < 1
5
+ end
6
+
7
+ def position
8
+ @options[:position]
9
+ end
10
+
11
+ def position=(value)
12
+ @options[:position] = value
13
+ end
14
+
15
+ def position?
16
+ position
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ module Valerie
2
+ class Organization
3
+ attr_reader :name, :department
4
+
5
+ class << self
6
+ def new(*organization)
7
+ if organization.first.is_a?(Hash)
8
+ super(name: organization.first[:name], department: organization.first[:department])
9
+ else
10
+ name, department = organization.flatten
11
+ super(name:, department:)
12
+ end
13
+ end
14
+
15
+ def from_s(data)
16
+ name, department = data.gsub("ORG:", "").split(";")
17
+ new(name:, department:)
18
+ end
19
+ end
20
+
21
+ def initialize(name:, department: nil, **options)
22
+ @name, @department = name, department
23
+ @options = options
24
+ end
25
+
26
+ def to_s
27
+ "ORG:#{@name};#{department_part}"
28
+ end
29
+
30
+ def present?
31
+ @name.to_s.length > 0 || @department.to_s.length > 0
32
+ end
33
+
34
+ private
35
+ def department_part
36
+ if @department.to_s.empty?
37
+ fallback_department
38
+ else
39
+ @department
40
+ end
41
+ end
42
+
43
+ def fallback_department
44
+ " \n"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'ordered'
2
+
3
+ module Valerie
4
+ class Phone
5
+ VALID_TYPES = %w[text voice fax cell video pager textphone].freeze
6
+
7
+ include Ordered
8
+
9
+ def self.from_s(data)
10
+ data = data[data.index("TEL;")..] unless data.start_with?("TEL;")
11
+ identifier = data.split(":").last
12
+ options = data.gsub("TEL", "").split(":").first.split(";").compact.filter { _1.to_s.include?('=')}.map { _1.downcase.split("=") }.to_h
13
+
14
+ new(identifier, **options)
15
+ end
16
+
17
+ attr_reader :number, :options
18
+
19
+ def initialize(number, **options)
20
+ @number = number
21
+ @options = options.transform_keys!(&:to_sym)
22
+ @type = @options[:type] || 'voice'
23
+
24
+ raise ArgumentError, 'Invalid Position' if invalid_position?
25
+ end
26
+
27
+ def [](key)
28
+ @options[key]
29
+ end
30
+
31
+ def to_s
32
+ parts = ['TEL']
33
+
34
+ parts << "PERF=#{@options[:position]}" if position?
35
+ parts << type_to_s if type?
36
+
37
+ parts.join(';') + ":#{@number}"
38
+ end
39
+
40
+ def to_h
41
+ {
42
+ number: @tel,
43
+ type: @type,
44
+ position: @options[:position],
45
+ }
46
+ end
47
+
48
+ private
49
+ def type?
50
+ @options[:type]
51
+ end
52
+
53
+ def type_to_s
54
+ if @options[:type].is_a?(Array)
55
+ "TYPE=\"#{@options[:type].join(',')}\""
56
+ else
57
+ "TYPE=#{@options[:type]}"
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/valerie.rb CHANGED
@@ -12,7 +12,7 @@ require 'valerie/collection/address_collection'
12
12
  require 'valerie/collection/phone_collection'
13
13
 
14
14
  module Valerie
15
- VERSION = '0.0.6'.freeze
15
+ VERSION = '0.0.7'.freeze
16
16
 
17
17
  def self.configuration
18
18
  @configuration ||= Configuration.new
@@ -0,0 +1,26 @@
1
+ require_relative 'test_helper'
2
+
3
+ class BirthdayTest < Minitest::Test
4
+ def test_from_s
5
+ Valerie::Birthday.from_s('BDAY:1999-11-04').to_date.tap do |date|
6
+ assert_equal(date.year, 1999)
7
+ assert_equal(date.month, 11)
8
+ assert_equal(date.day, 4)
9
+ end
10
+ end
11
+
12
+ def test_to_s
13
+ assert_equal(
14
+ Valerie::Birthday.from_s('BDAY:1999-11-04').to_s,
15
+ 'BDAY:1999-11-04'
16
+ )
17
+ end
18
+
19
+ def test_to_h
20
+ Valerie::Birthday.from_s('BDAY:1999-11-04').to_h.tap do |hash|
21
+ assert_equal(hash[:year], 1999)
22
+ assert_equal(hash[:month], 11)
23
+ assert_equal(hash[:day], 4)
24
+ end
25
+ end
26
+ end
data/test/card_test.rb ADDED
@@ -0,0 +1,73 @@
1
+ require_relative 'test_helper'
2
+
3
+ class VCardTest < Minitest::Test
4
+ def initialize(name)
5
+ super
6
+ @card = Valerie::Card.new
7
+ end
8
+
9
+ def test_setting_name_with_hash_parameter
10
+ @card.name = { first_name: 'John', last_name: 'Doe' }
11
+
12
+ assert_equal(@card.name.first_name, 'John')
13
+ assert_equal(@card.name.last_name, 'Doe')
14
+ end
15
+
16
+ def test_setting_name_with_string_parameter
17
+ @card.name = 'Adam Hull'
18
+
19
+ assert_equal(@card.name.first_name, 'Adam')
20
+ assert_equal(@card.name.last_name, 'Hull')
21
+ end
22
+
23
+ def test_setting_organization_with_hash
24
+ @card.organization = { name: 'Hellotext', department: 'Product Development' }
25
+
26
+ assert_equal(@card.organization.name, 'Hellotext')
27
+ assert_equal(@card.organization.department, 'Product Development')
28
+ end
29
+
30
+ def test_setting_organization_with_string
31
+ @card.organization = ['Hellotext', 'Product Development']
32
+
33
+ assert_equal(@card.organization.name, 'Hellotext')
34
+ assert_equal(@card.organization.department, 'Product Development')
35
+ end
36
+
37
+ def test_setting_gender_with_valid_identifier
38
+ assert_runs_without_errors do
39
+ @card.gender = 'Male'
40
+ assert_equal(@card.gender.sex, :male)
41
+ end
42
+ end
43
+
44
+ def test_setting_gender_with_invalid_identifier
45
+ assert_raises(ArgumentError) do
46
+ @card.gender = 'invalid-identifier'
47
+ end
48
+ end
49
+
50
+ def test_parsing_array
51
+ data = ['PRODID:-//Hellotext', 'N:Doe;John;;;', "ORG:HelloText;", "TEL;type=CELL:+598 94 000 000"]
52
+
53
+ Valerie::Card.parse(data).tap do |vcard|
54
+ assert_equal(vcard.name.first_name, 'John')
55
+ assert_equal(vcard.name.last_name, 'Doe')
56
+
57
+ assert_equal(vcard.organization.name, 'HelloText')
58
+
59
+ assert_equal(vcard.phones.first.number, '+598 94 000 000')
60
+ assert_equal(vcard.phones.first.options[:type], 'cell')
61
+ end
62
+ end
63
+
64
+ def test_parsing_string
65
+ data = "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Hellotext www.hellotext.com//EN\r\nN:Rosenbaum;Shira;;;\r\nTEL;:+598 94 987 924\r\nEND:VCARD"
66
+
67
+ Valerie::Card.parse(data).first.tap do |vcard|
68
+ assert_equal(vcard.name.first_name, 'Shira')
69
+ assert_equal(vcard.name.last_name, 'Rosenbaum')
70
+ assert_equal(vcard.phones.first.number, '+598 94 987 924')
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,39 @@
1
+ require_relative '../test_helper'
2
+
3
+ class EmailCollectionTest < Minitest::Test
4
+ def setup
5
+ @collection = Valerie::Collection::EmailCollection.new
6
+ end
7
+
8
+ def test_add_email_as_string
9
+ email = @collection.add('ahmed@hellotext.com', type: :work)
10
+
11
+ assert_instance_of(Valerie::Email, email)
12
+
13
+ assert_equal(email.address, 'ahmed@hellotext.com')
14
+ assert_equal(email[:type], :work)
15
+ end
16
+
17
+ def test_add_email_as_hash
18
+ email = @collection.add({ address: 'john@hellotext.com', type: :home })
19
+
20
+ assert_instance_of(Valerie::Email, email)
21
+ assert_equal(email.address, 'john@hellotext.com')
22
+ assert_equal(email[:type], :home)
23
+ end
24
+
25
+ def test_add_email_as_email_object
26
+ email = Valerie::Email.new(address: 'ahmed@hellotext.com', type: :work)
27
+ @collection.add({ address: 'john@hellotext.com', type: :home })
28
+
29
+ assert_equal(@collection.add(email).position, 2)
30
+ end
31
+
32
+ def test_setting_the_position_for_each_inserted_email
33
+ email = @collection.add('ahmed@hellotext.com')
34
+ email2 = @collection.add('john@hellotext.com')
35
+
36
+ assert_equal(email[:position], 1)
37
+ assert_equal(email2[:position], 2)
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../test_helper'
2
+
3
+ class PhoneCollectionTest < Minitest::Test
4
+ def setup
5
+ @collection = Valerie::Collection::PhoneCollection.new
6
+ end
7
+
8
+ def test_add_phone_as_string
9
+ phone = @collection.add('1234567890', type: :work)
10
+
11
+ assert_instance_of(Valerie::Phone, phone)
12
+
13
+ assert_equal(phone.number, '1234567890')
14
+ assert_equal(phone[:type], :work)
15
+ end
16
+
17
+ def test_add_phone_as_phone_object
18
+ phone = Valerie::Phone.new('1234567890', type: :work)
19
+ @collection.add('0987654321', type: :home)
20
+
21
+ assert_equal(@collection.add(phone).position, 2)
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'test_helper'
2
+
3
+ class EmailTest < Minitest::Test
4
+ def test_to_s_with_single_type
5
+ Valerie::Email.new(address: 'ahmed@hellotext.com', type: :work).tap do |email|
6
+ assert_equal(email.to_s.start_with?('EMAIL;TYPE=work:'), true)
7
+ end
8
+ end
9
+
10
+ def test_with_invalid_position
11
+ error = assert_raises(ArgumentError) do
12
+ Valerie::Email.new(address: 'ahmed@hellotext.com', position: -1)
13
+ end
14
+
15
+ assert_equal(error.message, 'Invalid Position')
16
+ end
17
+
18
+ def test_to_s_with_preferred
19
+ Valerie::Email.new(address: 'ahmed@hellotext.com', type: :work, position: 1).tap do |email|
20
+ assert_equal(
21
+ email.to_s.include?(';PERF=1;'),
22
+ true
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'test_helper'
2
+
3
+ class GenderTest < Minitest::Test
4
+ def test_from_s
5
+ identifier = 'GENDER:M'
6
+
7
+ Valerie::Gender.from_s(identifier).tap do |entry|
8
+ assert_equal(entry.identifier, 'M')
9
+ end
10
+ end
11
+
12
+ def test_initialize_with_valid_sex_component_key
13
+ assert_runs_without_errors do
14
+ Valerie::Gender.new('male').tap do |entry|
15
+ assert_equal(entry.identifier, 'M')
16
+ assert_equal(entry.sex, :male)
17
+ end
18
+ end
19
+ end
20
+
21
+ def test_initialize_with_invalid_identifier
22
+ assert_raises(ArgumentError) do
23
+ Valerie::Gender.new('invalid-identifier')
24
+ end
25
+ end
26
+
27
+ def test_to_s
28
+ assert_equal(Valerie::Gender.new('M').to_s, 'GENDER:M')
29
+ end
30
+
31
+ def test_to_h
32
+ Valerie::Gender.new('M').to_h.tap do |hash|
33
+ assert_equal(hash[:identifier], 'M')
34
+ end
35
+ end
36
+ end
data/test/name_test.rb ADDED
@@ -0,0 +1,17 @@
1
+ require_relative 'test_helper'
2
+
3
+ class NameTest < Minitest::Test
4
+ def test_initialize_with_hash
5
+ Valerie::Name.new(first_name: 'Ahmed', last_name: 'Khattab').tap do |name|
6
+ assert_equal(name.to_h[:first_name], 'Ahmed')
7
+ assert_equal(name.to_h[:last_name], 'Khattab')
8
+ end
9
+ end
10
+
11
+ def test_initialize_with_string
12
+ Valerie::Name.new('Ahmed Khattab').tap do |name|
13
+ assert_equal(name.to_h[:first_name], 'Ahmed')
14
+ assert_equal(name.to_h[:last_name], 'Khattab')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'test_helper'
2
+
3
+ class OrganizationTest < Minitest::Test
4
+ def test_new_constructor_with_array
5
+ Valerie::Organization.new('Hellotext', 'Product Engineer').tap do |org|
6
+ assert_equal(org.name, 'Hellotext')
7
+ assert_equal(org.department, 'Product Engineer')
8
+ end
9
+ end
10
+
11
+ def test_new_constructor_with_hash
12
+ Valerie::Organization.new(name: 'Hellotext', department: 'Product Engineer').tap do |org|
13
+ assert_equal(org.name, 'Hellotext')
14
+ assert_equal(org.department, 'Product Engineer')
15
+ end
16
+ end
17
+
18
+ def test_from_s
19
+ Valerie::Organization.from_s('ORG:Hellotext;Product Engineer').tap do |org|
20
+ assert_equal(org.name, 'Hellotext')
21
+ assert_equal(org.department, 'Product Engineer')
22
+ end
23
+ end
24
+
25
+ def test_to_s
26
+ Valerie::Organization.new(name: 'Hellotext', department: 'Product Engineer').tap do |org|
27
+ assert_equal(org.to_s, 'ORG:Hellotext;Product Engineer')
28
+ end
29
+ end
30
+
31
+ def test_present_when_name_is_set
32
+ Valerie::Organization.new(name: 'Hellotext').tap do |org|
33
+ assert_equal(org.present?, true)
34
+ end
35
+ end
36
+
37
+ def test_present_when_department_is_set
38
+ Valerie::Organization.new(department: 'Product Engineer').tap do |org|
39
+ assert_equal(org.present?, true)
40
+ end
41
+ end
42
+
43
+ def test_present_when_none_is_set
44
+ Valerie::Organization.new.tap do |org|
45
+ assert_equal(org.present?, false)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'test_helper'
2
+
3
+ class PhoneTest < Minitest::Test
4
+ def test_to_s_with_single_type
5
+ Valerie::Phone.new('01000000000', type: :voice).tap do |phone|
6
+ assert_equal(phone.to_s.start_with?('TEL;TYPE=voice:'), true)
7
+ end
8
+ end
9
+
10
+ def test_to_s_with_multiple_types
11
+ Valerie::Phone.new('01000000000', type: %i[voice work]).tap do |phone|
12
+ assert_equal(phone.to_s, 'TEL;TYPE="voice,work":01000000000')
13
+ end
14
+ end
15
+
16
+ def test_with_invalid_position
17
+ error = assert_raises(ArgumentError) do
18
+ Valerie::Phone.new('01000000000', position: -1)
19
+ end
20
+
21
+ assert_equal(error.message, 'Invalid Position')
22
+ end
23
+
24
+ def test_to_s_with_position
25
+ Valerie::Phone.new('01000000000', type: :voice, position: 1).tap do |phone|
26
+ assert_equal(phone.to_s, 'TEL;PERF=1;TYPE=voice:01000000000')
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../lib/valerie'
3
+
4
+ module Minitest::Assertions
5
+ def assert_runs_without_errors(*)
6
+ yield
7
+ end
8
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: valerie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hellotext
@@ -45,7 +45,33 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - README.md
49
+ - Rakefile
48
50
  - lib/valerie.rb
51
+ - lib/valerie/address.rb
52
+ - lib/valerie/birthday.rb
53
+ - lib/valerie/card.rb
54
+ - lib/valerie/collection/address_collection.rb
55
+ - lib/valerie/collection/collection.rb
56
+ - lib/valerie/collection/email_collection.rb
57
+ - lib/valerie/collection/phone_collection.rb
58
+ - lib/valerie/core/parser.rb
59
+ - lib/valerie/email.rb
60
+ - lib/valerie/gender.rb
61
+ - lib/valerie/name.rb
62
+ - lib/valerie/ordered.rb
63
+ - lib/valerie/organization.rb
64
+ - lib/valerie/phone.rb
65
+ - test/birthday_test.rb
66
+ - test/card_test.rb
67
+ - test/collection/email_collection_test.rb
68
+ - test/collection/phone_collection_test.rb
69
+ - test/email_test.rb
70
+ - test/gender_test.rb
71
+ - test/name_test.rb
72
+ - test/organization_test.rb
73
+ - test/phone_test.rb
74
+ - test/test_helper.rb
49
75
  homepage: https://github.com/hellotext/valerie
50
76
  licenses:
51
77
  - MIT