static-record 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +112 -0
  4. data/Rakefile +22 -0
  5. data/app/assets/javascripts/static_record/application.js +13 -0
  6. data/app/assets/stylesheets/static_record/application.css +15 -0
  7. data/app/controllers/static_record/application_controller.rb +4 -0
  8. data/app/helpers/static_record/application_helper.rb +4 -0
  9. data/app/models/concerns/query_building_concern.rb +103 -0
  10. data/app/models/concerns/sqlite_storing_concern.rb +61 -0
  11. data/app/models/static_record/base.rb +61 -0
  12. data/app/models/static_record/predicates.rb +100 -0
  13. data/app/models/static_record/querying.rb +25 -0
  14. data/app/models/static_record/relation.rb +110 -0
  15. data/app/views/layouts/static_record/application.html.erb +14 -0
  16. data/config/routes.rb +2 -0
  17. data/lib/static_record.rb +5 -0
  18. data/lib/static_record/engine.rb +5 -0
  19. data/lib/static_record/exceptions.rb +5 -0
  20. data/lib/static_record/version.rb +3 -0
  21. data/lib/tasks/static_record_tasks.rake +4 -0
  22. data/spec/models/static_record/base_spec.rb +10 -0
  23. data/spec/models/static_record/querying_spec.rb +8 -0
  24. data/spec/models/static_record/relation_spec.rb +242 -0
  25. data/spec/rails_helper.rb +15 -0
  26. data/spec/spec_helper.rb +18 -0
  27. data/spec/test_app/app/controllers/application_controller.rb +5 -0
  28. data/spec/test_app/app/helpers/application_helper.rb +2 -0
  29. data/spec/test_app/app/models/article.rb +6 -0
  30. data/spec/test_app/app/models/articles/article_four.rb +5 -0
  31. data/spec/test_app/app/models/articles/article_one.rb +5 -0
  32. data/spec/test_app/app/models/articles/article_three.rb +5 -0
  33. data/spec/test_app/app/models/articles/article_two.rb +5 -0
  34. data/spec/test_app/app/models/role.rb +5 -0
  35. data/spec/test_app/app/models/roles/role_one.rb +4 -0
  36. data/spec/test_app/config/application.rb +32 -0
  37. data/spec/test_app/config/boot.rb +5 -0
  38. data/spec/test_app/config/environment.rb +5 -0
  39. data/spec/test_app/config/environments/development.rb +41 -0
  40. data/spec/test_app/config/environments/production.rb +79 -0
  41. data/spec/test_app/config/environments/test.rb +42 -0
  42. data/spec/test_app/config/initializers/assets.rb +11 -0
  43. data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
  45. data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
  46. data/spec/test_app/config/initializers/inflections.rb +16 -0
  47. data/spec/test_app/config/initializers/mime_types.rb +4 -0
  48. data/spec/test_app/config/initializers/session_store.rb +3 -0
  49. data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
  50. data/spec/test_app/config/routes.rb +4 -0
  51. data/spec/test_app/db/schema.rb +16 -0
  52. metadata +199 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a8eb89ed57ec1a0a6fbc6574e1c1614cda528447
4
+ data.tar.gz: ffffe730a97c3953064f7e2f517d840852ecf48e
5
+ SHA512:
6
+ metadata.gz: 0d93d7d7ebcc4b347ccad1863e4ce43d3a51688022b7e4b2292820292a3ff0eafa49f1713c1f12058da444517d6c1605ff7d0db57da3401bd630e1bab0efb731
7
+ data.tar.gz: 40483219ffbe67bd87118e3d9175b5be8c7b681a6f8909b9188f1efc887f5fedc05c6a7594bdde42714fb5140d67b6be56dc099d50cfdb8bd5116330f7901b53
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Hugo Chevalier
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,112 @@
1
+ = StaticRecord
2
+
3
+ StaticRecord allows you to perform ActiveRecord-like queries over ruby files.
4
+
5
+ Those files act as immutable database records that only developers can alter.
6
+
7
+ You can use it when you need several files inheriting a base class.
8
+
9
+ == Installation
10
+
11
+ Add this to your Gemfile:
12
+
13
+ gem 'static-record'
14
+
15
+ and run the bundle install command.
16
+
17
+ == Getting Started
18
+
19
+ === Base class
20
+
21
+ Create your base class inheriting from StaticRecord::Base.
22
+
23
+ class Article < StaticRecord::Base
24
+ # Declare in which SQLite3 file "articles" will be store (db/static_<table>.sqlite3)
25
+ table :articles
26
+
27
+ # Declare at which path "article" files can be found
28
+ path Rails.root.join('app', 'models', 'articles', '**', '*.rb')
29
+
30
+ # Optionnal, declare which column can be used as the primary key (must be unique)
31
+ # .find will only be availble if a primary key is defined
32
+ primary_key :name
33
+
34
+ # Specify which "article" attributes can be queried over
35
+ columns [:name, :author, :rank]
36
+ end
37
+
38
+ At each application startup, an SQLite3 database will be created to store this class' children.
39
+
40
+ === Child class
41
+
42
+ Create has many child class as you want.
43
+
44
+ class ArticleOne < Article
45
+ # Define the attributes that will be available for your StaticRecord queries
46
+ attribute :name, 'Article One'
47
+ attribute :author, 'The author'
48
+ attribute :rank, '2'
49
+
50
+ # Your class can be used as any other Ruby class
51
+ def initialize
52
+ @an_instance_variable
53
+ super
54
+ end
55
+
56
+ def my_instance_method
57
+ end
58
+
59
+ def self.my_class_method
60
+ end
61
+ end
62
+
63
+ === Queries
64
+
65
+ In your code, you can perform queries like this one:
66
+
67
+ Article.where(name: 'Article Two').or.where.not(author: ['Author 1', 'Author 2']).limit(2).offset(3)
68
+
69
+ I tried to implement as many SQL functions wrappers that ActiveRecord provides as I could.
70
+
71
+ There is still a lot of work before everything is available, but I chosed to release the 1.0.0.pre nevertheless.
72
+
73
+ Here is a full list:
74
+ * where
75
+ * supports Hash -> where(name: 'Name', author: 'Author')
76
+ * supports String -> where("name = 'Name'") or where("name = ?", 'Name') or where("name = :name", name: 'Name')
77
+ * find (requires a primary key has been set)
78
+ * find_by
79
+ * not
80
+ * or
81
+ * all
82
+ * take
83
+ * first
84
+ * last
85
+ * limit
86
+ * offset
87
+ * order
88
+
89
+ == IDs
90
+
91
+ Records are being assigned an ID in the SQLite3 database when inserted.
92
+
93
+ As the database is recreated at each application startup and IDs depend on the insertion order, I advise you to rely on another column if you want to hardcode a specific record somewhere in your app.
94
+
95
+ == Questions?
96
+
97
+ If you have any question or doubt regarding StaticRecord which you cannot find the solution to in the documentation, you can send me an email. I'll try to answer in less than 24 hours.
98
+
99
+ == Bugs?
100
+
101
+ If you find a bug please add an issue on GitHub or fork the project and send a pull request.
102
+
103
+ == Development
104
+
105
+ As StaticRecord is in active development and a full list of feature is already scheduled, I won't be accepting any feature-oriented pull requests before the 1.0.0 release (hopefully before mi-January).
106
+
107
+ Here is what will be available soon:
108
+ - Better documentation
109
+ - Typed attributes
110
+ - Foreign keys
111
+ - Joins
112
+ - Generators
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'StaticRecord'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('../spec/test_app/Rakefile', __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module StaticRecord
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module StaticRecord
2
+ module ApplicationHelper # :nodoc:
3
+ end
4
+ end
@@ -0,0 +1,103 @@
1
+ # Helps building SQL queries
2
+ module QueryBuildingConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ def build_query
6
+ sql = sql_select_from
7
+ sql += sql_where unless @where_clauses.empty?
8
+ sql += sql_order unless @order_by.empty?
9
+ sql += sql_limit_offset if @sql_limit
10
+ sql
11
+ end
12
+
13
+ private
14
+
15
+ def sql_select_from
16
+ sql = "SELECT #{@columns} FROM #{@table}"
17
+ end
18
+
19
+ def sql_where
20
+ " WHERE #{where_clause_builder}"
21
+ end
22
+
23
+ def sql_order
24
+ ord_sql = ''
25
+ @order_by.each do |ord|
26
+ ord_sql += ord_sql.empty? ? ' ORDER BY' : ', '
27
+ case ord.class.name
28
+ when Hash.name
29
+ ord_sql += ord.map { |k, v| " #{@table}.#{k.to_s} #{v.to_s.upcase}" }.join(',')
30
+ when Array.name
31
+ ord_sql += ord.map { |sym| " #{@table}.#{sym.to_s} ASC" }.join(',')
32
+ when Symbol.name
33
+ ord_sql += " #{@table}.#{ord.to_s} ASC"
34
+ when String.name
35
+ ord_sql += " #{ord}"
36
+ end
37
+ end
38
+ ord_sql
39
+ end
40
+
41
+ def sql_limit_offset
42
+ sql = " LIMIT #{@sql_limit}"
43
+ sql += " OFFSET #{@sql_offset}" if @sql_offset
44
+ sql
45
+ end
46
+
47
+ def where_clause_builder
48
+ params = []
49
+ @where_clauses.map do |clause|
50
+ subquery = clause[:q]
51
+ if subquery.is_a?(Hash)
52
+ params << where_clause_from_hash(clause, subquery)
53
+ elsif subquery.is_a?(String)
54
+ params << where_clause_from_string(clause, subquery)
55
+ end
56
+
57
+ if params.size > 1
58
+ joint = clause[:chain] == :or ? 'OR' : 'AND'
59
+ params = [params.join(" #{joint} ")]
60
+ end
61
+ end
62
+
63
+ params.first
64
+ end
65
+
66
+ def where_clause_from_hash(clause, subquery)
67
+ parts = subquery.keys.map do |key|
68
+ value = subquery[key]
69
+ part = ''
70
+ if value.is_a?(Array)
71
+ # ex: where(name: ['John', 'Jack'])
72
+ # use IN operator
73
+ value.map! { |v| v =~ /^\d+$/ ? v : "\"#{v}\"" }
74
+ inverse = 'NOT ' if clause[:operator] == :not_eq
75
+ part = "#{key.to_s} #{inverse}IN (#{value.join(',')})"
76
+ else
77
+ # ex: where(name: 'John')
78
+ # use = operator
79
+ inverse = '!' if clause[:operator] == :not_eq
80
+ part = "#{key.to_s} #{inverse}= '#{value}'"
81
+ end
82
+ end
83
+ parts.join(' AND ')
84
+ end
85
+
86
+ def where_clause_from_string(clause, subquery)
87
+ final_string = subquery
88
+ if clause[:parameters].is_a?(Array)
89
+ # Anon parameters
90
+ # ex: where("name = ? OR name = ?", 'John', 'Jack')
91
+ clause[:parameters].each do |param|
92
+ final_string.sub!(/\?/, "\"#{param}\"")
93
+ end
94
+ elsif clause[:parameters].is_a?(Hash)
95
+ # Named parameters (placeholder condition)
96
+ # ex: where("name = :one OR name = :two", one: 'John', two: 'Smith')
97
+ clause[:parameters].each do |key, value|
98
+ final_string.sub!(":#{key}", "\"#{value}\"")
99
+ end
100
+ end
101
+ final_string
102
+ end
103
+ end
@@ -0,0 +1,61 @@
1
+ # Reads ruby files whose path matches path pattern and store them
2
+ # as records in an SQLite3 database
3
+ module SqliteStoringConcern
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods # :nodoc:
7
+ def create_store
8
+ columns = class_variable_get(:@@_columns)
9
+ begin
10
+ dbname = Rails.root.join('db', "static_#{store}.sqlite3").to_s
11
+ SQLite3::Database.new(dbname)
12
+ db = SQLite3::Database.open(dbname)
13
+ db.execute("DROP TABLE IF EXISTS #{store}")
14
+ create_table(db, columns)
15
+ load_records.each_with_index do |record, index|
16
+ insert_into_database(db, record, index, columns)
17
+ end
18
+ rescue SQLite3::Exception => e
19
+ puts 'Exception occurred', e
20
+ ensure
21
+ db.close if db
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def create_table(db, columns)
28
+ cols = columns.map { |c| c.to_s + ' TEXT' }.join(', ')
29
+ sql = "CREATE TABLE #{store}(id INTEGER PRIMARY KEY, klass TEXT, #{cols})"
30
+ db.execute(sql)
31
+ end
32
+
33
+ def insert_into_database(db, record, index, columns)
34
+ attrs = record.constantize.new.attributes
35
+ sqlized = [index.to_s, "'#{record}'"] # id, klass
36
+ sqlized += columns.map { |c| "'#{attrs[c]}'" } # model's attributes
37
+ db.execute("INSERT INTO #{store} VALUES(#{sqlized.join(', ')})")
38
+ end
39
+
40
+ def load_records
41
+ records = []
42
+ Dir.glob(path_pattern) do |filepath|
43
+ klass = get_class_from_file(filepath)
44
+ if klass
45
+ require filepath
46
+ records << klass
47
+ end
48
+ end
49
+ records
50
+ end
51
+
52
+ def get_class_from_file(filepath)
53
+ klass = nil
54
+ File.open(filepath) do |file|
55
+ match = file.grep(/class\s+([a-zA-Z0-9_]+)/)
56
+ klass = match.first.chomp.gsub(/class\s+/, '').split(' ')[0] if match
57
+ end
58
+ klass
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ module StaticRecord
2
+ # Class that immutable model instances can inherit from
3
+ class Base
4
+ include StaticRecord::Querying
5
+ include StaticRecord::SqliteStoringConcern
6
+
7
+ RESERVED_ATTRIBUTES = [
8
+ :@@_columns,
9
+ :@@_primary_key,
10
+ :@@_path_pattern,
11
+ :@@_store
12
+ ].freeze
13
+
14
+ def self.attribute(name, value)
15
+ err = RESERVED_ATTRIBUTES.include?("@@#{name}".to_sym)
16
+ raise StaticRecord::ReservedAttributeName, "#{name} is a reserved name" if err
17
+ class_variable_set("@@#{name}", value)
18
+ end
19
+
20
+ def self.primary_key(name)
21
+ err = RESERVED_ATTRIBUTES.include?("@@#{name}".to_sym)
22
+ raise StaticRecord::ReservedAttributeName, "#{name} is a reserved name" if err
23
+ class_variable_set('@@_primary_key', name)
24
+ end
25
+
26
+ def self.pkey
27
+ class_variable_defined?(:@@_primary_key) ? class_variable_get('@@_primary_key') : nil
28
+ end
29
+
30
+ def self.table(store)
31
+ class_variable_set('@@_store', store.to_s)
32
+ end
33
+
34
+ def self.store
35
+ class_variable_get('@@_store')
36
+ end
37
+
38
+ def self.path(path)
39
+ class_variable_set('@@_path_pattern', path)
40
+ end
41
+
42
+ def self.path_pattern
43
+ class_variable_get('@@_path_pattern')
44
+ end
45
+
46
+ def attributes
47
+ attrs = {}
48
+ klass = self.class
49
+ klass.class_variables.each do |var|
50
+ next if RESERVED_ATTRIBUTES.include?(var)
51
+ attrs[var.to_s.sub(/@@/, '').to_sym] = klass.class_variable_get(var)
52
+ end
53
+ attrs
54
+ end
55
+
56
+ def self.columns(cols)
57
+ class_variable_set('@@_columns', cols)
58
+ create_store
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,100 @@
1
+ module StaticRecord
2
+ # Contains ActiveRecord-like query predicates
3
+ module Predicates
4
+ private
5
+
6
+ def where(query = nil, *params)
7
+ params = [params] unless params.is_a?(Array) || params.is_a?(Hash)
8
+ params = params.first if params.size == 1 && params[0].is_a?(Hash)
9
+ add_subclause({ q: query }, params) if query
10
+ self
11
+ end
12
+
13
+ def not(query, *params)
14
+ params = [params] unless params.is_a?(Array) || params.is_a?(Hash)
15
+ params = params.first if params.size == 1 && params[0].is_a?(Hash)
16
+ add_subclause({ q: query, operator: :not_eq }, params)
17
+ self
18
+ end
19
+
20
+ def or
21
+ @chain = :or
22
+ self
23
+ end
24
+
25
+ def all
26
+ to_a
27
+ end
28
+
29
+ def find(value)
30
+ raise StaticRecord::NoPrimaryKey, 'No primary key have been set' if @primary_key.nil?
31
+ @result_type = :record unless value.is_a?(Array)
32
+ add_subclause(q: { :"#{@primary_key}" => value })
33
+ @sql_limit = 1 if @result_type == :record
34
+
35
+ res = to_a
36
+ return res if @only_sql
37
+
38
+ raise StaticRecord::RecordNotFound, "Couldn't find all #{@store.singularize.capitalize} with '#{@primary_key.to_s}' IN #{value}" if @result_type == :array && res.size != value.size
39
+ raise StaticRecord::RecordNotFound, "Couldn't find #{@store.singularize.capitalize} with '#{@primary_key.to_s}'=#{value}" if @result_type == :record && res.nil?
40
+
41
+ res
42
+ end
43
+
44
+ def find_by(query)
45
+ add_subclause(q: query)
46
+ take(1)
47
+ end
48
+
49
+ def take(amount = 1)
50
+ @sql_limit = amount
51
+ @result_type = :record if amount == 1
52
+ to_a
53
+ end
54
+
55
+ def first(amount = 1)
56
+ @order_by << { :"#{@primary_key}" => :asc } if @order_by.empty?
57
+ take(amount)
58
+ end
59
+
60
+ def last(amount = 1)
61
+ @order_by << { :"#{@primary_key}" => :desc } if @order_by.empty?
62
+ res = take(amount)
63
+ res.reverse! if res.is_a?(Array)
64
+ res
65
+ end
66
+
67
+ def limit(amount)
68
+ @sql_limit = amount
69
+ self
70
+ end
71
+
72
+ def offset(amount)
73
+ @sql_offset = amount
74
+ self
75
+ end
76
+
77
+ def order(ord)
78
+ @order_by << ord
79
+ self
80
+ end
81
+
82
+ def count
83
+ @columns = 'COUNT(*)'
84
+ exec_request(:integer)
85
+ end
86
+
87
+ def to_sql
88
+ build_query
89
+ end
90
+
91
+ def see_sql_of
92
+ @only_sql = true
93
+ self
94
+ end
95
+
96
+ def to_a
97
+ exec_request
98
+ end
99
+ end
100
+ end