cal-invite 0.2.1 → 0.3.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 +4 -4
- data/AGENTS.md +13 -8
- data/CHANGELOG.md +49 -0
- data/CONFIGURATION.md +25 -29
- data/README.md +32 -20
- data/SECURITY.md +39 -10
- data/assets/social/cal-invite-github.png +0 -0
- data/lib/cal_invite/configuration.rb +20 -11
- data/lib/cal_invite/event.rb +87 -114
- data/lib/cal_invite/ical_timezone.rb +64 -18
- data/lib/cal_invite/providers/base_provider.rb +302 -264
- data/lib/cal_invite/providers/ical.rb +7 -152
- data/lib/cal_invite/providers/ics.rb +68 -13
- data/lib/cal_invite/providers/ics_content.rb +7 -129
- data/lib/cal_invite/providers.rb +8 -0
- data/lib/cal_invite/version.rb +1 -1
- data/lib/cal_invite.rb +1 -8
- metadata +3 -8
- data/.DS_Store +0 -0
- data/CACHING.md +0 -148
- data/CLAUDE.md +0 -49
- data/lib/.DS_Store +0 -0
- data/lib/cal_invite/.DS_Store +0 -0
- data/lib/cal_invite/caching.rb +0 -74
data/CACHING.md
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
## Caching
|
|
2
|
-
|
|
3
|
-
CalInvite supports flexible caching options when used within a Rails application. You can configure the cache store to use any of Rails' supported cache stores including memory store, Redis, or a custom cache store implementation.
|
|
4
|
-
|
|
5
|
-
### Basic Configuration
|
|
6
|
-
|
|
7
|
-
Configure caching in your Rails application's initializer:
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
# config/initializers/cal_invite.rb
|
|
11
|
-
CalInvite.configure do |config|
|
|
12
|
-
# Use Rails.cache by default
|
|
13
|
-
config.cache_store = Rails.cache
|
|
14
|
-
|
|
15
|
-
# Optional: Set a custom prefix for cache keys
|
|
16
|
-
config.cache_prefix = 'my_app_cal_invite'
|
|
17
|
-
|
|
18
|
-
# Optional: Set cache expiration (in seconds)
|
|
19
|
-
config.cache_expires_in = 3600 # 1 hour
|
|
20
|
-
end
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Available Cache Stores
|
|
24
|
-
|
|
25
|
-
CalInvite supports the following cache stores:
|
|
26
|
-
|
|
27
|
-
1. Memory Store (default)
|
|
28
|
-
```ruby
|
|
29
|
-
config.cache_store = :memory_store
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
2. Null Store (for disabling caching)
|
|
33
|
-
```ruby
|
|
34
|
-
config.cache_store = :null_store
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
3. Custom Cache Store
|
|
38
|
-
```ruby
|
|
39
|
-
# Any object that implements read/write/delete methods
|
|
40
|
-
config.cache_store = MyCacheStore.new
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
4. Rails Cache
|
|
44
|
-
```ruby
|
|
45
|
-
# Use your Rails application's configured cache
|
|
46
|
-
config.cache_store = Rails.cache
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Cache Configuration Options
|
|
50
|
-
|
|
51
|
-
- `cache_store`: The storage mechanism for cached data
|
|
52
|
-
- `cache_prefix`: Prefix for all cache keys (default: 'cal_invite')
|
|
53
|
-
- `cache_expires_in`: Default cache expiration time in seconds
|
|
54
|
-
- `timezone`: Default timezone for cache keys (default: 'UTC')
|
|
55
|
-
|
|
56
|
-
### Custom Cache Adapters
|
|
57
|
-
|
|
58
|
-
You can implement custom cache adapters by creating a class that responds to the following methods:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
class CustomCacheStore
|
|
62
|
-
def read(key)
|
|
63
|
-
# Implementation for retrieving cached value
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def write(key, value, options = {})
|
|
67
|
-
# Implementation for storing value in cache
|
|
68
|
-
# options may include :expires_in
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def delete(key)
|
|
72
|
-
# Implementation for removing cached value
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def clear
|
|
76
|
-
# Implementation for clearing all cached values
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Configure CalInvite to use your custom cache store
|
|
81
|
-
CalInvite.configure do |config|
|
|
82
|
-
config.cache_store = CustomCacheStore.new
|
|
83
|
-
end
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Automatic Cache Invalidation
|
|
87
|
-
|
|
88
|
-
The cache is automatically invalidated in the following scenarios:
|
|
89
|
-
|
|
90
|
-
1. When event attributes are updated via `update_attributes`
|
|
91
|
-
2. When a new calendar URL is generated
|
|
92
|
-
3. When the configuration changes
|
|
93
|
-
|
|
94
|
-
### Best Practices
|
|
95
|
-
|
|
96
|
-
1. **Cache Store Selection**: Choose an appropriate cache store based on your needs:
|
|
97
|
-
- Use `:memory_store` for development and small applications
|
|
98
|
-
- Use Rails.cache for production applications
|
|
99
|
-
- Implement a custom cache store for specific requirements
|
|
100
|
-
|
|
101
|
-
2. **Cache Key Management**:
|
|
102
|
-
- The gem automatically generates unique cache keys based on event attributes
|
|
103
|
-
- Keys include a prefix for namespace isolation
|
|
104
|
-
- Consider your timezone settings when debugging cache issues
|
|
105
|
-
|
|
106
|
-
3. **Expiration Strategy**:
|
|
107
|
-
- Set appropriate expiration times based on your use case
|
|
108
|
-
- Consider using shorter expiration times for frequently changing data
|
|
109
|
-
- Use `nil` expiration for permanent caching (until manual invalidation)
|
|
110
|
-
|
|
111
|
-
### Example Rails Integration
|
|
112
|
-
|
|
113
|
-
Here's a complete example of setting up caching in a Rails application:
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
# config/initializers/cal_invite.rb
|
|
117
|
-
CalInvite.configure do |config|
|
|
118
|
-
if Rails.env.production?
|
|
119
|
-
# Use Rails cache in production
|
|
120
|
-
config.cache_store = Rails.cache
|
|
121
|
-
config.cache_expires_in = 1.hour
|
|
122
|
-
else
|
|
123
|
-
# Use memory store in development
|
|
124
|
-
config.cache_store = :memory_store
|
|
125
|
-
config.cache_expires_in = 5.minutes
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
config.cache_prefix = "cal_invite:#{Rails.env}"
|
|
129
|
-
config.timezone = 'UTC'
|
|
130
|
-
end
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Testing with Caching
|
|
134
|
-
|
|
135
|
-
When writing tests that involve caching:
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
# In your test setup
|
|
139
|
-
CalInvite.configure do |config|
|
|
140
|
-
config.cache_store = :memory_store
|
|
141
|
-
config.cache_expires_in = 3600 # 1 hour in seconds
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Clear cache between tests
|
|
145
|
-
def setup
|
|
146
|
-
CalInvite.configuration.cache_store.clear
|
|
147
|
-
end
|
|
148
|
-
```
|
data/CLAUDE.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
CalInvite is a Ruby gem for generating calendar invitations (URLs and .ics files) across multiple platforms: Google, Outlook, Outlook 365, Yahoo, Apple iCal, and generic .ics. Ruby >= 3.0.0, Rails 6.0–8.0 compatible, depends on `activesupport`.
|
|
8
|
-
|
|
9
|
-
## Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
bundle install # install dependencies
|
|
13
|
-
rake test # run full test suite (default rake task)
|
|
14
|
-
rake test TEST=test/cal_invite/event_test.rb # run a single test file
|
|
15
|
-
ruby -Itest -Ilib test/cal_invite/event_test.rb # run a single test file directly
|
|
16
|
-
rake rdoc # generate RDoc documentation into doc/
|
|
17
|
-
rake rdoc:clean # remove generated doc/
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Tests are Minitest-based, files live under `test/cal_invite/`, named `*_test.rb`. Coverage is collected via SimpleCov (see `coverage/`).
|
|
21
|
-
|
|
22
|
-
## Architecture
|
|
23
|
-
|
|
24
|
-
- **`CalInvite` module** (`lib/cal_invite.rb`) — top-level entry point. Holds a singleton `CalInvite.configuration` (a `Configuration` instance) set via `CalInvite.configure { |c| ... }`. Mixes in `Caching` at the module level for class-level cache helpers.
|
|
25
|
-
|
|
26
|
-
- **`CalInvite::Event`** (`lib/cal_invite/event.rb`) — the central object. Holds all event attributes (title, start/end time, location, url, attendees, timezone, notes, multi-day sessions, all-day flag). `generate_calendar_url(provider)` is the main API: it validates the event, checks the cache (MD5 hash of all attributes + provider as key), and if not cached, resolves `CalInvite::Providers::<CapitalizedProvider>`, instantiates it with `self`, and calls `#generate`. Location and virtual-meeting URL are intentionally separate fields (not merged) so each provider can format them appropriately.
|
|
27
|
-
|
|
28
|
-
- **`CalInvite::Providers`** (`lib/cal_invite/providers.rb`) — namespace with `SUPPORTED_PROVIDERS = %i[google ical outlook yahoo ics office365]`. Provider classes are autoloaded from `lib/cal_invite/providers/*.rb`. To add a new provider: create a class extending `BaseProvider`, implement `#generate`, register it here with `autoload`, and add its symbol to `SUPPORTED_PROVIDERS`.
|
|
29
|
-
|
|
30
|
-
- **`BaseProvider`** (`lib/cal_invite/providers/base_provider.rb`) — abstract base class (note: it's a top-level `class BaseProvider`, not namespaced under `CalInvite::Providers`). Defines the `#generate` contract (must raise `NotImplementedError` if not overridden) plus shared helpers used by subclasses: URL-building (`url_encode`, `format_description`, `format_location`, `format_url`, `format_description_with_url`, `add_optional_params`) and iCalendar property formatting (`attendees_list`, `attendee_emails`, `attendee_hash_value`, `attendee_line`, `organizer_line`, `method_value`, `status_line`, `geo_line`, `transp_line`, `busystatus_line`, `class_line`, `importance_lines`, `disallow_counter_line`, `rrule_line`, `valarm_lines`, `local_wall_time`, `vtimezone_lines`). `attendee_hash_value(attendee, key)` is the correct way to read an attendee-hash field (tries symbol then string key) — don't reintroduce `attendee[:key] || attendee["key"]`, which silently drops explicit `false` values. Each concrete provider (`Google`, `Outlook`, `Office365`, `Yahoo`, `Ical`, `Ics`) builds a provider-specific URL or content string from the `Event` using these helpers.
|
|
31
|
-
|
|
32
|
-
- **`Ics` / `IcsContent` providers** (`ics.rb`, `ics_content.rb`) — generate actual `.ics` file content (RFC 5545) rather than a URL, for direct download flows (see README's `send_data` controller example). `IcsDownload` lives in `ics_content.rb` alongside `IcsContent` (not an alias). All ics-family providers accept a `method:` (`:publish`/`:request`/`:cancel`/`:reply`/`:counter`/`:decline_counter`) passed through `Event#generate_calendar_url`. Use `BaseProvider#method_value` (not `method.to_s.upcase`) when emitting the `METHOD:` line — `:decline_counter` must render as `METHOD:DECLINECOUNTER` (one word) per RFC 5545, unlike its Ruby symbol. `:request` plus `Event#organizer` produces an RFC 5545 meeting request that mail clients render with RSVP actions; `:cancel` (reusing the same `Event#uid`, with `Event#sequence` bumped) produces a real cancellation; `:reply`/`:counter`/`:decline_counter` all omit `RSVP=TRUE` on `ATTENDEE` lines (none of them request a further response). `Event#uid` is stable (auto-generated and memoized per instance, or pass your own) — required so a later `:request`/`:cancel` is recognized as updating the original invite rather than creating a new one. `Event#attendees` entries can be plain email strings or `{ email:, name:, partstat:, rsvp: }` hashes (`partstat:` drives `PARTSTAT=` on the `ATTENDEE` line, default `NEEDS-ACTION`; `rsvp:` overrides the method-derived `RSVP=TRUE`/absent default — needed for e.g. a registration-confirmation invite where the attendee is already `:accepted` and `rsvp: false` suppresses it under `method: :request`). Optional `Event` fields `geo`, `reminders`, `busy`, `visibility`, `rrule`, `calendar_name` map to `GEO:`, `VALARM`, `TRANSP:`, `CLASS:`, `RRULE:`, and `X-WR-CALNAME` respectively; `importance`/`allow_counter` map to Outlook's non-standard `X-MICROSOFT-CDO-IMPORTANCE`/`X-MICROSOFT-DISALLOW-COUNTER` (plus standard `PRIORITY:` and `X-MICROSOFT-CDO-BUSYSTATUS` alongside `TRANSP:`) — safe to always emit since unrecognized `X-` properties are required to be ignored by other clients. See `CONFIGURATION.md`.
|
|
33
|
-
- **Timezones** — `:ics`/`:ical` build a real `VTIMEZONE` (via `CalInvite::IcalTimezone`, backed by `tzinfo`) for any recognized IANA `timezone` other than `'UTC'`, and correctly convert `start_time`/`end_time` (always UTC) to that zone's local wall-clock time for `DTSTART;TZID=...`. `'UTC'` uses plain `Z`-suffixed timestamps with no `VTIMEZONE`.
|
|
34
|
-
|
|
35
|
-
- **`Caching`** (`lib/cal_invite/caching.rb`) — an `ActiveSupport::Concern` mixed into `CalInvite` at the class level. Provides `fetch_from_cache`, `write_to_cache`, `read_from_cache`, `clear_cache!`, `clear_event_cache!`, `clear_provider_cache!`, all keyed under `CalInvite.configuration.cache_prefix`. Works with any `ActiveSupport::Cache::Store`-compatible store, or a custom object responding to `read`/`write`/`delete`. See also `CACHING.md`.
|
|
36
|
-
|
|
37
|
-
- **`Configuration`** (`lib/cal_invite/configuration.rb`) — holds `cache_store`, `cache_prefix` (default `'cal_invite'`), `cache_expires_in` (default 24 hours), `webhook_secret`, `timezone` (default `'UTC'`). `cache_store=` accepts `:memory_store`, `:null_store`, or any object implementing `read`/`write`/`delete`.
|
|
38
|
-
|
|
39
|
-
## Conventions
|
|
40
|
-
|
|
41
|
-
- Always pass times to `Event.new` in UTC; use the `timezone` attribute only to control display/formatting in the generated invite.
|
|
42
|
-
- `location` (physical) and `url` (virtual meeting link) are kept as distinct fields throughout the provider layer — don't conflate them when adding provider logic.
|
|
43
|
-
- All-day events skip `start_time`/`end_time` validation; multi-day events use the separate `multi_day_sessions` array instead of `start_time`/`end_time`.
|
|
44
|
-
- YARD-style doc comments (`@param`, `@return`, `@example`) are used throughout `lib/` and feed the RDoc-generated docs published to GitHub Pages (`.github/workflows/documentation.yml`) — keep them accurate when changing public method signatures.
|
|
45
|
-
- CalInvite is outbound-only: it renders `.ics` content/URLs and has no concept of inbound RSVP replies. It also can't express Google/Microsoft Calendar-API-only concepts like guest permissions (`guestsCanInviteOthers` etc.) since those have no `.ics`/URL representation at all — but it does generate the genuinely protocol-level equivalents that exist: `METHOD:COUNTER`/`DECLINECOUNTER` and Outlook's `X-MICROSOFT-*` extensions (`allow_counter`, `importance`, `busy`). See CONFIGURATION.md's "Tracking RSVPs" and "Guest permissions" sections for the full breakdown of what's real vs. not addable, and why.
|
|
46
|
-
|
|
47
|
-
## Example app (`Example/calendar_app`)
|
|
48
|
-
|
|
49
|
-
Rails 8.1 app demonstrating the gem. Its Gemfile depends on the **published** `cal-invite` gem (never commit it pointing at `path: "../.."`) — when testing example-app changes that rely on unreleased gem code (like the `Meeting` demo below, which needs the still-unreleased 0.2.0), temporarily uncomment the `path: "../.."` line locally, `bundle install`, test, then revert the Gemfile and re-run `bundle install` against rubygems before committing. `Meeting`/`MeetingAttendee` (with a migration) is a working reference implementation of the `uid`/`sequence` persistence pattern CONFIGURATION.md describes — see `app/models/meeting.rb`, `app/controllers/meetings_controller.rb` (create/reschedule/cancel), and `app/controllers/event_replies_controller.rb` (illustrative inbound `METHOD:REPLY` parsing, not wired to a real inbound-email provider). This demo won't actually run against the currently-published gem until 0.2.0 ships.
|
data/lib/.DS_Store
DELETED
|
Binary file
|
data/lib/cal_invite/.DS_Store
DELETED
|
Binary file
|
data/lib/cal_invite/caching.rb
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# lib/cal_invite/caching.rb
|
|
4
|
-
require 'active_support/concern'
|
|
5
|
-
|
|
6
|
-
module CalInvite
|
|
7
|
-
module Caching
|
|
8
|
-
extend ActiveSupport::Concern
|
|
9
|
-
|
|
10
|
-
class_methods do
|
|
11
|
-
def fetch_from_cache(key, options = {}, &block)
|
|
12
|
-
full_key = generate_cache_key(key)
|
|
13
|
-
store = CalInvite.configuration.cache_store
|
|
14
|
-
|
|
15
|
-
if store.respond_to?(:fetch)
|
|
16
|
-
store.fetch(full_key, options, &block)
|
|
17
|
-
else
|
|
18
|
-
cached = store.read(full_key)
|
|
19
|
-
return cached if cached
|
|
20
|
-
|
|
21
|
-
value = block.call
|
|
22
|
-
store.write(full_key, value, options)
|
|
23
|
-
value
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def write_to_cache(key, value, options = {})
|
|
28
|
-
full_key = generate_cache_key(key)
|
|
29
|
-
options[:expires_in] ||= CalInvite.configuration.cache_expires_in
|
|
30
|
-
CalInvite.configuration.cache_store.write(full_key, value, options)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def read_from_cache(key)
|
|
34
|
-
full_key = generate_cache_key(key)
|
|
35
|
-
CalInvite.configuration.cache_store.read(full_key)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def clear_cache!
|
|
39
|
-
store = CalInvite.configuration.cache_store
|
|
40
|
-
if store.respond_to?(:delete_matched)
|
|
41
|
-
store.delete_matched("#{CalInvite.configuration.cache_prefix}:*")
|
|
42
|
-
else
|
|
43
|
-
store.clear
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def clear_event_cache!(event_id)
|
|
48
|
-
delete_cache_pattern("events:#{event_id}")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def clear_provider_cache!(provider)
|
|
52
|
-
delete_cache_pattern("providers:#{provider}")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def generate_cache_key(*parts)
|
|
56
|
-
([CalInvite.configuration.cache_prefix] + parts).join(':')
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
def delete_cache_pattern(pattern)
|
|
62
|
-
store = CalInvite.configuration.cache_store
|
|
63
|
-
full_pattern = generate_cache_key(pattern)
|
|
64
|
-
|
|
65
|
-
if store.respond_to?(:delete_matched)
|
|
66
|
-
store.delete_matched("#{full_pattern}:*")
|
|
67
|
-
else
|
|
68
|
-
# Fallback for stores that don't support pattern deletion
|
|
69
|
-
store.delete(full_pattern)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|