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.
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
Binary file
@@ -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