valerie 0.0.8 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fcc95de73f6b9d8ace62e7fbea8a9862577be748b87b087c465f84964da5b01
4
- data.tar.gz: 7cc7925d3b688f0a31a0d791d281f7b5d94333e88c2c2f57bb24dd188067f116
3
+ metadata.gz: b91f4c4cf72b67b12a9d9b801981c087a70fd9fd999e2c86b864f58ef002c074
4
+ data.tar.gz: a20aa5ec46fda11f4ee95b1f560af1a7c9468ea0773f465a00cbb6a64e617d6e
5
5
  SHA512:
6
- metadata.gz: 05ed263488558b7720e60bdd8652a703a7610fc2c499bdf642363476380ddccc25a5d764c77cd5fae56a346ee8d72a5a7cd4fd14fd82b1a759ae0a3ddcd6cdab
7
- data.tar.gz: a5f3112e7fd9ac862747799fab885f71b7e8cac092c2df3722e6396d5cf69dd7109af54b171285e7c94c538057f7c9706770abcf02556e1564e02d03f2482bfa
6
+ metadata.gz: 10a5580d8e3239ab5a3a1690dd00844d22e08fa8549758cfbf1711bf0ffc40cef476531a0aacea6978b0bffe89e374b70313ff459636b48be53a3476743c58e0
7
+ data.tar.gz: 8f332e9666ea5fbcf51f0c0984c5571e6d81f7782925e40481b91e41ed240588004e5b3307757375de8cafcd7e8c3453662ddabc60785487a4e7759b2f9c352d
data/README.md CHANGED
@@ -11,7 +11,7 @@ Add this line to your application's Gemfile:
11
11
  gem 'valerie'
12
12
  ```
13
13
 
14
- Or install it yourself as:
14
+ Or install it yourself as:
15
15
 
16
16
  ```bash
17
17
  gem install valerie
@@ -19,7 +19,7 @@ gem install valerie
19
19
 
20
20
  ## Usage
21
21
 
22
- Parsing a VCard is as simple as passing a VCard string, like
22
+ Parsing a VCard is as simple as passing a VCard string, like
23
23
 
24
24
  ```ruby
25
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"
@@ -42,8 +42,7 @@ According to the VCard 3.0 specification, some types of properties can have mult
42
42
  - `TEL`: for telephone numbers
43
43
  - `ADR`: for addresses
44
44
 
45
- Aside from these, every other support property supports a single value.
46
-
45
+ Aside from these, every other support property supports a single value.
47
46
 
48
47
  ### Single value properties
49
48
 
@@ -69,9 +68,9 @@ Or an array with the following order `[first_name, last_name, middle_name, prefi
69
68
  card.name = %w[Shira Rosenbaum M Ms. PhD]
70
69
  ```
71
70
 
72
- #### Gender
71
+ #### Gender
73
72
 
74
- To set the Gender of the card you can, set it value `Card#gender=`, this accepts one of the following constants:
73
+ To set the Gender of the card you can, set it value `Card#gender=`, this accepts one of the following constants:
75
74
  `male`, `female`, `other`, `none` and `unknown`. Passing another value raises an `ArgumentError`.
76
75
 
77
76
  ```ruby
@@ -97,7 +96,7 @@ card.organization = { organization: 'Hellotext', department: 'Engineering' }
97
96
  card.organization = 'Hellotext'
98
97
  ```
99
98
 
100
- ### Collections
99
+ ### Collections
101
100
 
102
101
  The card exposes methods to adding the respective collectable properties. Email, telephone and address.
103
102
 
@@ -129,11 +128,11 @@ card.addresses.add(
129
128
  )
130
129
  ```
131
130
 
132
- #### Positions
131
+ #### Positions
133
132
 
134
- Emails, phones and addresses are ordered. They can include an optional `position` argument
133
+ Emails, phones and addresses are ordered. They can include an optional `position` argument
135
134
  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.
135
+ a default position argument is picked and the value is appended as the last element.
137
136
 
138
137
  To specify the position, you can pass the `position` argument as a keyword argument. Unlike arrays
139
138
  the position is 1-based and not 0-based.
@@ -153,9 +152,9 @@ card.emails.add('user@domain.com', type: 'work')
153
152
  card.emails.add('user@domain.com', type: %w[work internet])
154
153
  ```
155
154
 
156
- ### Configuration
155
+ ### Configuration
157
156
 
158
- You can configure the following properties of the card.
157
+ You can configure the following properties of the card.
159
158
 
160
159
  - `prodid`: The product id of the card. This defaults to 'Valerie www.hellotext.com'.
161
160
  - `version`: The version of the card. This defaults to '3.0'.
@@ -169,6 +168,24 @@ Valerie.configure do |config|
169
168
  end
170
169
  ```
171
170
 
171
+ ### Documentation
172
+
173
+ Valerie uses [YARD](https://yardoc.org/) for API documentation. To generate the documentation locally:
174
+
175
+ ```bash
176
+ # Install dependencies
177
+ bundle install
178
+
179
+ # Generate documentation
180
+ bundle exec rake yard
181
+
182
+ # Or generate and view
183
+ bundle exec rake doc
184
+ open doc/index.html
185
+ ```
186
+
187
+ The documentation covers all public APIs with examples and usage information.
188
+
172
189
  ### Licence
173
190
 
174
191
  This code is released under the MIT License. See the LICENSE file for more information.
@@ -181,7 +198,7 @@ This code is released under the MIT License. See the LICENSE file for more infor
181
198
 
182
199
  Contributions are welcome. Please follow the steps below to contribute.
183
200
 
184
- 1. Fork it
201
+ 1. Fork it
185
202
  2. Create your feature branch (`git checkout -b my-new-feature`)
186
203
  3. Commit your changes (`git commit -am 'Add some feature'`)
187
204
  4. Push to the branch (`git push origin my-new-feature`)
data/Rakefile CHANGED
@@ -6,3 +6,27 @@ end
6
6
 
7
7
  desc 'Run tests'
8
8
  task default: :test
9
+
10
+ # YARD documentation task
11
+ begin
12
+ require 'yard'
13
+
14
+ YARD::Rake::YardocTask.new do |t|
15
+ t.files = ['lib/**/*.rb']
16
+ t.options = ['--markup', 'markdown', '--title', 'Valerie Documentation']
17
+ end
18
+
19
+ desc 'Generate YARD documentation and open in browser'
20
+ task :doc => :yard do
21
+ puts "Documentation generated in doc/"
22
+ puts "Run 'open doc/index.html' to view"
23
+ end
24
+ rescue LoadError
25
+ # YARD not available
26
+ desc 'Generate YARD documentation (YARD not installed)'
27
+ task :yard do
28
+ puts "YARD is not available. Install it with: gem install yard"
29
+ end
30
+
31
+ task :doc => :yard
32
+ end
@@ -1,9 +1,37 @@
1
1
  require_relative 'ordered'
2
2
 
3
3
  module Valerie
4
+ # Represents a physical address in a VCard
5
+ #
6
+ # @example Full address
7
+ # address = Valerie::Address.new(
8
+ # post_office_box: 'PO Box 123',
9
+ # extended_address: 'Suite 200',
10
+ # street_address: '123 Main St',
11
+ # locality: 'New York',
12
+ # region: 'NY',
13
+ # postal_code: '10001',
14
+ # country: 'USA'
15
+ # )
16
+ #
17
+ # @example Simple address
18
+ # address = Valerie::Address.new(
19
+ # post_office_box: '',
20
+ # extended_address: '',
21
+ # street_address: '123 Main St',
22
+ # locality: 'New York',
23
+ # region: 'NY',
24
+ # postal_code: '10001',
25
+ # country: 'USA'
26
+ # )
4
27
  class Address
5
28
  include Ordered
6
-
29
+
30
+ # Parse an address from VCard ADR field string
31
+ #
32
+ # @param data [String] ADR field string
33
+ # @return [Address] Parsed address object
34
+ # @api private
7
35
  def self.from_s(data)
8
36
  data = data[data.index("ADR;")..] unless data.start_with?("ADR;")
9
37
  identifier = data.split(":").last.split(";")
@@ -21,6 +49,19 @@ module Valerie
21
49
  )
22
50
  end
23
51
 
52
+ # Create a new address
53
+ #
54
+ # @param post_office_box [String] Post office box
55
+ # @param extended_address [String] Extended address (e.g., apartment, suite)
56
+ # @param street_address [String] Street address
57
+ # @param locality [String] City or locality
58
+ # @param region [String] State, province, or region
59
+ # @param postal_code [String] ZIP or postal code
60
+ # @param country [String] Country
61
+ # @param options [Hash] Additional options
62
+ # @option options [Integer] :position Position in collection (1-based, used internally)
63
+ # @raise [ArgumentError] if position is invalid (< 1)
64
+ # @return [Address]
24
65
  def initialize(post_office_box:, extended_address:, street_address:, locality:, region:, postal_code:, country:, **options)
25
66
  @post_office_box = post_office_box
26
67
  @extended_address = extended_address
@@ -30,7 +71,7 @@ module Valerie
30
71
  @postal_code = postal_code
31
72
  @country = country
32
73
  @options = options
33
-
74
+
34
75
  raise ArgumentError, 'Invalid Position' if invalid_position?
35
76
  end
36
77
 
@@ -40,14 +81,14 @@ module Valerie
40
81
 
41
82
  def to_s
42
83
  parts = ['ADR']
43
-
44
- parts << "PERF=#{position}" if position?
45
-
84
+
85
+ parts << "PREF=#{position}" if position?
86
+
46
87
  @options.map do |key, value|
47
88
  next if key == :position
48
89
  parts << "#{key}=#{value}"
49
90
  end
50
-
91
+
51
92
  parts.join(';') + ":#{identifier}"
52
93
  end
53
94
 
data/lib/valerie/card.rb CHANGED
@@ -2,25 +2,98 @@ require_relative '../valerie'
2
2
  require_relative 'core/parser'
3
3
 
4
4
  module Valerie
5
+ # Represents a VCard (Contact Card) that can be generated or parsed.
6
+ #
7
+ # A Card contains various contact properties like name, email addresses,
8
+ # phone numbers, addresses, and more. It can be converted to VCard 3.0 format
9
+ # or parsed from VCard strings.
10
+ #
11
+ # @example Creating a new card
12
+ # card = Valerie::Card.new
13
+ # card.name = { first_name: 'Jane', last_name: 'Smith' }
14
+ # card.organization = { name: 'Acme Corp', department: 'Engineering' }
15
+ # card.birthday = Date.new(1990, 5, 15)
16
+ # card.gender = 'female'
17
+ #
18
+ # @example Adding contacts
19
+ # card.emails.add('jane@example.com', type: 'work')
20
+ # card.phones.add('+1-555-1234', type: 'cell')
21
+ # card.addresses.add(
22
+ # street_address: '123 Main St',
23
+ # locality: 'New York',
24
+ # region: 'NY',
25
+ # postal_code: '10001',
26
+ # country: 'USA'
27
+ # )
28
+ #
29
+ # @example Generating VCard output
30
+ # vcard_string = card.to_s
31
+ #
32
+ # @example Parsing a VCard
33
+ # cards = Valerie::Card.parse("BEGIN:VCARD\r\n...")
5
34
  class Card
6
35
  extend Core::Parser
7
-
36
+
37
+ # @return [Name, nil] The name of the contact
38
+ # @return [String, nil] The formatted full name (FN field)
39
+ # @return [Organization, nil] The organization details
40
+ # @return [Gender, nil] The gender
41
+ # @return [Birthday, nil] The birthday
8
42
  attr_reader :name, :formatted_name, :organization, :gender, :birthday
9
-
43
+
44
+ # Set the formatted name (FN field) for this card
45
+ #
46
+ # @param value [String] The formatted full name
47
+ # @return [String] The formatted name
48
+ # @example
49
+ # card.formatted_name = 'Dr. Jane Smith Jr.'
50
+ def formatted_name=(value)
51
+ @formatted_name = value
52
+ end
53
+
54
+ # Set the name for this card
55
+ #
56
+ # Automatically sets the formatted_name if not already set.
57
+ #
58
+ # @param parts [Hash, String, Name] Name data
59
+ # @option parts [String] :first_name Given name
60
+ # @option parts [String] :last_name Family name
61
+ # @option parts [String] :middle_name Middle name (optional)
62
+ # @option parts [String] :prefix Prefix like "Dr." or "Ms." (optional)
63
+ # @option parts [String] :suffix Suffix like "Jr." or "PhD" (optional)
64
+ # @return [Name] The created Name object
65
+ # @example With hash
66
+ # card.name = { first_name: 'John', last_name: 'Doe' }
67
+ # @example With string (splits on space)
68
+ # card.name = 'John Doe'
69
+ # @example With Name object
70
+ # card.name = Valerie::Name.new(first_name: 'John', last_name: 'Doe')
10
71
  def name=(parts)
11
72
  if parts.instance_of?(Name)
12
73
  @name = parts
13
74
  else
14
75
  @name = Name.new parts
15
76
  end
16
-
77
+
17
78
  if (@name.first_name || @name.last_name) && @formatted_name.nil?
18
79
  @formatted_name = "#{@name.first_name} #{@name.last_name}".strip
19
80
  end
20
-
81
+
21
82
  @name
22
83
  end
23
-
84
+
85
+ # Set the organization for this card
86
+ #
87
+ # @param args [Hash, Array, String, Organization] Organization data
88
+ # @option args [String] :name Organization name
89
+ # @option args [String] :department Department name (optional)
90
+ # @return [Organization] The created Organization object
91
+ # @example With hash
92
+ # card.organization = { name: 'Acme Corp', department: 'Engineering' }
93
+ # @example With array
94
+ # card.organization = ['Acme Corp', 'Engineering']
95
+ # @example With string (department will be empty)
96
+ # card.organization = 'Acme Corp'
24
97
  def organization=(*args)
25
98
  if args.first.instance_of?(Organization)
26
99
  @organization = args.flatten.first
@@ -28,55 +101,99 @@ module Valerie
28
101
  @organization = Organization.new(*args)
29
102
  end
30
103
  end
31
-
104
+
105
+ # Set the gender for this card
106
+ #
107
+ # @param value [String, Symbol, Gender] Gender identifier
108
+ # Valid values: 'male', 'female', 'other', 'none', 'unknown', 'M', 'F', 'O', 'N', 'U'
109
+ # @return [Gender] The created Gender object
110
+ # @raise [ArgumentError] if the gender identifier is invalid
111
+ # @example
112
+ # card.gender = 'male'
113
+ # card.gender = :female
32
114
  def gender=(value)
33
115
  @gender = value.is_a?(Gender) ? value : Gender.new(value)
34
116
  end
35
-
117
+
118
+ # Set the birthday for this card
119
+ #
120
+ # @param value [Date, Time, String, Birthday] Birthday value
121
+ # If the value responds to strftime, it will be formatted as YYYY-MM-DD
122
+ # @return [Birthday] The created Birthday object
123
+ # @example With Date object
124
+ # card.birthday = Date.new(1990, 5, 15)
125
+ # @example With string
126
+ # card.birthday = '1990-05-15'
36
127
  def birthday=(value)
37
128
  @birthday = value.is_a?(Birthday) ? value : Birthday.new(value)
38
129
  end
39
-
130
+
131
+ # Get the email collection for this card
132
+ #
133
+ # @return [Collection::EmailCollection] The email collection
134
+ # @example
135
+ # card.emails.add('user@example.com', type: 'work')
40
136
  def emails
41
137
  @emails ||= Collection::EmailCollection.new
42
138
  end
43
-
139
+
140
+ # Get the address collection for this card
141
+ #
142
+ # @return [Collection::AddressCollection] The address collection
143
+ # @example
144
+ # card.addresses.add(street_address: '123 Main St', locality: 'NYC', ...)
44
145
  def addresses
45
146
  @addresses ||= Collection::AddressCollection.new
46
147
  end
47
-
148
+
149
+ # Get the phone collection for this card
150
+ #
151
+ # @return [Collection::PhoneCollection] The phone collection
152
+ # @example
153
+ # card.phones.add('+1-555-1234', type: 'cell')
48
154
  def phones
49
155
  @phones ||= Collection::PhoneCollection.new
50
156
  end
51
-
157
+
158
+ # Convert this card to a VCard 3.0 formatted string
159
+ #
160
+ # @return [String] VCard string with \r\n line endings
161
+ # @example
162
+ # vcard_string = card.to_s
163
+ # File.write('contact.vcf', vcard_string)
52
164
  def to_s
53
165
  to_a.join("\r\n")
54
166
  end
55
-
167
+
168
+ # Convert this card to an array of VCard lines
169
+ #
170
+ # @return [Array<String>] Array of VCard property lines
171
+ # @api private
56
172
  def to_a
57
173
  parts = [
58
174
  'BEGIN:VCARD',
59
175
  "VERSION:#{Valerie.configuration.version}",
60
176
  "PRODID:-//#{Valerie.configuration.product}//#{Valerie.configuration.language}",
61
177
  ]
62
-
178
+
63
179
  parts << @name.to_s if @name
180
+ parts << "FN:#{@formatted_name}" if @formatted_name
64
181
  parts << @organization.to_s if @organization
65
182
  parts << @birthday.to_s if @birthday
66
183
  parts << @gender.to_s if @gender
67
-
184
+
68
185
  emails.each do |email|
69
186
  parts << email.to_s
70
187
  end
71
-
188
+
72
189
  phones.each do |phone|
73
190
  parts << phone.to_s
74
191
  end
75
-
192
+
76
193
  addresses.each do |address|
77
194
  parts << address.to_s
78
195
  end
79
-
196
+
80
197
  parts << 'END:VCARD'
81
198
  parts
82
199
  end
@@ -3,17 +3,49 @@ require_relative '../address'
3
3
 
4
4
  module Valerie
5
5
  module Collection
6
+ # Collection of addresses for a VCard
7
+ #
8
+ # @example
9
+ # collection = Valerie::Collection::AddressCollection.new
10
+ # collection.add(
11
+ # street_address: '123 Main St',
12
+ # locality: 'New York',
13
+ # region: 'NY',
14
+ # postal_code: '10001',
15
+ # country: 'USA'
16
+ # )
6
17
  class AddressCollection < Base
7
- def add(address, **options)
18
+ # Add an address to the collection
19
+ #
20
+ # Addresses are automatically sorted by position after adding.
21
+ #
22
+ # @param address [Hash, Address, nil] Address data hash or Address object
23
+ # @param options [Hash] Additional options merged with address data
24
+ # @option options [Integer] :position Position in collection (1-based, auto-assigned if not specified)
25
+ # @return [Address] The added address object
26
+ # @example Add with hash
27
+ # collection.add(
28
+ # post_office_box: '',
29
+ # extended_address: '',
30
+ # street_address: '123 Main St',
31
+ # locality: 'New York',
32
+ # region: 'NY',
33
+ # postal_code: '10001',
34
+ # country: 'USA'
35
+ # )
36
+ def add(address = nil, **options)
8
37
  @item = if address.is_a?(Address)
9
- address.dup { _1.position = options[:position] || @items.size + 1 }
38
+ address.dup.tap { _1.position = options[:position] || @items.size + 1 }
10
39
  else
11
- Address.new(**address, **options)
40
+ merged_options = (address || {}).merge(options)
41
+ merged_options[:position] ||= @items.size + 1
42
+
43
+ Address.new(**merged_options)
12
44
  end
13
-
45
+
14
46
  @items << @item
15
47
  @items = @items.sort_by(&:position)
16
-
48
+
17
49
  @item
18
50
  end
19
51
  end
@@ -3,7 +3,27 @@ require_relative '../email'
3
3
 
4
4
  module Valerie
5
5
  module Collection
6
+ # Collection of email addresses for a VCard
7
+ #
8
+ # @example
9
+ # collection = Valerie::Collection::EmailCollection.new
10
+ # collection.add('work@example.com', type: :work)
11
+ # collection.add('personal@example.com', type: :home, position: 2)
6
12
  class EmailCollection < Base
13
+ # Add an email address to the collection
14
+ #
15
+ # Email addresses are automatically sorted by position after adding.
16
+ #
17
+ # @param email [String, Email] Email address string or Email object
18
+ # @param options [Hash] Options (only used if email is a String)
19
+ # @option options [String, Symbol, Array] :type Email type (:work, :home, :internet, etc.)
20
+ # @option options [Integer] :position Position in collection (1-based, auto-assigned if not specified)
21
+ # @return [Email] The added email object
22
+ # @example Add with type
23
+ # collection.add('user@example.com', type: :work)
24
+ # @example Add Email object
25
+ # email = Valerie::Email.new(address: 'user@example.com', type: :work)
26
+ # collection.add(email)
7
27
  def add(email, **options)
8
28
  @item = if email.is_a?(Email)
9
29
  email.dup.tap { _1.position = options[:position] || @items.size + 1 }
@@ -12,10 +32,10 @@ module Valerie
12
32
  else
13
33
  Email.new(address: email, **options, position: options[:position] || @items.size + 1)
14
34
  end
15
-
35
+
16
36
  @items << @item
17
37
  @items = @items.sort_by(&:position)
18
-
38
+
19
39
  @item
20
40
  end
21
41
  end
@@ -3,17 +3,36 @@ require_relative '../phone'
3
3
 
4
4
  module Valerie
5
5
  module Collection
6
+ # Collection of phone numbers for a VCard
7
+ #
8
+ # @example
9
+ # collection = Valerie::Collection::PhoneCollection.new
10
+ # collection.add('+1-555-1234', type: :work)
11
+ # collection.add('+1-555-9999', type: :cell, position: 1)
6
12
  class PhoneCollection < Base
13
+ # Add a phone number to the collection
14
+ #
15
+ # Phone numbers are automatically sorted by position after adding.
16
+ #
17
+ # @param phone [String, Phone] Phone number string or Phone object
18
+ # @param options [Hash] Options (only used if phone is a String)
19
+ # @option options [String, Symbol, Array] :type Phone type (:work, :home, :cell, etc.)
20
+ # @option options [Integer] :position Position in collection (1-based, auto-assigned if not specified)
21
+ # @return [Phone] The added phone object
22
+ # @example Add with type
23
+ # collection.add('+1-555-1234', type: :work)
24
+ # @example Add with position
25
+ # collection.add('+1-555-1234', type: :cell, position: 1)
7
26
  def add(phone, **options)
8
27
  @item = if phone.is_a?(Phone)
9
28
  phone.dup.tap { _1.position = options[:position] || @items.size + 1 }
10
29
  else
11
30
  Phone.new(phone, **options, position: options[:position] || @items.size + 1)
12
31
  end
13
-
32
+
14
33
  @items << @item
15
34
  @items = @items.sort_by(&:position)
16
-
35
+
17
36
  @item
18
37
  end
19
38
  end