crest 0.1.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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/lib/crest/card.rb +49 -0
- data/lib/crest/cards_controller.rb +19 -0
- data/lib/crest/config.rb +36 -0
- data/lib/crest/error.rb +4 -0
- data/lib/crest/photo.rb +32 -0
- data/lib/crest/railtie.rb +14 -0
- data/lib/crest/routes.rb +14 -0
- data/lib/crest/version.rb +4 -0
- data/lib/crest.rb +24 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1cad1f39fb20344579f8ba34190bf9ee7dc17f8fa4bea75a581f9219c74cc589
|
|
4
|
+
data.tar.gz: a227eaf7e1d5fc43864fcb9ce019e3e9575703549e72cdad8a5a22667716c6fc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e18330c407056f688bc665a6f01588204a9faf5036fd2088802d73891cc506fdb8e31c50a0c456f6fa85da653b3af22d2c15e0b0d314b3e2dd91da2522e9af55
|
|
7
|
+
data.tar.gz: 5f4db2d8af24f882704f0b83af41122739fe294f56859cb9f1df435b1090409ab27760e954022c8496178a3ddbe65b04189a0088216acc94ec99b0ed5ce8b7b2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
For more information about changelogs, check [Keep a Changelog](http://keepachangelog.com) and
|
|
6
|
+
[Vandamme](http://tech-angels.github.io/vandamme).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## 0.1.0 - 2026-09-11
|
|
11
|
+
|
|
12
|
+
* [Feature] Build a vCard 3.0 from a name, a number and whatever else is worth saving --
|
|
13
|
+
`Crest::Card.new(name:, phone:, url:, email:, org:, photo:).to_s` -- CRLF-delimited, folded at
|
|
14
|
+
74 characters with the continuation space the format asks for, and with the photo typed PNG or
|
|
15
|
+
JPEG by its own first bytes rather than by anything the caller declares. Plain Ruby: it loads
|
|
16
|
+
and answers with no Rails in the process
|
|
17
|
+
* [Feature] Draw the card at a path the host names with `crest 'houseaccount.vcf'`, a routes DSL
|
|
18
|
+
method rather than an engine to mount, so the line may sit inside a `scope` or a `constraints`.
|
|
19
|
+
The controller it draws to descends from `ActionController::Base` and not from the host's
|
|
20
|
+
`ApplicationController`, since the card is public by definition and an inherited
|
|
21
|
+
`before_action :authenticate` would refuse the one request it exists to answer
|
|
22
|
+
* [Feature] State the name, number, URL, email, organization and picture in one initializer, each
|
|
23
|
+
of them a value or a callable resolved per request, so a number read out of the environment is
|
|
24
|
+
right in production and in staging without a restart
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Claudio Baccigalupo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Crest
|
|
2
|
+
|
|
3
|
+
A contact card your Rails app hands out, so a text from an unknown number arrives under a name.
|
|
4
|
+
|
|
5
|
+
`GET /houseaccount.vcf` answers with a vCard. A phone offers to save it, and from then on your
|
|
6
|
+
messages come from *HouseAccount* rather than from `+1 800 555 0100`.
|
|
7
|
+
|
|
8
|
+
## How to install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
gem install crest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or, in your `Gemfile`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'crest', '~> 0.1.0'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Crest is below `1.0`, so the pin stops short of `0.2.0` rather than admitting every `0.x`:
|
|
21
|
+
`~> major.minor` only promises what it says once the major is real. From `1.0` on it will be
|
|
22
|
+
`~> 1.0`, and `bundle update` will never cross a breaking change.
|
|
23
|
+
|
|
24
|
+
## Handing out a card
|
|
25
|
+
|
|
26
|
+
One line in `config/routes.rb`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
crest 'houseaccount.vcf'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
and one initializer, `config/initializers/crest.rb`:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Crest.configure do |config|
|
|
36
|
+
config.name = 'HouseAccount'
|
|
37
|
+
config.org = 'HouseAccount'
|
|
38
|
+
config.url = 'https://houseaccount.com/'
|
|
39
|
+
config.email = 'hello@houseaccount.com'
|
|
40
|
+
config.photo = Rails.root.join 'public/cube.png'
|
|
41
|
+
config.phone = -> { Delivery::Twilio.sender }
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That is the whole of it. The route gives you a `houseaccount_vcf_path` helper to link to, and
|
|
46
|
+
the response is `text/vcard`, named after the file, and cacheable.
|
|
47
|
+
|
|
48
|
+
Only `name` and `phone` are required, and the gem raises rather than handing out a card that
|
|
49
|
+
saves under a blank name. Everything else is left out of the card when you do not set it.
|
|
50
|
+
|
|
51
|
+
**Every setting may be a value or a callable**, and a callable is asked again on every request.
|
|
52
|
+
That is what makes a number read out of the environment safe: production and staging differ, and
|
|
53
|
+
a value read once at boot would be wrong in one of them until a restart.
|
|
54
|
+
|
|
55
|
+
The picture is bytes, or a path the gem reads. It is read from disk once, and PNG and JPEG are
|
|
56
|
+
told apart by the image's own first bytes rather than by anything you have to declare.
|
|
57
|
+
|
|
58
|
+
## Why a route and not an engine to mount
|
|
59
|
+
|
|
60
|
+
You name the path, because the filename is your app's own name. And because it is a route rather
|
|
61
|
+
than an engine, the line goes wherever you want it — inside a `scope`, behind a `constraints`,
|
|
62
|
+
under a subdomain — which a mount point cannot do.
|
|
63
|
+
|
|
64
|
+
## Why the controller is not your ApplicationController
|
|
65
|
+
|
|
66
|
+
`Crest::CardsController` descends from `ActionController::Base`, not from your
|
|
67
|
+
`ApplicationController`. That is deliberate and it is the surprising half: the card is public by
|
|
68
|
+
definition — a phone fetches it with no session — so an inherited `before_action :authenticate`
|
|
69
|
+
would refuse the one request the gem exists to answer.
|
|
70
|
+
|
|
71
|
+
## Without Rails
|
|
72
|
+
|
|
73
|
+
The builder is plain Ruby and loads on its own, Rails or no Rails:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
require 'crest/card'
|
|
77
|
+
|
|
78
|
+
Crest::Card.new(name: 'HouseAccount', phone: '+18005550100',
|
|
79
|
+
url: 'https://houseaccount.com/', photo: 'cube.png').to_s
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
It answers a vCard 3.0 string, CRLF-delimited and folded at 74 characters with the continuation
|
|
83
|
+
space the format asks for.
|
|
84
|
+
|
|
85
|
+
## Documentation
|
|
86
|
+
|
|
87
|
+
The API reference is at [rubydoc.info/gems/crest](https://rubydoc.info/gems/crest).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT, see [LICENSE.txt](LICENSE.txt).
|
data/lib/crest/card.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'crest/photo'
|
|
2
|
+
|
|
3
|
+
module Crest
|
|
4
|
+
# A contact card a phone offers to save, written as the vCard 3.0 every phone reads.
|
|
5
|
+
class Card
|
|
6
|
+
# What a line may hold before a vCard folds it, less the space that continues it.
|
|
7
|
+
FOLD = 74
|
|
8
|
+
|
|
9
|
+
# @param name [String] name the card is saved under.
|
|
10
|
+
# @param phone [String] number in the form a phone should dial it.
|
|
11
|
+
# @param url [String] address of the site behind the name.
|
|
12
|
+
# @param email [String] address somebody may write back to.
|
|
13
|
+
# @param org [String] organization the name stands for.
|
|
14
|
+
# @param photo [String, Pathname] image bytes, or the file to read them from.
|
|
15
|
+
def initialize(name:, phone:, url: nil, email: nil, org: nil, photo: nil)
|
|
16
|
+
@name = name
|
|
17
|
+
@phone = phone
|
|
18
|
+
@url = url
|
|
19
|
+
@email = email
|
|
20
|
+
@org = org
|
|
21
|
+
@photo = photo
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [String] card itself, CRLF-delimited and folded, as the format asks.
|
|
25
|
+
def to_s = lines.flat_map { |line| fold line }.join("\r\n") << "\r\n"
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def lines
|
|
30
|
+
[
|
|
31
|
+
'BEGIN:VCARD', 'VERSION:3.0',
|
|
32
|
+
"N:;#{@name};;;", "FN:#{@name}",
|
|
33
|
+
("ORG:#{@org}" if @org),
|
|
34
|
+
"TEL;TYPE=CELL:#{@phone}",
|
|
35
|
+
("URL:#{@url}" if @url),
|
|
36
|
+
("EMAIL;TYPE=INTERNET:#{@email}" if @email),
|
|
37
|
+
(Photo.of(@photo).to_s if @photo),
|
|
38
|
+
'END:VCARD',
|
|
39
|
+
].compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fold(line)
|
|
43
|
+
return [line] if line.length <= FOLD
|
|
44
|
+
|
|
45
|
+
# A long line continues on the next one, which a single space marks as the same line.
|
|
46
|
+
[line[0, FOLD], *line[FOLD..].scan(/.{1,#{FOLD - 1}}/).map { |part| " #{part}" }]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Crest
|
|
2
|
+
# Hands out the contact card. It descends from ActionController::Base rather than from the
|
|
3
|
+
# host's ApplicationController on purpose: the card is public by definition, and an inherited
|
|
4
|
+
# `before_action :authenticate` would refuse the one request this exists to answer.
|
|
5
|
+
class CardsController < ActionController::Base
|
|
6
|
+
# What a phone has to be told the body is before it offers to save it.
|
|
7
|
+
TYPE = 'text/vcard; charset=utf-8'
|
|
8
|
+
|
|
9
|
+
# Answers the card, named after the path it was drawn at and cached until the card changes.
|
|
10
|
+
# @return [void]
|
|
11
|
+
def show
|
|
12
|
+
card = Crest.card.to_s
|
|
13
|
+
|
|
14
|
+
if stale?(etag: card, public: true)
|
|
15
|
+
send_data card, type: TYPE, disposition: 'inline', filename: File.basename(request.path)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/crest/config.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'crest/error'
|
|
2
|
+
|
|
3
|
+
module Crest
|
|
4
|
+
# Every fact about the app a card is handed out by, so none of the gem's own classes names one.
|
|
5
|
+
# A host states each as a value or as something callable, and a callable is asked every time a
|
|
6
|
+
# card is drawn: an app reading its number out of the environment differs between production and
|
|
7
|
+
# staging, and one read at boot would be wrong in one of them until a restart.
|
|
8
|
+
class Config
|
|
9
|
+
# What a host states, each resolved when a card is drawn rather than when this is written.
|
|
10
|
+
attr_writer :name, :phone, :url, :email, :org, :photo
|
|
11
|
+
|
|
12
|
+
# @return [String] name the card is saved under, which the gem cannot invent.
|
|
13
|
+
def name = resolve(@name) || missing('name')
|
|
14
|
+
|
|
15
|
+
# @return [String] number a text of ours arrives from, which the gem cannot invent.
|
|
16
|
+
def phone = resolve(@phone) || missing('phone')
|
|
17
|
+
|
|
18
|
+
# @return [String] address of the site behind the name, left out of the card where unset.
|
|
19
|
+
def url = resolve @url
|
|
20
|
+
|
|
21
|
+
# @return [String] address somebody may write back to, left out of the card where unset.
|
|
22
|
+
def email = resolve @email
|
|
23
|
+
|
|
24
|
+
# @return [String] organization the name stands for, left out of the card where unset.
|
|
25
|
+
def org = resolve @org
|
|
26
|
+
|
|
27
|
+
# @return [String, Pathname] picture the card carries, left out of the card where unset.
|
|
28
|
+
def photo = resolve @photo
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def resolve(setting) = setting.respond_to?(:call) ? setting.call : setting
|
|
33
|
+
|
|
34
|
+
def missing(setting) = raise(Error, "Crest.config.#{setting} has to be set")
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/crest/error.rb
ADDED
data/lib/crest/photo.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'crest/error'
|
|
2
|
+
|
|
3
|
+
module Crest
|
|
4
|
+
# The picture a card carries: bytes as they were handed over, or the file a path names.
|
|
5
|
+
class Photo
|
|
6
|
+
# The first bytes of each image format a phone will read a saved card's picture from.
|
|
7
|
+
SIGNATURES = { 'PNG' => "\x89PNG".b, 'JPEG' => "\xFF\xD8\xFF".b }
|
|
8
|
+
|
|
9
|
+
# The picture a path or a string of bytes stands for, read from disk only the first time,
|
|
10
|
+
# so a card handed out a thousand times costs one read and one encoding.
|
|
11
|
+
# @return [Crest::Photo] picture that file holds, or those bytes are.
|
|
12
|
+
def self.of(photo)
|
|
13
|
+
@read ||= {}
|
|
14
|
+
@read[photo] ||= new photo
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param photo [String, Pathname] image bytes, or the file to read them from.
|
|
18
|
+
def initialize(photo)
|
|
19
|
+
@bytes = photo.respond_to?(:binread) ? photo.binread : photo
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String] PHOTO property, base64-encoded and typed by the image's own first bytes.
|
|
23
|
+
def to_s = @property ||= "PHOTO;ENCODING=b;TYPE=#{type}:#{[@bytes].pack 'm0'}"
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def type
|
|
28
|
+
SIGNATURES.find { |_, signature| @bytes.b.start_with? signature }&.first ||
|
|
29
|
+
raise(Error, 'a photo has to be a PNG or a JPEG')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'crest/routes'
|
|
2
|
+
|
|
3
|
+
module Crest
|
|
4
|
+
# Teaches a host's routes file the `crest` method, and loads the controller that answers it.
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
initializer 'crest.routes' do |app|
|
|
7
|
+
ActionDispatch::Routing::Mapper.include Crest::Routes
|
|
8
|
+
|
|
9
|
+
# Loaded once the routes are drawn, so the controller inherits the host's own URL helpers
|
|
10
|
+
# the way a controller the host wrote does.
|
|
11
|
+
app.config.to_prepare { require 'crest/cards_controller' }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/crest/routes.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Crest
|
|
2
|
+
# Extends the config/routes.rb DSL, so `crest` draws a contact card wherever a host puts it.
|
|
3
|
+
module Routes
|
|
4
|
+
# Draws a GET at the path the host names, answered by the card the initializer describes.
|
|
5
|
+
# The host names it because the filename is the app's own name, and the helper is named after
|
|
6
|
+
# the file -- `crest 'houseaccount.vcf'` gives `houseaccount_vcf_path`. A route rather than an
|
|
7
|
+
# engine to mount, so the line may sit inside a `scope` or a `constraints` the host wrote.
|
|
8
|
+
# @param path [String] name the card is downloaded under, `.vcf` and all.
|
|
9
|
+
# @return [void]
|
|
10
|
+
def crest(path)
|
|
11
|
+
get path, to: 'crest/cards#show', as: path.parameterize(separator: '_')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/crest.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'crest/error'
|
|
2
|
+
require 'crest/version'
|
|
3
|
+
require 'crest/config'
|
|
4
|
+
require 'crest/card'
|
|
5
|
+
|
|
6
|
+
# Rails-only from here: the builder above is plain Ruby, and a script may load it on its own.
|
|
7
|
+
require 'crest/railtie' if defined?(Rails)
|
|
8
|
+
|
|
9
|
+
# A contact card an app hands out, so a text from an unknown number arrives under a name.
|
|
10
|
+
module Crest
|
|
11
|
+
# @return [Crest::Config] everything a card has to be told about the app around it.
|
|
12
|
+
def self.config = @config ||= Config.new
|
|
13
|
+
|
|
14
|
+
# Yields the configuration, so a host states its own facts in one initializer.
|
|
15
|
+
# @return [void]
|
|
16
|
+
def self.configure = yield config
|
|
17
|
+
|
|
18
|
+
# Built afresh for every request, since a setting stated as a callable is asked each time.
|
|
19
|
+
# @return [Crest::Card] card this app is saved under.
|
|
20
|
+
def self.card
|
|
21
|
+
Card.new name: config.name, phone: config.phone, url: config.url, email: config.email,
|
|
22
|
+
org: config.org, photo: config.photo
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: crest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Claudio Baccigalupo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Answers a .vcf route with the vCard a phone offers to save
|
|
13
|
+
email:
|
|
14
|
+
- claudiob@users.noreply.github.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- LICENSE.txt
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/crest.rb
|
|
23
|
+
- lib/crest/card.rb
|
|
24
|
+
- lib/crest/cards_controller.rb
|
|
25
|
+
- lib/crest/config.rb
|
|
26
|
+
- lib/crest/error.rb
|
|
27
|
+
- lib/crest/photo.rb
|
|
28
|
+
- lib/crest/railtie.rb
|
|
29
|
+
- lib/crest/routes.rb
|
|
30
|
+
- lib/crest/version.rb
|
|
31
|
+
homepage: https://github.com/claudiob/crest
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://github.com/claudiob/crest
|
|
36
|
+
source_code_uri: https://github.com/claudiob/crest/
|
|
37
|
+
changelog_uri: https://github.com/claudiob/crest/blob/main/CHANGELOG.md
|
|
38
|
+
documentation_uri: https://rubydoc.info/gems/crest
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 4.0.3
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Hand out a contact card your app is saved under
|
|
56
|
+
test_files: []
|