ten_cubed 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/.rspec +3 -0
- data/.standard.yml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +202 -0
- data/Rakefile +19 -0
- data/lib/generators/ten_cubed/connection/connection_generator.rb +37 -0
- data/lib/generators/ten_cubed/connection/templates/connection.rb +25 -0
- data/lib/generators/ten_cubed/connection/templates/create_connections.rb +15 -0
- data/lib/generators/ten_cubed/install/install_generator.rb +69 -0
- data/lib/generators/ten_cubed/install/templates/add_max_degree_to_users.rb +8 -0
- data/lib/generators/ten_cubed/install/templates/initializer.rb +17 -0
- data/lib/generators/ten_cubed/user/templates/create_users.rb +18 -0
- data/lib/generators/ten_cubed/user/templates/user.rb +9 -0
- data/lib/generators/ten_cubed/user/user_generator.rb +54 -0
- data/lib/ten_cubed/configuration.rb +22 -0
- data/lib/ten_cubed/connection.rb +38 -0
- data/lib/ten_cubed/engine.rb +39 -0
- data/lib/ten_cubed/models/concerns/ten_cubed_user.rb +69 -0
- data/lib/ten_cubed/version.rb +5 -0
- data/lib/ten_cubed.rb +23 -0
- data/sig/ten_cubed.rbs +4 -0
- metadata +211 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 46b0d9afa96923136e1b0a005ce47f86518d3cf6bb9d527881df2df1157fd800
|
4
|
+
data.tar.gz: 25c8497f22e9f5576d3a458f1aa7151d9f8b4355be9005b111422fc4520212e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ed86c62a478218f8470926cc3683a79ddda901df554e2b72dda52bcf5b325518dc7ca6f9cec27b22bfbeb90f742fd278a1d6340528902deedf4e459af8f3558e
|
7
|
+
data.tar.gz: 6304479ac2398459ec173b3e7cb439c9ff74657413b1ac8cee60385c2e5f545e25e161edcdbb656701ab221ae21b2c8da2a867a847b50f39189463d816f4bc47
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Sebastian Wildwood
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# ten_cubed
|
2
|
+
[](https://github.com/darkpicnic/ten_cubed/actions/workflows/main.yml)
|
3
|
+
|
4
|
+
A Ruby on Rails gem implementing the ten_cubed networking system - an artificially restricted social graph that limits users to 10 direct connections with a maximum network size of 1,110 total connections (10 + 100 + 1000).
|
5
|
+
|
6
|
+
The ten_cubed system promotes healthier social interactions by imposing natural limits on connection growth, limiting viral content spread, and eliminating influencer dynamics.
|
7
|
+
|
8
|
+
## Disclosure of AI generated content
|
9
|
+
The core functionality of ten_cubed was written by myself, however, this gem was almost entirely AI generated through Cursor + Claude 3.7. I've added specific strings throughout the project to indicate when files were entirely written by AI. There is almost certainly some head scratchers in this repository; feel free to submit PRs if something seems really off.
|
10
|
+
|
11
|
+
## Project Status
|
12
|
+
|
13
|
+
⚠️ **ACTIVE DEVELOPMENT** ⚠️
|
14
|
+
|
15
|
+
This project is currently in active development. APIs and functionality may change without notice. Breaking changes are possible between versions. While we encourage testing and contributions, this gem is not yet recommended for production environments without thorough evaluation.
|
16
|
+
|
17
|
+
## Requirements
|
18
|
+
|
19
|
+
* Ruby 3.2+
|
20
|
+
* Rails 7.0+
|
21
|
+
* PostgreSQL database (required for the recursive CTE queries used by the gem)
|
22
|
+
|
23
|
+
## About
|
24
|
+
|
25
|
+
The ten_cubed networking system is based on three key concepts:
|
26
|
+
|
27
|
+
1. Users can have no more than 10 connections in their immediate network
|
28
|
+
2. Users have three levels of their network (1st, 2nd, and 3rd degree) with a theoretical maximum of 1,110 total connections
|
29
|
+
3. Users can set the maximum degree allowed for different activities within your app
|
30
|
+
|
31
|
+
For more details on the ten_cubed concept, see: [https://highlyprobable.io/articles/ten-cubed](https://highlyprobable.io/articles/ten-cubed)
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
|
35
|
+
Add this line to your application's Gemfile:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
gem 'ten_cubed', github: "darkpicnic/ten_cubed"
|
39
|
+
```
|
40
|
+
|
41
|
+
And then execute:
|
42
|
+
|
43
|
+
```bash
|
44
|
+
$ bundle install
|
45
|
+
```
|
46
|
+
|
47
|
+
Then run the installer:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
$ rails generate ten_cubed:install
|
51
|
+
```
|
52
|
+
|
53
|
+
The installer will:
|
54
|
+
- Create an initializer at `config/initializers/ten_cubed.rb`
|
55
|
+
- Check if a User model exists
|
56
|
+
- If it exists: Add a migration for the `max_degree` column and inject the TenCubed concern
|
57
|
+
- If it doesn't exist: Generate a new User model with TenCubed functionality
|
58
|
+
- Check if a Connection model exists
|
59
|
+
- If it exists: Raise an error (you need to rename or remove the existing model)
|
60
|
+
- If it doesn't exist: Generate the Connection model for TenCubed
|
61
|
+
- Install all required migrations
|
62
|
+
|
63
|
+
After running the installer, don't forget to run:
|
64
|
+
|
65
|
+
```bash
|
66
|
+
$ rails db:migrate
|
67
|
+
```
|
68
|
+
|
69
|
+
### Manual Setup (Alternative)
|
70
|
+
|
71
|
+
If you prefer to set up the components individually:
|
72
|
+
|
73
|
+
```bash
|
74
|
+
# Generate the User model (if you don't already have one)
|
75
|
+
$ rails generate ten_cubed:user
|
76
|
+
|
77
|
+
# Generate the Connection model
|
78
|
+
$ rails generate ten_cubed:connection
|
79
|
+
|
80
|
+
# Run migrations
|
81
|
+
$ rails db:migrate
|
82
|
+
```
|
83
|
+
|
84
|
+
## Usage
|
85
|
+
|
86
|
+
### Configuration
|
87
|
+
|
88
|
+
You can configure TenCubed in the initializer file at `config/initializers/ten_cubed.rb`:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
TenCubed.configure do |config|
|
92
|
+
# Maximum number of direct connections a user can have
|
93
|
+
# Default: 10
|
94
|
+
config.max_direct_connections = 10
|
95
|
+
|
96
|
+
# Maximum network depth for querying connections
|
97
|
+
# Default: 3
|
98
|
+
config.max_network_depth = 3
|
99
|
+
|
100
|
+
# Table name for connections
|
101
|
+
# Default: :connections
|
102
|
+
config.connection_table_name = :connections
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
### User Model
|
107
|
+
|
108
|
+
If you already have a User model, the `ten_cubed:install` generator will add the `max_degree` column to your users table automatically. If you create a User model with the `ten_cubed:user` generator, the model will include the TenCubed functionality by default.
|
109
|
+
|
110
|
+
User models will have the following methods:
|
111
|
+
|
112
|
+
#### `my_network`
|
113
|
+
|
114
|
+
Returns the user's network up to their `max_degree` setting.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
current_user.my_network
|
118
|
+
# => [<User>, <User>, ...] (array of users in the network)
|
119
|
+
```
|
120
|
+
|
121
|
+
#### `degree_of_connection(user)`
|
122
|
+
|
123
|
+
Returns the degree of connection between the current user and another user.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
current_user.degree_of_connection(other_user)
|
127
|
+
# => 1 (direct connection)
|
128
|
+
# => 2 (friend of friend)
|
129
|
+
# => 3 (third-degree connection)
|
130
|
+
# => nil (not connected)
|
131
|
+
```
|
132
|
+
|
133
|
+
#### `in_network?(user)`
|
134
|
+
|
135
|
+
Checks if a user is in the current user's network.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
current_user.in_network?(other_user)
|
139
|
+
# => true or false
|
140
|
+
```
|
141
|
+
|
142
|
+
#### `network(max_depth = 3)`
|
143
|
+
|
144
|
+
Returns the user's network up to the specified depth (1-3).
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
current_user.network(2)
|
148
|
+
# => [<User>, <User>, ...] (users within 2 degrees)
|
149
|
+
```
|
150
|
+
|
151
|
+
### Connection Model
|
152
|
+
|
153
|
+
The `Connection` model enforces the ten_cubed constraints:
|
154
|
+
|
155
|
+
1. A user cannot have more than 10 connections
|
156
|
+
2. A user cannot connect to themselves
|
157
|
+
|
158
|
+
To create a connection between two users:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
TenCubed::Connection.create(user: current_user, target: other_user)
|
162
|
+
```
|
163
|
+
|
164
|
+
### Example Usage
|
165
|
+
|
166
|
+
Here's an example of how you might use ten_cubed in a controller:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class PostsController < ApplicationController
|
170
|
+
def index
|
171
|
+
# Show posts only from users in the current user's network
|
172
|
+
@posts = Post.where(user_id: current_user.my_network.pluck(:id) + [current_user.id])
|
173
|
+
end
|
174
|
+
|
175
|
+
def show
|
176
|
+
@post = Post.find(params[:id])
|
177
|
+
|
178
|
+
# Check if the current user can access this post based on connection degree
|
179
|
+
unless current_user.id == @post.user_id ||
|
180
|
+
(current_user.in_network?(@post.user) &&
|
181
|
+
current_user.degree_of_connection(@post.user) <= @post.visibility_degree)
|
182
|
+
flash[:alert] = "You don't have permission to view this post"
|
183
|
+
redirect_to posts_path
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
## Development
|
190
|
+
|
191
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
192
|
+
|
193
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
194
|
+
|
195
|
+
## Contributing
|
196
|
+
|
197
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/darkpicnic/ten_cubed.
|
198
|
+
|
199
|
+
## License
|
200
|
+
|
201
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
202
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
# Task specifically for testing generators
|
9
|
+
RSpec::Core::RakeTask.new(:generator_spec) do |t|
|
10
|
+
t.pattern = "spec/generators/**/*_spec.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
require "standard/rake"
|
14
|
+
|
15
|
+
task default: %i[spec standard]
|
16
|
+
|
17
|
+
# Run generator tests specifically
|
18
|
+
desc "Run generator tests"
|
19
|
+
task test_generators: :generator_spec
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
module TenCubed
|
6
|
+
module Generators
|
7
|
+
class ConnectionGenerator < Rails::Generators::Base
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
desc "Create Connection model and migration for TenCubed"
|
11
|
+
|
12
|
+
# Required for Rails::Generators::Migration
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
next_migration_number = current_migration_number(dirname) + 1
|
15
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_connection_model
|
19
|
+
template "connection.rb", "app/models/connection.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_connection_migration
|
23
|
+
migration_template "create_connections.rb",
|
24
|
+
"db/migrate/create_connections.rb",
|
25
|
+
migration_version: migration_version
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def migration_version
|
31
|
+
if Rails.version >= "5.0.0"
|
32
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
class Connection < ApplicationRecord
|
6
|
+
belongs_to :user
|
7
|
+
belongs_to :target, class_name: "User"
|
8
|
+
|
9
|
+
validate :ensure_user_has_less_than_max_connections, on: :create
|
10
|
+
validate :ensure_target_is_not_self, on: :create
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def ensure_user_has_less_than_max_connections
|
15
|
+
if user.connections.count >= 10
|
16
|
+
errors.add(:user, "can't have more than 10 connections")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def ensure_target_is_not_self
|
21
|
+
if user == target
|
22
|
+
errors.add(:target, "can't be the same as user")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Created by AI
|
3
|
+
|
4
|
+
class CreateConnections < ActiveRecord::Migration[<%= migration_version %>]
|
5
|
+
def change
|
6
|
+
create_table :connections do |t|
|
7
|
+
t.references :user, null: false, foreign_key: true
|
8
|
+
t.references :target, null: false, foreign_key: { to_table: :users }
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :connections, [:user_id, :target_id], unique: true
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
module TenCubed
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
desc "Install TenCubed into your Rails application"
|
11
|
+
|
12
|
+
# Required for Rails::Generators::Migration
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
next_migration_number = current_migration_number(dirname) + 1
|
15
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_migrations
|
19
|
+
rails_command "railties:install:migrations FROM=ten_cubed", inline: true
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_initializer
|
23
|
+
template "initializer.rb", "config/initializers/ten_cubed.rb"
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup_user_model
|
27
|
+
if model_exists?("User")
|
28
|
+
add_max_degree_to_existing_user
|
29
|
+
inject_ten_cubed_user_concern
|
30
|
+
else
|
31
|
+
generate "ten_cubed:user"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_connection_model
|
36
|
+
if model_exists?("Connection")
|
37
|
+
# Throw an error if Connection model already exists
|
38
|
+
raise "Connection model already exists. Please remove it or rename it before installing TenCubed."
|
39
|
+
else
|
40
|
+
generate "ten_cubed:connection"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def model_exists?(model_name)
|
47
|
+
File.exist?(Rails.root.join("app", "models", "#{model_name.underscore}.rb"))
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_max_degree_to_existing_user
|
51
|
+
migration_template "add_max_degree_to_users.rb",
|
52
|
+
"db/migrate/add_max_degree_to_users.rb",
|
53
|
+
migration_version: migration_version
|
54
|
+
end
|
55
|
+
|
56
|
+
def inject_ten_cubed_user_concern
|
57
|
+
inject_into_file "app/models/user.rb", after: "class User < ApplicationRecord\n" do
|
58
|
+
" include TenCubed::Models::Concerns::TenCubedUser\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def migration_version
|
63
|
+
if Rails.version >= "5.0.0"
|
64
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
TenCubed.configure do |config|
|
6
|
+
# Maximum number of direct connections a user can have
|
7
|
+
# Default: 10
|
8
|
+
# config.max_direct_connections = 10
|
9
|
+
|
10
|
+
# Maximum network depth for querying connections
|
11
|
+
# Default: 3
|
12
|
+
# config.max_network_depth = 3
|
13
|
+
|
14
|
+
# Table name for connections
|
15
|
+
# Default: :connections
|
16
|
+
# config.connection_table_name = :connections
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Created by AI
|
3
|
+
|
4
|
+
class CreateUsers < ActiveRecord::Migration[<%= migration_version %>]
|
5
|
+
def change
|
6
|
+
create_table :users do |t|
|
7
|
+
t.string :name
|
8
|
+
t.string :email, null: false, default: ""
|
9
|
+
t.integer :max_degree, default: 3, null: false
|
10
|
+
|
11
|
+
# Add any other fields needed for your User model
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
add_index :users, :email, unique: true
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
module TenCubed
|
6
|
+
module Generators
|
7
|
+
class UserGenerator < Rails::Generators::Base
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
desc "Create a User model with ten_cubed functionality"
|
11
|
+
|
12
|
+
# Required for Rails::Generators::Migration
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
next_migration_number = current_migration_number(dirname) + 1
|
15
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_user_model
|
19
|
+
user_file = "app/models/user.rb"
|
20
|
+
|
21
|
+
if File.exist?(user_file)
|
22
|
+
# Check if the file already has the include statement
|
23
|
+
contents = File.read(user_file)
|
24
|
+
if contents.include?("include TenCubed::Models::Concerns::TenCubedUser")
|
25
|
+
say_status :identical, "User model already includes TenCubedUser", :blue
|
26
|
+
else
|
27
|
+
# Insert the include statement after the class declaration
|
28
|
+
inject_into_file user_file, after: /class User < .*\n/ do
|
29
|
+
" include TenCubed::Models::Concerns::TenCubedUser\n"
|
30
|
+
end
|
31
|
+
say_status :insert, "include TenCubed::Models::Concerns::TenCubedUser added to User model", :green
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# Create a new user.rb file from template
|
35
|
+
template "user.rb", user_file
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_user_migration
|
40
|
+
migration_template "create_users.rb",
|
41
|
+
"db/migrate/create_users.rb",
|
42
|
+
migration_version: migration_version
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def migration_version
|
48
|
+
if Rails.version >= "5.0.0"
|
49
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
module TenCubed
|
6
|
+
class Configuration
|
7
|
+
# Default maximum direct connections allowed
|
8
|
+
attr_accessor :max_direct_connections
|
9
|
+
|
10
|
+
# Default maximum network depth allowed
|
11
|
+
attr_accessor :max_network_depth
|
12
|
+
|
13
|
+
# Table name for the connections table
|
14
|
+
attr_accessor :connection_table_name
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@max_direct_connections = 10
|
18
|
+
@max_network_depth = 3
|
19
|
+
@connection_table_name = :connections
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
require "active_record"
|
6
|
+
|
7
|
+
module TenCubed
|
8
|
+
class Connection < ::ActiveRecord::Base
|
9
|
+
def self.table_name
|
10
|
+
TenCubed.configuration.connection_table_name.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
if defined?(::User)
|
14
|
+
belongs_to :user, class_name: "::User"
|
15
|
+
belongs_to :target, class_name: "::User"
|
16
|
+
else
|
17
|
+
# For tests
|
18
|
+
attr_accessor :user, :target
|
19
|
+
end
|
20
|
+
|
21
|
+
validate :ensure_user_has_less_than_max_connections, on: :create
|
22
|
+
validate :ensure_target_is_not_self, on: :create
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def ensure_user_has_less_than_max_connections
|
27
|
+
if user && user.connections.count >= TenCubed.configuration.max_direct_connections
|
28
|
+
errors.add(:user, "can't have more than #{TenCubed.configuration.max_direct_connections} connections")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_target_is_not_self
|
33
|
+
if user && target && user == target
|
34
|
+
errors.add(:target, "can't be the same as user")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
require "rails"
|
6
|
+
require "active_record"
|
7
|
+
require "active_support/all"
|
8
|
+
|
9
|
+
module TenCubed
|
10
|
+
class Engine < ::Rails::Engine
|
11
|
+
isolate_namespace TenCubed
|
12
|
+
|
13
|
+
config.generators do |g|
|
14
|
+
g.test_framework :rspec
|
15
|
+
g.fixture_replacement :factory_bot
|
16
|
+
g.factory_bot dir: "spec/factories"
|
17
|
+
end
|
18
|
+
|
19
|
+
initializer "ten_cubed.check_postgres" do
|
20
|
+
ActiveSupport.on_load(:active_record) do
|
21
|
+
unless /postgresql/i.match?(ActiveRecord::Base.connection.adapter_name)
|
22
|
+
raise "TenCubed requires PostgreSQL as the database adapter. Found: #{ActiveRecord::Base.connection.adapter_name}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
initializer "ten_cubed.load_concerns" do
|
28
|
+
ActiveSupport.on_load(:active_record) do
|
29
|
+
require "ten_cubed/models/concerns/ten_cubed_user"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
config.to_prepare do
|
34
|
+
if defined?(User)
|
35
|
+
User.include TenCubed::Models::Concerns::TenCubedUser
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Created by AI
|
4
|
+
|
5
|
+
require "active_record"
|
6
|
+
require "active_support/concern"
|
7
|
+
|
8
|
+
module TenCubed
|
9
|
+
module Models
|
10
|
+
module Concerns
|
11
|
+
module TenCubedUser
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
has_many :connections, dependent: :destroy, class_name: "TenCubed::Connection", foreign_key: "user_id"
|
16
|
+
has_many :friends, through: :connections, source: :target
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the user's network up to the user's max_degree
|
20
|
+
def my_network
|
21
|
+
@my_network ||= {}
|
22
|
+
@my_network[max_degree] ||= network(max_degree)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the degree of connection between self and another user
|
26
|
+
def degree_of_connection(user)
|
27
|
+
my_network.each do |connection|
|
28
|
+
return connection.degree if connection.id == user.id
|
29
|
+
end
|
30
|
+
nil # Return nil if no connection exists
|
31
|
+
end
|
32
|
+
|
33
|
+
# Checks if a user is in the current user's network
|
34
|
+
def in_network?(user)
|
35
|
+
my_network.include?(user)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a network of connections up to the specified max_depth
|
39
|
+
def network(max_depth = 3)
|
40
|
+
return [] if max_depth <= 0 || max_depth > 3
|
41
|
+
|
42
|
+
sql = <<-SQL
|
43
|
+
WITH RECURSIVE friend_of_friend AS (
|
44
|
+
SELECT connections.target_id, users.max_degree, 1 AS depth
|
45
|
+
FROM #{TenCubed.configuration.connection_table_name}
|
46
|
+
JOIN users ON connections.target_id = users.id
|
47
|
+
WHERE connections.user_id = :user_id
|
48
|
+
UNION ALL
|
49
|
+
SELECT connections.target_id, users.max_degree, depth + 1
|
50
|
+
FROM #{TenCubed.configuration.connection_table_name}
|
51
|
+
JOIN users ON connections.target_id = users.id
|
52
|
+
JOIN friend_of_friend ON connections.user_id = friend_of_friend.target_id
|
53
|
+
WHERE depth < :max_depth
|
54
|
+
AND depth < users.max_degree -- Only add users that can appear at current depth.
|
55
|
+
)
|
56
|
+
SELECT DISTINCT ON (users.id) users.*, friend_of_friend.depth AS degree
|
57
|
+
FROM users
|
58
|
+
JOIN friend_of_friend ON users.id = friend_of_friend.target_id
|
59
|
+
WHERE users.id != :user_id
|
60
|
+
ORDER BY users.id, friend_of_friend.depth;
|
61
|
+
SQL
|
62
|
+
|
63
|
+
connections = self.class.find_by_sql([sql, {user_id: id, max_depth: max_depth}])
|
64
|
+
connections.uniq { |c| c.id }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/ten_cubed.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require_relative "ten_cubed/version"
|
5
|
+
require_relative "ten_cubed/configuration"
|
6
|
+
require_relative "ten_cubed/connection"
|
7
|
+
require_relative "ten_cubed/models/concerns/ten_cubed_user"
|
8
|
+
require_relative "ten_cubed/engine" if defined?(Rails)
|
9
|
+
|
10
|
+
module TenCubed
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
# Configuration
|
14
|
+
class << self
|
15
|
+
def configuration
|
16
|
+
@configuration ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure
|
20
|
+
yield(configuration)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/sig/ten_cubed.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ten_cubed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Wildwood
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-03-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 7.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '7.0'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 7.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activerecord
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '7.0'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 7.0.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '7.0'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 7.0.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: pg
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.5'
|
60
|
+
type: :runtime
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '1.5'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rspec-rails
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 7.1.1
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 7.1.1
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: database_cleaner-active_record
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.2'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '2.2'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: standard
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1.3'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1.3'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: factory_bot_rails
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - "~>"
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '6.2'
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - "~>"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '6.2'
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: simplecov
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: 0.22.0
|
130
|
+
type: :development
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - "~>"
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 0.22.0
|
137
|
+
- !ruby/object:Gem::Dependency
|
138
|
+
name: generator_spec
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - "~>"
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: 0.9.5
|
144
|
+
type: :development
|
145
|
+
prerelease: false
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - "~>"
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: 0.9.5
|
151
|
+
description: The ten_cubed gem allows you to easily integrate an artificially restricted
|
152
|
+
social graph into your Rails application. It limits users to 10 direct connections
|
153
|
+
and provides a network of up to 1,110 total connections (10 + 100 + 1000) with configurable
|
154
|
+
degree access. Requires PostgreSQL.
|
155
|
+
email:
|
156
|
+
- sebastian@lemery.io
|
157
|
+
executables: []
|
158
|
+
extensions: []
|
159
|
+
extra_rdoc_files: []
|
160
|
+
files:
|
161
|
+
- ".rspec"
|
162
|
+
- ".standard.yml"
|
163
|
+
- CHANGELOG.md
|
164
|
+
- LICENSE
|
165
|
+
- README.md
|
166
|
+
- Rakefile
|
167
|
+
- lib/generators/ten_cubed/connection/connection_generator.rb
|
168
|
+
- lib/generators/ten_cubed/connection/templates/connection.rb
|
169
|
+
- lib/generators/ten_cubed/connection/templates/create_connections.rb
|
170
|
+
- lib/generators/ten_cubed/install/install_generator.rb
|
171
|
+
- lib/generators/ten_cubed/install/templates/add_max_degree_to_users.rb
|
172
|
+
- lib/generators/ten_cubed/install/templates/initializer.rb
|
173
|
+
- lib/generators/ten_cubed/user/templates/create_users.rb
|
174
|
+
- lib/generators/ten_cubed/user/templates/user.rb
|
175
|
+
- lib/generators/ten_cubed/user/user_generator.rb
|
176
|
+
- lib/ten_cubed.rb
|
177
|
+
- lib/ten_cubed/configuration.rb
|
178
|
+
- lib/ten_cubed/connection.rb
|
179
|
+
- lib/ten_cubed/engine.rb
|
180
|
+
- lib/ten_cubed/models/concerns/ten_cubed_user.rb
|
181
|
+
- lib/ten_cubed/version.rb
|
182
|
+
- sig/ten_cubed.rbs
|
183
|
+
homepage: https://github.com/darkpicnic/ten_cubed
|
184
|
+
licenses:
|
185
|
+
- MIT
|
186
|
+
metadata:
|
187
|
+
allowed_push_host: https://rubygems.org
|
188
|
+
homepage_uri: https://github.com/darkpicnic/ten_cubed
|
189
|
+
source_code_uri: https://github.com/darkpicnic/ten_cubed
|
190
|
+
changelog_uri: https://github.com/darkpicnic/ten_cubed/blob/main/CHANGELOG.md
|
191
|
+
documentation_uri: https://github.com/darkpicnic/ten_cubed/blob/main/README.md
|
192
|
+
post_install_message:
|
193
|
+
rdoc_options: []
|
194
|
+
require_paths:
|
195
|
+
- lib
|
196
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: 3.2.0
|
201
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
requirements: []
|
207
|
+
rubygems_version: 3.5.11
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: Implementation of the ten_cubed networking system for Rails applications
|
211
|
+
test_files: []
|