@@ -0,0 +1,2 @@ | |||||
.pid | |||||
log.txt |
@@ -0,0 +1,11 @@ | |||||
/.bundle/ | |||||
/.yardoc | |||||
/_yardoc/ | |||||
/coverage/ | |||||
/doc/ | |||||
/pkg/ | |||||
/spec/reports/ | |||||
/tmp/ | |||||
# rspec failure tracking | |||||
.rspec_status |
@@ -0,0 +1,3 @@ | |||||
--format documentation | |||||
--color | |||||
--require spec_helper |
@@ -0,0 +1,7 @@ | |||||
# frozen_string_literal: true | |||||
source "https://rubygems.org" | |||||
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } | |||||
# gem "rails" |
@@ -0,0 +1,11 @@ | |||||
GEM | |||||
remote: https://rubygems.org/ | |||||
specs: | |||||
PLATFORMS | |||||
ruby | |||||
DEPENDENCIES | |||||
BUNDLED WITH | |||||
2.1.4 |
@@ -0,0 +1,21 @@ | |||||
The MIT License (MIT) | |||||
Copyright (c) 2023 Luka | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in | |||||
all copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
THE SOFTWARE. |
@@ -0,0 +1,39 @@ | |||||
# Aries | |||||
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/aries`. To experiment with that code, run `bin/console` for an interactive prompt. | |||||
TODO: Delete this and the text above, and describe your gem | |||||
## Installation | |||||
Add this line to your application's Gemfile: | |||||
```ruby | |||||
gem 'aries' | |||||
``` | |||||
And then execute: | |||||
$ bundle install | |||||
Or install it yourself as: | |||||
$ gem install aries | |||||
## Usage | |||||
TODO: Write usage instructions here | |||||
## Development | |||||
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. | |||||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). | |||||
## Contributing | |||||
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/aries. | |||||
## License | |||||
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). |
@@ -0,0 +1,8 @@ | |||||
# frozen_string_literal: true | |||||
require "bundler/gem_tasks" | |||||
require "rspec/core/rake_task" | |||||
RSpec::Core::RakeTask.new(:spec) | |||||
task default: :spec |
@@ -0,0 +1,39 @@ | |||||
# frozen_string_literal: true | |||||
require_relative "lib/aries/version" | |||||
Gem::Specification.new do |spec| | |||||
spec.name = "aries" | |||||
spec.version = Aries::VERSION | |||||
spec.authors = ["Luka"] | |||||
spec.email = ["licina.luka@outlook.com"] | |||||
spec.summary = "TODO: Write a short summary, because RubyGems requires one." | |||||
spec.description = "TODO: Write a longer description or delete this line." | |||||
spec.homepage = "TODO: Put your gem's website or public repo URL here." | |||||
spec.license = "MIT" | |||||
spec.required_ruby_version = ">= 2.6.0" | |||||
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" | |||||
spec.metadata["homepage_uri"] = spec.homepage | |||||
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." | |||||
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." | |||||
# Specify which files should be added to the gem when it is released. | |||||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git. | |||||
spec.files = Dir.chdir(File.expand_path(__dir__)) do | |||||
`git ls-files -z`.split("\x0").reject do |f| | |||||
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) | |||||
end | |||||
end | |||||
spec.bindir = "exe" | |||||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } | |||||
spec.require_paths = ["lib"] | |||||
# Uncomment to register a new dependency of your gem | |||||
# spec.add_dependency "example-gem", "~> 1.0" | |||||
# For more information and examples about making a new gem, checkout our | |||||
# guide at: https://bundler.io/guides/creating_gem.html | |||||
end |
@@ -0,0 +1,15 @@ | |||||
#!/usr/bin/env ruby | |||||
# frozen_string_literal: true | |||||
require "bundler/setup" | |||||
require "aries" | |||||
# You can add fixtures and/or initialization code here to make experimenting | |||||
# with your gem easier. You can also use a different console, if you like. | |||||
# (If you use this, don't forget to add pry to your Gemfile!) | |||||
# require "pry" | |||||
# Pry.start | |||||
require "irb" | |||||
IRB.start(__FILE__) |
@@ -0,0 +1,8 @@ | |||||
#!/usr/bin/env bash | |||||
set -euo pipefail | |||||
IFS=$'\n\t' | |||||
set -vx | |||||
bundle install | |||||
# Do any other automated setup that you need to do here |
@@ -0,0 +1,106 @@ | |||||
require "json" | |||||
require "csv" | |||||
sentences = ['abcdefg', | |||||
'abcdhfg', | |||||
'abqwerty', | |||||
'abqwerty'] | |||||
def maker charlist, index, data, trie | |||||
if index == (charlist.length) | |||||
return trie | |||||
end | |||||
exists = trie.filter {|n| n.first == charlist[index] }.first | |||||
node = (! exists) ? [ | |||||
charlist[index], # symbol, | |||||
[], # items | |||||
] : exists | |||||
node[1] = maker charlist, index + 1, data, node[1] | |||||
if data && index == (charlist.length - 1) | |||||
if node[2] | |||||
node[2] << data | |||||
else | |||||
node[2] = [data] | |||||
end | |||||
end | |||||
if ! exists | |||||
trie << node | |||||
end | |||||
return trie | |||||
end | |||||
def fact value, data, trie | |||||
chars = value.to_s.chars | |||||
maker chars, 0, data, trie | |||||
end | |||||
initial_trie = [] | |||||
sentences.each_with_index do |sentence, idx| | |||||
intial_trie = fact sentence, idx, initial_trie | |||||
end | |||||
pp initial_trie | |||||
initial_trie = [] | |||||
File.readlines("/home/luka/Databases/aries/dissolver/db.csv").each_with_index do |row, idx| | |||||
if 1 > idx | |||||
next | |||||
end | |||||
entity, attribute, value, timestmap, added = CSV.parse_line row | |||||
added = added == 'true' | |||||
if 1 < value.to_s.length && value.to_s.length < 30 | |||||
initial_trie = fact "id_#{entity}", idx, initial_trie | |||||
initial_trie = fact "kv_#{attribute}_#{value}", entity, initial_trie | |||||
end | |||||
end | |||||
initial_trie | |||||
def find trie, phrase, index = 0 | |||||
if index == phrase.length - 1 | |||||
return trie.first.last | |||||
end | |||||
node = trie.filter { |n| n.first == phrase[index] }.first | |||||
if ! node | |||||
return nil | |||||
end | |||||
return find(node[1], phrase, index + 1) | |||||
end | |||||
criteria = { 'parent' => 'c282cb2a6395cc1d', | |||||
'back' => nil } | |||||
# x = find initial_trie, 'context_primary' # 'ede6dd2ccf3a0bfe' | |||||
results = {} | |||||
criteria.each do |k, v| | |||||
puts k | |||||
if ! v.is_a?(NilClass) | |||||
sequence = "#{k}_#{v}" | |||||
results[k] = find initial_trie, sequence | |||||
end | |||||
end | |||||
results |
@@ -0,0 +1,113 @@ | |||||
# frozen_string_literal: true | |||||
require_relative "aries/version" | |||||
require_relative "aries/database" | |||||
require "json" | |||||
require "socket" | |||||
# require "profile" | |||||
module Aries | |||||
class Error < StandardError; end | |||||
# Your code goes here... | |||||
def self.action callable | |||||
begin | |||||
[callable.call, nil] | |||||
rescue => e | |||||
pp e.backtrace << e | |||||
[nil, e] | |||||
end | |||||
end | |||||
def self.valid? commands | |||||
for command in commands | |||||
command.keys.each do |k| | |||||
if ! ["add", "find", "first", "last"].include? k | |||||
return [nil, "Undefined command key '#{k}'"] | |||||
end | |||||
end | |||||
end | |||||
return [true, nil] | |||||
end | |||||
host = "localhost" | |||||
port = 1234 | |||||
server = TCPServer.new host, port | |||||
puts "Server started, listening #{host}:#{port}" | |||||
dbs = {} | |||||
db_factory = lambda do |n| | |||||
if ! dbs.keys.include? n | |||||
dbs[n] = Database.new n | |||||
puts "Initialized database #{n}" | |||||
end | |||||
dbs[n] | |||||
end | |||||
# ticker thread | |||||
Thread.new do | |||||
limit = 60 * 10 | |||||
loop do | |||||
dbs.keys.each do |n| | |||||
db = dbs[n] | |||||
if db.used_at and ((db.used_at.to_i + limit) < Time.now.to_i) | |||||
puts "Closing database #{n} it was last used more than #{limit} seconds ago" | |||||
dbs.delete n | |||||
puts "Closed database #{n}" | |||||
end | |||||
end | |||||
sleep 1 | |||||
end | |||||
end | |||||
loop do | |||||
Thread.start server.accept do |client| | |||||
# client = server.accept | |||||
connection = JSON.parse(client.gets) | |||||
db = db_factory.call connection['database'] | |||||
# client.puts "Connected to Aries" | |||||
puts "Client connected #{client}" | |||||
while commands = client.gets | |||||
commands, err = action -> { JSON.parse commands } | |||||
if ! err.nil? | |||||
client.puts JSON.generate(err: err) | |||||
next | |||||
end | |||||
_, err = valid? commands | |||||
if ! err.nil? | |||||
client.puts JSON.generate(err: err) | |||||
next | |||||
end | |||||
results = {} | |||||
commands.each_with_index do |command, idx| | |||||
func, vals = command.to_a.first | |||||
begin | |||||
results[idx] = db.send(func, vals) | |||||
db.used_at = Time.now.to_i | |||||
rescue => e | |||||
pp [e.backtrace << e] | |||||
results = {err: e} | |||||
break | |||||
end | |||||
end | |||||
client.puts JSON.generate(results) | |||||
end | |||||
client.close | |||||
puts "Client disconnected #{client}" | |||||
end | |||||
end | |||||
end |
@@ -0,0 +1,396 @@ | |||||
require "csv" | |||||
require "erb" | |||||
require "fileutils" | |||||
require "json" | |||||
module Aries | |||||
class Index | |||||
attr_reader :path, :name | |||||
def initialize path, name | |||||
@path = path | |||||
@name = name | |||||
end | |||||
def refresh | |||||
puts "Refreshing index..." | |||||
File.readlines(File.join(@path, "#{@name}.csv")).each_with_index do |row, idx| | |||||
if 1 > idx | |||||
next | |||||
end | |||||
entity, attribute, value, timestamp, added = CSV.parse_line row | |||||
self.commit idx, entity | |||||
self.commit idx, attribute | |||||
end | |||||
puts "Index refreshed!" | |||||
end | |||||
def commit idx, target | |||||
fp = File.join @path, ".#{@name}.index.#{target}" | |||||
if File.exists?(fp) and File.foreach(fp).any? { |ln| ln[idx.to_s] } | |||||
return | |||||
end | |||||
File.write fp, "#{idx.to_s}\n", mode: 'a+' | |||||
end | |||||
def check target | |||||
fp = File.join @path, ".#{@name}.index.#{target}" | |||||
if ! File.exists?(fp) | |||||
return [] | |||||
end | |||||
return File.readlines(fp).map &:to_i | |||||
end | |||||
end | |||||
class Trie | |||||
attr_accessor :tree | |||||
def initialize | |||||
@tree = [] | |||||
end | |||||
def maker charlist, index, setter, trie | |||||
if index == (charlist.length) | |||||
return trie | |||||
end | |||||
exists = trie.filter {|n| n.first == charlist[index] }.first | |||||
node = (! exists) ? [ | |||||
charlist[index], # symbol, | |||||
[], # items | |||||
] : exists | |||||
node[1] = maker charlist, index + 1, setter, node[1] | |||||
if setter && index == (charlist.length - 1) | |||||
node[2] = setter.call node[2] | |||||
end | |||||
if ! exists | |||||
trie << node | |||||
end | |||||
return trie | |||||
end | |||||
def fact value, setter, trie | |||||
chars = value.to_s.chars | |||||
maker chars, 0, setter, trie | |||||
end | |||||
def refresh path | |||||
puts "Refreshing trie..." | |||||
File.readlines(path).each_with_index do |row, idx| | |||||
if 1 > idx | |||||
next | |||||
end | |||||
entity, attribute, value, timestmap, added = CSV.parse_line row | |||||
added = added == 'true' | |||||
if 1 < value.to_s.length && value.to_s.length < 30 | |||||
setter = added ? lambda { |leaf| (leaf or []) + [entity] } : lambda { |leaf| (leaf or []) - [entity] } | |||||
@tree = fact "id_#{entity}", lambda { |leaf| (leaf or []) << idx }, @tree | |||||
@tree = fact "kv_#{attribute}_#{value}", setter, @tree | |||||
end | |||||
end | |||||
puts "Trie refreshed!" | |||||
end | |||||
def find trie, path, index = 0 | |||||
if index == path.length - 1 | |||||
return trie.first.last | |||||
end | |||||
node = trie.filter { |n| n.first == path[index] }.first | |||||
if ! node | |||||
return nil | |||||
end | |||||
return find(node[1], path, index + 1) | |||||
end | |||||
def ready? | |||||
return true | |||||
end | |||||
def seek criteria, all = true | |||||
hits = [] | |||||
criteria.each do |k, v| | |||||
if ! v.is_a?(NilClass) | |||||
sequence = "kv_#{k}_#{v}" | |||||
hit = find @tree, sequence | |||||
if hit | |||||
hits += hit | |||||
end | |||||
end | |||||
end | |||||
return hits | |||||
end | |||||
end | |||||
class Database | |||||
attr_reader :headers | |||||
attr_accessor :index | |||||
attr_accessor :used_at | |||||
# @version ? | |||||
def initialize path_part, name = nil | |||||
path = File.join(Dir.home, "Databases/aries", path_part) | |||||
FileUtils.mkdir_p path | |||||
name = "db" unless name | |||||
@headers = ['entity', 'attribute', 'value', 'timestamp', 'added'] | |||||
@path = File.join path, "#{name}.csv" | |||||
@index = Index.new path, name | |||||
@trie = Trie.new | |||||
if ! File.exists? @path | |||||
CSV.open @path, "w" do |fh| | |||||
fh << @headers | |||||
end | |||||
end | |||||
@index.refresh | |||||
@trie.refresh @path | |||||
end | |||||
# @brief appends to database | |||||
# @version ? | |||||
# | |||||
# @todo append to file instead of loading a CSV object? | |||||
def append entity, attribute, value, added = true | |||||
idx = (File.foreach(@path).inject(0) { |c, ln| c+1 }) | |||||
CSV.open @path, "ab" do |fh| | |||||
@index.commit idx, entity | |||||
@index.commit idx, attribute | |||||
# obviously this should be within some Trie method | |||||
if 1 < value.to_s.length && value.to_s.length < 30 | |||||
setter = added ? lambda { |leaf| (leaf or []) + [entity] } : lambda { |leaf| (leaf or []) - [entity] } | |||||
@trie.tree = @trie.fact "id_#{entity}", lambda { |leaf| (leaf or []) << idx }, @trie.tree | |||||
@trie.tree = @trie.fact "kv_#{attribute}_#{value}", setter, @trie.tree | |||||
end | |||||
time = Time.now.to_i | |||||
fh << [entity, attribute, value, time, added] | |||||
end | |||||
end | |||||
# @version ? | |||||
def add eav | |||||
append eav['entity'], eav['attribute'], eav['value'] | |||||
end | |||||
def readline position | |||||
line = nil | |||||
File.open(@path, 'r') do |f| | |||||
position.to_i.times { f.gets } | |||||
line = f.gets | |||||
end | |||||
return line | |||||
end | |||||
# @brief finds (latest) exact attribute | |||||
# @version ? | |||||
def check entity_id, key | |||||
rows = @index.check entity_id | |||||
attr = @index.check key | |||||
pos = rows & attr | |||||
val = nil # 'random-'.concat (('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a).shuffle[0..4].join | |||||
File.readlines(@path).each_with_index do |row, idx| | |||||
if 1 > idx | |||||
next | |||||
end | |||||
if ! pos.include? idx | |||||
next | |||||
end | |||||
entity, attribute, value, timestamp, added = CSV.parse_line row | |||||
added = JSON.parse added | |||||
val = added ? value : nil | |||||
end | |||||
return val | |||||
end | |||||
# @version ? | |||||
def get entity_id | |||||
created = updated = 0 | |||||
rows = @index.check entity_id | |||||
facts = {} | |||||
File.readlines(@path).each_with_index do |row, idx| | |||||
if 1 > idx | |||||
next | |||||
end | |||||
if ! rows.empty? | |||||
if ! rows.include? idx | |||||
next | |||||
end | |||||
end | |||||
parts = CSV.parse_line row | |||||
entity, attribute, value, timestamp, added = parts | |||||
if entity == entity_id | |||||
added = JSON.parse added | |||||
facts[attribute] = added ? value : nil | |||||
created = [created, timestamp.to_i].min | |||||
updated = [updated, timestamp.to_i].max | |||||
end | |||||
end | |||||
facts['created'] = created | |||||
facts['updated'] = updated | |||||
facts['entity'] = entity_id | |||||
return facts | |||||
end | |||||
# @return Array<String> | |||||
# | |||||
# @version ? | |||||
def scan criteria, all = true | |||||
hits = [] | |||||
seen = [] | |||||
criteria = criteria.transform_keys &:to_sym | |||||
File.readlines(@path).each_with_index do |row, idx| | |||||
if 1 > idx | |||||
next | |||||
end | |||||
entity = row.split(',').first | |||||
if seen.include? entity | |||||
next | |||||
end | |||||
seen.append entity | |||||
entity, attribute, value, timestamp, added = CSV.parse_line row | |||||
added = added == 'true' | |||||
matches = {} | |||||
criteria.keys.each do |k| | |||||
v = criteria[k] | |||||
if k == "$check" | |||||
matches[k] = ERB.new(criteria[k]).result(binding) # @todo - this does not work | |||||
end | |||||
val = check entity, k | |||||
matches[k] = val == v | |||||
# pp e: entity, q: criteria, curr: {k: k, v: v}, db: val, eq: val == v | |||||
end | |||||
if matches.values.all? | |||||
record = get entity | |||||
record = record.transform_keys &:to_sym | |||||
if ! all | |||||
return [record] | |||||
end | |||||
hits.append record | |||||
end | |||||
end | |||||
return hits | |||||
end | |||||
def matching record, criteria | |||||
matches = {} | |||||
criteria.keys.each do |k| | |||||
v = criteria[k] | |||||
if k == "$check" | |||||
matches[k] = ERB.new(criteria[k]).result(binding) # @todo - this does not work | |||||
end | |||||
val = record[k] | |||||
matches[k] = val == v | |||||
# pp e: entity, q: criteria, curr: {k: k, v: v}, db: val, eq: val == v | |||||
end | |||||
return matches.values.all? | |||||
end | |||||
# @version ? | |||||
def query criteria, all = true | |||||
if criteria.empty? | |||||
return scan criteria, all | |||||
end | |||||
if ! @trie.ready? | |||||
return scan criteria, all | |||||
end | |||||
x = @trie.seek(criteria).uniq.map {|e| get(e)}.filter { |record| matching(record, criteria) }.map { |e| e.transform_keys(&:to_sym) } | |||||
if ! all | |||||
return [x.first] | |||||
end | |||||
return x | |||||
end | |||||
# @version ? | |||||
def find criteria | |||||
query criteria | |||||
end | |||||
# @version ? | |||||
def first criteria | |||||
matches = query criteria, false | |||||
if 1 > matches.length | |||||
return nil | |||||
end | |||||
matches.first | |||||
end | |||||
# @version ? | |||||
def last criteria | |||||
matches = query criteria, false | |||||
if 1 > matches.length | |||||
return nil | |||||
end | |||||
matches.last | |||||
end | |||||
end | |||||
end |
@@ -0,0 +1,5 @@ | |||||
# frozen_string_literal: true | |||||
module Aries | |||||
VERSION = "0.1.0" | |||||
end |
@@ -0,0 +1,11 @@ | |||||
# frozen_string_literal: true | |||||
RSpec.describe Aries do | |||||
it "has a version number" do | |||||
expect(Aries::VERSION).not_to be nil | |||||
end | |||||
it "does something useful" do | |||||
expect(false).to eq(true) | |||||
end | |||||
end |
@@ -0,0 +1,15 @@ | |||||
# frozen_string_literal: true | |||||
require "aries" | |||||
RSpec.configure do |config| | |||||
# Enable flags like --only-failures and --next-failure | |||||
config.example_status_persistence_file_path = ".rspec_status" | |||||
# Disable RSpec exposing methods globally on `Module` and `main` | |||||
config.disable_monkey_patching! | |||||
config.expect_with :rspec do |c| | |||||
c.syntax = :expect | |||||
end | |||||
end |
@@ -0,0 +1,9 @@ | |||||
#!/bin/bash | |||||
# if [ -d "aries" ]; then | |||||
# cd aries && git pull && cd .. | |||||
# else | |||||
# git clone https://github.com/licina-luka/aries | |||||
# fi | |||||
ruby aries/lib/aries.rb |