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 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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Crest
2
+ # Raised for every failure the gem reports, so hosts can rescue one type.
3
+ class Error < StandardError; end
4
+ end
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Crest
2
+ # The version of this gem, as RubyGems knows it.
3
+ VERSION = '0.1.0'
4
+ 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: []