shill 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/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +12 -0
- data/lib/shill/version.rb +5 -0
- data/lib/shill.rb +152 -0
- data/sig/shill.rbs +4 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c856b97b9e77c751f97f81f2b4ccf1d4bab2d939ad808d86cd86703bbe7616ae
|
4
|
+
data.tar.gz: 2aee215e4a54aed30f179e7da637a3ede554e3a689bc36690f39d7251b1941e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7d7abda12e3b7d075e20cce210951639a814379542537cc922cc216178f39e2f6fbbc55ed6ff2bee8973890758f8ff71965d1c87cb128b70ef3757a561ba37a7
|
7
|
+
data.tar.gz: 4dd8770a375b40f3d2440ca5a51859abbd3447306cf1bc8e9df136cbebc1f38daeca369962510fe6865715a349f21bd37fee15a8463cb03ca1871e85217f7427
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Marc Köhlbrugge
|
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,148 @@
|
|
1
|
+
# Shill
|
2
|
+
|
3
|
+
A tiny, dependency-free Ruby gem that fetches and exposes a list of projects from a remote JSON endpoint.
|
4
|
+
|
5
|
+
* Fetch once, cache in memory.
|
6
|
+
* Pick a random project with one call.
|
7
|
+
* Zero external dependencies (uses Net::HTTP & JSON from Ruby stdlib).
|
8
|
+
* Rails-friendly configuration via `Shill.configure`.
|
9
|
+
|
10
|
+
---
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add the gem to your project:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem "shill", github: "marckohlbrugge/shill" # until published on RubyGems
|
18
|
+
gem "shill" # once published
|
19
|
+
```
|
20
|
+
|
21
|
+
Then install:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
### Rails
|
30
|
+
|
31
|
+
Create `config/initializers/shill.rb`:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Shill.configure do |config|
|
35
|
+
config.endpoint_url = "https://marc.io/shill.json"
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
### Plain Ruby
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require "shill"
|
43
|
+
|
44
|
+
Shill.endpoint_url = "https://marc.io/shill.json"
|
45
|
+
```
|
46
|
+
|
47
|
+
---
|
48
|
+
|
49
|
+
## JSON format
|
50
|
+
|
51
|
+
The endpoint must return an **array of objects** with these keys:
|
52
|
+
|
53
|
+
* **url** – required
|
54
|
+
* **description** – required
|
55
|
+
* **name** – required
|
56
|
+
* **logo_url** – optional
|
57
|
+
|
58
|
+
Example:
|
59
|
+
|
60
|
+
```json
|
61
|
+
[
|
62
|
+
{
|
63
|
+
"name": "BetaList",
|
64
|
+
"url": "https://betalist.com",
|
65
|
+
"description": "Startup discovery platform"
|
66
|
+
},
|
67
|
+
{
|
68
|
+
"name": "Room AI",
|
69
|
+
"url": "https://roomai.com",
|
70
|
+
"description": "Generate interior design with AI",
|
71
|
+
"logo_url": "https://roomai.com/logo.png"
|
72
|
+
}
|
73
|
+
]
|
74
|
+
```
|
75
|
+
|
76
|
+
---
|
77
|
+
|
78
|
+
## Usage
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# All projects (memoised)
|
82
|
+
projects = Shill.projects
|
83
|
+
|
84
|
+
# A single random project (memoised list reused)
|
85
|
+
featured = Shill.random_project
|
86
|
+
|
87
|
+
puts featured.name # => "Room AI"
|
88
|
+
puts featured.url # => "https://roomai.com"
|
89
|
+
```
|
90
|
+
|
91
|
+
Need fresh data? Pass `refresh: true`:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
Shill.projects(refresh: true)
|
95
|
+
```
|
96
|
+
|
97
|
+
---
|
98
|
+
|
99
|
+
## Error handling
|
100
|
+
|
101
|
+
Every exception is wrapped in `Shill::Error` with a helpful message, e.g.:
|
102
|
+
|
103
|
+
* endpoint not configured
|
104
|
+
* invalid JSON
|
105
|
+
* missing required keys
|
106
|
+
|
107
|
+
---
|
108
|
+
|
109
|
+
## Development
|
110
|
+
|
111
|
+
```bash
|
112
|
+
git clone https://github.com/marckohlbrugge/shill.git
|
113
|
+
cd shill
|
114
|
+
bundle install
|
115
|
+
bundle exec rake test # runs the Minitest suite
|
116
|
+
```
|
117
|
+
|
118
|
+
---
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
Pull requests are very welcome. Please include tests for any change.
|
123
|
+
|
124
|
+
---
|
125
|
+
|
126
|
+
## License
|
127
|
+
|
128
|
+
Shill is released under the MIT License. See `LICENSE.txt` for details.
|
129
|
+
|
130
|
+
---
|
131
|
+
|
132
|
+
## Caching
|
133
|
+
|
134
|
+
By default Shill caches the fetched payload in memory. If you're running inside a Rails application (with `Rails.cache` configured), it will automatically use that instead. You can also set your own cache store:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
Shill.configure do |c|
|
138
|
+
c.cache_store = MyCustomCache.new # must respond to fetch(key) { ... } and delete(key)
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
Force a reload at any time:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
Shill.projects(refresh: true) # bypasses cache and stores fresh data
|
146
|
+
```
|
147
|
+
|
148
|
+
---
|
data/Rakefile
ADDED
data/lib/shill.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "shill/version"
|
4
|
+
|
5
|
+
module Shill
|
6
|
+
class Error < StandardError; end
|
7
|
+
# Your code goes here...
|
8
|
+
|
9
|
+
# Simple value-object that stores all configurable options.
|
10
|
+
class Configuration
|
11
|
+
# URL that returns a JSON array of project objects.
|
12
|
+
attr_accessor :endpoint_url
|
13
|
+
|
14
|
+
# Custom cache store (defaults to Rails.cache when available or a lightweight in-memory store).
|
15
|
+
# The store should respond to `fetch(key, **options) { ... }` and `delete(key)`.
|
16
|
+
attr_accessor :cache_store
|
17
|
+
end
|
18
|
+
|
19
|
+
# Simple value object representing a single project.
|
20
|
+
Project = Struct.new(:name, :url, :description, :logo_url, keyword_init: true)
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Access the global configuration object.
|
24
|
+
def configuration
|
25
|
+
@configuration ||= Configuration.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Configure the gem inside an initializer or plain Ruby
|
29
|
+
# Example (Rails):
|
30
|
+
# Shill.configure do |config|
|
31
|
+
# config.endpoint_url = "https://example.com/projects.json"
|
32
|
+
# end
|
33
|
+
def configure
|
34
|
+
yield(configuration) if block_given?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Convenience getter/setter so existing `Shill.endpoint_url =` still works.
|
38
|
+
def endpoint_url
|
39
|
+
configuration.endpoint_url
|
40
|
+
end
|
41
|
+
|
42
|
+
def endpoint_url=(value)
|
43
|
+
configuration.endpoint_url = value
|
44
|
+
end
|
45
|
+
|
46
|
+
# Choose the cache store. Priority:
|
47
|
+
# 1. Explicitly configured via `Shill.configure { |c| c.cache_store = ... }`
|
48
|
+
# 2. Rails.cache if Rails is loaded
|
49
|
+
# 3. A simple in-process memory cache
|
50
|
+
def cache_store
|
51
|
+
configuration.cache_store || default_cache_store
|
52
|
+
end
|
53
|
+
|
54
|
+
def cache_store=(store)
|
55
|
+
configuration.cache_store = store
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return all projects fetched from the configured endpoint.
|
59
|
+
# Results are memoised – pass `refresh: true` to force a new HTTP request.
|
60
|
+
def projects(refresh: false)
|
61
|
+
clear_cache if refresh
|
62
|
+
|
63
|
+
cache_store.fetch("shill_projects") { fetch_projects }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return a single random project (or nil if none present).
|
67
|
+
def random_project(refresh: false)
|
68
|
+
projects(refresh: refresh).sample
|
69
|
+
end
|
70
|
+
|
71
|
+
# Remove cached payload.
|
72
|
+
def clear_cache
|
73
|
+
if cache_store.respond_to?(:delete)
|
74
|
+
cache_store.delete("shill_projects")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# Fetch and parse the projects JSON from the endpoint.
|
81
|
+
def fetch_projects
|
82
|
+
raise Error, "Shill.endpoint_url must be configured" unless endpoint_url && !endpoint_url.empty?
|
83
|
+
|
84
|
+
require "json"
|
85
|
+
require "uri"
|
86
|
+
require "net/http"
|
87
|
+
|
88
|
+
uri = URI.parse(endpoint_url)
|
89
|
+
|
90
|
+
response_body = Net::HTTP.get(uri)
|
91
|
+
|
92
|
+
begin
|
93
|
+
parsed = JSON.parse(response_body, symbolize_names: true)
|
94
|
+
rescue JSON::ParserError => e
|
95
|
+
raise Error, "Invalid JSON received from #{endpoint_url}: #{e.message}"
|
96
|
+
end
|
97
|
+
|
98
|
+
validate_projects!(parsed)
|
99
|
+
# Return an array of Project objects for nicer dot-notation access.
|
100
|
+
parsed.map { |attrs| Project.new(attrs) }
|
101
|
+
rescue StandardError => e
|
102
|
+
# Re-wrap into Shill::Error to keep API consistent
|
103
|
+
raise Error, e.message unless e.is_a?(Error)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Ensure we have an array of hashes with required keys.
|
107
|
+
def validate_projects!(obj)
|
108
|
+
unless obj.is_a?(Array)
|
109
|
+
raise Error, "Projects JSON must be an array"
|
110
|
+
end
|
111
|
+
|
112
|
+
obj.each_with_index do |proj, idx|
|
113
|
+
unless proj.is_a?(Hash)
|
114
|
+
raise Error, "Project at index #{idx} must be an object"
|
115
|
+
end
|
116
|
+
|
117
|
+
required_keys = %i[name url description]
|
118
|
+
|
119
|
+
missing = required_keys.reject { |k| proj.key?(k) }
|
120
|
+
|
121
|
+
unless missing.empty?
|
122
|
+
raise Error, "Project at index #{idx} is missing keys: #{missing.join(", ")}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Determine the default cache store based on environment.
|
128
|
+
def default_cache_store
|
129
|
+
if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
|
130
|
+
Rails.cache
|
131
|
+
else
|
132
|
+
@memory_cache ||= MemoryCache.new
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Minimal in-process cache that exposes `fetch`/`delete` to mimic ActiveSupport::Cache API.
|
137
|
+
class MemoryCache
|
138
|
+
def initialize
|
139
|
+
@data = {}
|
140
|
+
end
|
141
|
+
|
142
|
+
def fetch(key)
|
143
|
+
return @data[key] if @data.key?(key)
|
144
|
+
@data[key] = yield
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete(key)
|
148
|
+
@data.delete(key)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/sig/shill.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shill
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marc Köhlbrugge
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A tiny dependency-free gem that fetches a remote JSON file containing
|
14
|
+
projects and exposes helper methods to list or randomly select them.
|
15
|
+
email:
|
16
|
+
- subscriptions@marckohlbrugge.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- CHANGELOG.md
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- lib/shill.rb
|
27
|
+
- lib/shill/version.rb
|
28
|
+
- sig/shill.rbs
|
29
|
+
homepage: https://github.com/example/shill
|
30
|
+
licenses:
|
31
|
+
- MIT
|
32
|
+
metadata:
|
33
|
+
allowed_push_host: https://rubygems.org
|
34
|
+
homepage_uri: https://github.com/example/shill
|
35
|
+
source_code_uri: https://github.com/example/shill
|
36
|
+
changelog_uri: https://github.com/example/shill/blob/main/CHANGELOG.md
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.1.0
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubygems_version: 3.5.3
|
53
|
+
signing_key:
|
54
|
+
specification_version: 4
|
55
|
+
summary: Fetch and promote your projects via JSON endpoint.
|
56
|
+
test_files: []
|