menilite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8bd4f42ddb852546849eaf34e1ad3247d19b7365
4
+ data.tar.gz: 572240d87bfb87dc8c75747a427e3e6e4ed08afe
5
+ SHA512:
6
+ metadata.gz: 6c3cfffbe205a5c878560d0fa28aee2d357ce4351b277c8f134dcdf6c6257d0a3aae86f0eb500eb37e1ee54cfece83c3d66a4c211179bde49f596afaff4fd30b
7
+ data.tar.gz: 157c094db9e46ecf109730510cdcbfa8c82d7592d99d0f37a47708c20fa78d3b096349a331f44e0566c64de2bb731db383fa67e98a8304b01b2b175810f8c68a
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in menilite.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Menilite
2
+
3
+ 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/menilite`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'menilite'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install menilite
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/menilite.
36
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "menilite"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/menilite.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "menilite/version"
2
+
3
+ if RUBY_ENGINE == "opal"
4
+ require 'menilite/model'
5
+ require 'menilite/controller'
6
+ require 'menilite/client/store'
7
+ else
8
+ require 'opal'
9
+ require 'menilite/model'
10
+ require 'menilite/controller'
11
+ require 'menilite/router'
12
+ require 'menilite/server/activerecord_store'
13
+
14
+ Opal.append_path File.expand_path('../', __FILE__).untaint
15
+ Opal.append_path File.expand_path('../../vendor', __FILE__).untaint
16
+ end
@@ -0,0 +1,84 @@
1
+ require 'browser/http'
2
+
3
+ module Menilite
4
+ class Store
5
+ def initialize
6
+ @tables = {}
7
+ end
8
+
9
+ def self.instance
10
+ @instance ||= Store.new
11
+ end
12
+
13
+ def register(model_class)
14
+ @tables[model_class] = {}
15
+ end
16
+
17
+ def [](model_class)
18
+ @tables[model_class]
19
+ end
20
+
21
+ def find(model_class, id)
22
+ return self[model_class][id] if self[model_class][id]
23
+
24
+ res = Browser::HTTP.get!("api/#{model_class.to_s}/#{id}")
25
+ model = model_class.new(res.json)
26
+ @tables[model_class][id] = model
27
+ end
28
+
29
+ def save(model)
30
+ is_array = model.is_a?(Array)
31
+ models = is_array ? model : [ model ]
32
+ model_class = models.first.class
33
+ table = @tables[model_class]
34
+ Browser::HTTP.post("api/#{model_class.to_s}", models.to_json) do
35
+ on :success do |res|
36
+ results = res.json.map do |value|
37
+ if table.has_key?(value[:id])
38
+ table[value[:id]].update(value)
39
+ table[value[:id]]
40
+ else
41
+ table[value[:id]] = model_class.new(value)
42
+ end
43
+ end
44
+
45
+ yield(is_array ? results : results.first) if block_given?
46
+ end
47
+
48
+ on :failure do |res|
49
+ puts ">> Error: #{res.error}"
50
+ puts ">>>> save: #{model.inspect}"
51
+ end
52
+ end
53
+ end
54
+
55
+ def fetch(model_class, filter: nil, order: nil, &block)
56
+ tables = @tables
57
+ params = filter && (?? + filter.map {|k,v| "#{k}=#{v}" }.join(?&))
58
+ params = (params ? params + ?& : ??) + "order=#{[order].flatten.join(?,)}" if order
59
+ Browser::HTTP.get("api/#{model_class}#{params}") do
60
+ on :success do |res|
61
+ tables[model_class] = res.json.map {|value| [value[:id], model_class.new(value)] }.to_h
62
+ yield tables[model_class].values if block_given?
63
+ end
64
+
65
+ on :failure do |res|
66
+ puts ">> Error: #{res.error}"
67
+ puts ">>>> save: #{model.inspect}"
68
+ end
69
+ end
70
+ end
71
+
72
+ def delete(mdoel_class)
73
+ @tables[model_class] = {}
74
+ end
75
+
76
+ def max(model_class, field_name)
77
+ res = Browser::HTTP.get!("api/#{model_class}?order=#{field_name}")
78
+ if res.json.last
79
+ model = model_class.new(res.json.last)
80
+ model.fields[field_name]
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,66 @@
1
+ if RUBY_ENGINE == 'opal'
2
+ require 'browser/http'
3
+ require 'opal-parser'
4
+ end
5
+
6
+ module Menilite
7
+ class Controller
8
+ unless RUBY_ENGINE == 'opal'
9
+ def initialize(session, settings)
10
+ @settings = settings
11
+ @session = session
12
+ end
13
+ end
14
+
15
+ def session
16
+ @session
17
+ end
18
+
19
+ def settings
20
+ @settings
21
+ end
22
+
23
+ class << self
24
+ def action_info
25
+ @action_info ||= {}
26
+ end
27
+
28
+ def before_action_handlers
29
+ @before_action_handlers ||= []
30
+ end
31
+
32
+ ActionInfo = Struct.new(:name, :args, :options)
33
+
34
+ def action(name, options = {}, &block)
35
+ action_info[name.to_s] = ActionInfo.new(name, block.parameters, options)
36
+ if RUBY_ENGINE == 'opal'
37
+ method = Proc.new do |*args, &callback| # todo: should adopt keyword parameters
38
+ action_url = self.respond_to?(:namespace) ? "api/#{self.class.namespace}/#{name}" : "api/#{name}"
39
+ post_data = {}
40
+ post_data[:args] = args
41
+ Browser::HTTP.post(action_url, post_data.to_json) do
42
+ on :success do |res|
43
+ callback.call(:success, res) if callback
44
+ end
45
+
46
+ on :failure do |res|
47
+ callback.call(:failure, res) if callback
48
+ end
49
+ end
50
+ end
51
+ self.instance_eval do
52
+ define_singleton_method(name, method)
53
+ end
54
+ else
55
+ self.instance_eval do
56
+ define_method(name, block)
57
+ end
58
+ end
59
+ end
60
+
61
+ def before_action(options = {}, &block)
62
+ before_action_handlers << { proc: block, options: options }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,282 @@
1
+ require 'securerandom'
2
+ if RUBY_ENGINE == 'opal'
3
+ require 'browser/http'
4
+ require 'opal-parser'
5
+ end
6
+
7
+ class String
8
+ def camel_case
9
+ return self if self !~ /_/ && self =~ /[A-Z]+.*/
10
+ split('_').map{|e| e.capitalize}.join
11
+ end
12
+ end
13
+
14
+ module Menilite
15
+ class Model
16
+ attr_reader :fields
17
+
18
+ def initialize(fields = {})
19
+ self.class.init
20
+
21
+ if RUBY_ENGINE == 'opal'
22
+ fields = fields.clone
23
+ else
24
+ fields = fields.map{|k,v| [k.to_sym, v] }.to_h
25
+ end
26
+
27
+ defaults = self.class.field_info.map{|k, d| [d.name, d.params[:default]] if d.params.has_key?(:default) }.compact.to_h
28
+ @guid = fields.delete(:id) || SecureRandom.uuid
29
+ @fields = defaults.merge(fields)
30
+ @listeners = {}
31
+ end
32
+
33
+ def id
34
+ @guid
35
+ end
36
+
37
+ def save(&block)
38
+ self.class.store.save(self, &block)
39
+ self
40
+ end
41
+
42
+ def update(data)
43
+ case data
44
+ when self.class
45
+ @fields.merge(data.fields)
46
+ when Hash
47
+ @fields.merge(data)
48
+ when String
49
+ @fields.merge(JSON.parse(json))
50
+ end
51
+ end
52
+
53
+ def update!(data, &block)
54
+ self.update(data)
55
+ self.save(&block)
56
+ end
57
+
58
+ def on(event, *field_names, &block)
59
+ field_names.each {|file_name| set_listener(event, file_name, &block) }
60
+ end
61
+
62
+ def handle_event(event, field_name, value)
63
+ get_listeners(event, field_name).each do |listener|
64
+ listener.call(value)
65
+ end
66
+ value
67
+ end
68
+
69
+ class << self
70
+ def init
71
+ if Model.subclasses.has_key?(self) && !Model.subclasses[self]
72
+ store.register(self)
73
+ Model.subclasses[self] = true
74
+ end
75
+ end
76
+
77
+ def field_info
78
+ @field_info ||= {}
79
+ end
80
+
81
+ def action_info
82
+ @action_info ||= {}
83
+ end
84
+
85
+ def save(collection, &block)
86
+ self.init
87
+ self.store.save(collection, &block)
88
+ end
89
+
90
+ def create(fields={}, &block)
91
+ self.init
92
+ self.new(fields).save(&block)
93
+ end
94
+
95
+ def delete_all
96
+ self.init
97
+ store.delete(self)
98
+ end
99
+
100
+ def fetch(filter: nil, order: nil)
101
+ self.init
102
+ filter = filter.map{|k, v| type_convert(k, v) }.to_h if filter
103
+ store.fetch(self, filter: filter, order: order) do |list|
104
+ yield list if block_given?
105
+ list
106
+ end
107
+ end
108
+
109
+ def type_convert(key, value)
110
+ field_info = self.field_info[key.to_s] || self.field_info[key.to_s.sub(/_id\z/,'')]
111
+ raise "no such field #{key} in #{self}" unless field_info
112
+ converted = case field_info.type
113
+ when :boolean
114
+ value.is_a?(String) ? (value == 'true' ? true : false) : value
115
+ else
116
+ value
117
+ end
118
+ [key, converted]
119
+ end
120
+
121
+ def store
122
+ Store.instance
123
+ end
124
+
125
+ def subclasses
126
+ @subclasses ||= {}
127
+ end
128
+
129
+ def inherited(child)
130
+ subclasses[child] = false
131
+ end
132
+
133
+ FieldInfo = Struct.new(:name, :type, :params)
134
+
135
+ def field(name, type = :string, params = {})
136
+ params.merge!(client: true, server: true)
137
+
138
+ if RUBY_ENGINE == 'opal'
139
+ return unless params[:client]
140
+ else
141
+ return unless params[:server]
142
+ end
143
+
144
+ field_info[name.to_s] = FieldInfo.new(name, type, params)
145
+
146
+ self.instance_eval do
147
+ if type == :reference
148
+ field_name = "#{name}_id"
149
+
150
+ define_method(name) do
151
+ id = @fields[field_name.to_sym]
152
+ model_class = Object.const_get(name.to_s.camel_case)
153
+ model_class[id]
154
+ end
155
+
156
+ define_method(name.to_s + "=") do |value|
157
+ @fields[field_name.to_sym] = value.id
158
+ end
159
+ else
160
+ field_name = name.to_s
161
+ end
162
+
163
+ define_method(field_name) do
164
+ @fields[field_name.to_sym]
165
+ end
166
+
167
+ define_method(field_name + "=") do |value|
168
+ unless type_validator(type).call(value, name)
169
+ raise 'type error'
170
+ end
171
+ @fields[field_name.to_sym] = value
172
+ handle_event(:change, field_name.to_sym, value)
173
+ end
174
+ end
175
+ end
176
+
177
+ ActionInfo = Struct.new(:name, :args, :options)
178
+
179
+ def action(name, options = {}, &block)
180
+ action_info[name.to_s] = ActionInfo.new(name, block.parameters, options)
181
+ if RUBY_ENGINE == 'opal'
182
+ method = Proc.new do |model, *args, &callback| # todo: should adopt keyword parameters
183
+ action_url = options[:on_create] || options[:class] ? "api/#{self}/#{name}" : "api/#{self}/#{model.id}/#{name}"
184
+ post_data = {}
185
+ post_data[:model] = model.to_h if options[:on_create]
186
+ post_data[:args] = args
187
+ Browser::HTTP.post(action_url, post_data.to_json) do
188
+ on :success do |res|
189
+ callback.call(:success, res) if callback
190
+ end
191
+
192
+ on :failure do |res|
193
+ callback.call(:failure, res) if callback
194
+ end
195
+ end
196
+ end
197
+ self.instance_eval do
198
+ if options[:class]
199
+ define_singleton_method(name) {|*args, &callback| method.call(self, *args, &callback) }
200
+ else
201
+ define_method(name) {|*args, &callback| method.call(self, *args, &callback) }
202
+ end
203
+ end
204
+ else
205
+ self.instance_eval do
206
+ if options[:class]
207
+ define_singleton_method(name, block)
208
+ else
209
+ define_method(name, block)
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def find(id)
216
+ self.init
217
+
218
+ case id
219
+ when String
220
+ self[id]
221
+ when Hash
222
+ self.fetch(filter:id).first
223
+ end
224
+
225
+ end
226
+
227
+ def [](id)
228
+ self.init
229
+ store.find(self, id)
230
+ end
231
+
232
+ def max(field_name)
233
+ self.init
234
+ store.max(self, field_name)
235
+ end
236
+ end
237
+
238
+ def type_validator(type)
239
+ case type
240
+ when :string
241
+ -> (value, name) { value.is_a? String }
242
+ when :int
243
+ -> (value, name) { value.is_a? Integer }
244
+ when :boolean
245
+ -> (value, name) { value == true || value == false }
246
+ when :date
247
+ -> (value, name) { value.is_a? Date }
248
+ when :time
249
+ -> (value, name) { value.is_a? Time }
250
+ when :reference
251
+ -> (value, name) { valiedate_reference(value, name) }
252
+ end
253
+ end
254
+
255
+ def valiedate_reference(value, name)
256
+ return false unless value.is_a? String
257
+
258
+ model_class = Object.const_get(name.camel_case)
259
+ not model_class[value].nil?
260
+ end
261
+
262
+ def to_h
263
+ @fields.merge(id: @guid)
264
+ end
265
+
266
+ def to_json(arg)
267
+ @fields.merge(id: @guid).to_json
268
+ end
269
+
270
+ private
271
+
272
+ def get_listeners(event, field_name)
273
+ @listeners[event].try {|l1| l1[field_name] || [] } || []
274
+ end
275
+
276
+ def set_listener(event, field_name, &block)
277
+ @listeners[event] ||= {}
278
+ @listeners[event][field_name] ||= []
279
+ @listeners[event][field_name] << block
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,104 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/json'
3
+ require 'json'
4
+
5
+ class Class
6
+ def subclass_of?(klass)
7
+ raise ArgumentError.new unless klass.is_a?(Class)
8
+
9
+ if self == klass
10
+ true
11
+ else
12
+ if self.superclass
13
+ self.superclass.subclass_of?(klass)
14
+ else
15
+ false
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ module Menilite
22
+ class Router
23
+ def initialize(*classes)
24
+ @classes = classes
25
+ end
26
+
27
+ def before_action_handlers(klass, action)
28
+ @handlers ||= @classes.select{|c| c.subclass_of?(Menilite::Controller) }.map{|c| c.before_action_handlers }.flatten
29
+ @handlers.reject do |c|
30
+ [c[:options][:expect]].flatten.any? do |expect|
31
+ (classname, _, name) = expect.to_s.partition(?#)
32
+ (classname == klass.name) && (name.empty? || name == action.to_s)
33
+ end
34
+ end
35
+ end
36
+
37
+ def routes(settings)
38
+ classes = @classes
39
+ router = self
40
+ Sinatra.new do
41
+ enable :sessions
42
+
43
+ classes.each do |klass|
44
+ case
45
+ when klass.subclass_of?(Menilite::Model)
46
+ klass.init
47
+ resource_name = klass.name
48
+ get "/#{resource_name}" do
49
+ router.before_action_handlers(klass, 'index').each {|h| self.instance_eval(&h[:proc]) }
50
+ order = params.delete('order')&.split(?,)
51
+ data = klass.fetch(filter: params, order: order)
52
+ json data.map(&:to_h)
53
+ end
54
+
55
+ get "/#{resource_name}/:id" do
56
+ router.before_action_handlers(klass, 'get').each {|h| self.instance_eval(&h[:proc]) }
57
+ json klass[params[:id]].to_h
58
+ end
59
+
60
+ post "/#{resource_name}" do
61
+ router.before_action_handlers(klass, 'post').each {|h| self.instance_eval(&h[:proc]) }
62
+ data = JSON.parse(request.body.read)
63
+ results = data.map do |model|
64
+ instance = klass.new model.map{|key, value| [key.to_sym, value] }.to_h
65
+ instance.save
66
+ instance
67
+ end
68
+
69
+ json results.map(&:to_h)
70
+ end
71
+
72
+ klass.action_info.each do |name, action|
73
+ path = action.options[:on_create] || action.options[:class] ? "/#{resource_name}/#{action.name}" : "/#{resource_name}/#{action.name}/:id"
74
+
75
+ post path do
76
+ router.before_action_handlers(klass, action.name).each {|h| self.instance_eval(&h[:proc]) }
77
+ data = JSON.parse(request.body.read)
78
+ result = if action.options[:on_create]
79
+ klass.new(data["model"]).send(action.name, *data["args"])
80
+ elsif action.options[:class]
81
+ klass.send(action.name, *data["args"])
82
+ else
83
+ klass[params[:id]].send(action.name, *data["args"])
84
+ end
85
+ json result
86
+ end
87
+ end
88
+ when klass.subclass_of?(Menilite::Controller)
89
+ klass.action_info.each do |name, action|
90
+ path = klass.respond_to?(:namespace) ? "/#{klass.namespace}/#{action.name}" : "/#{action.name}"
91
+ post path do
92
+ router.before_action_handlers(klass, action.name).each {|h| self.instance_eval(&h[:proc]) }
93
+ data = JSON.parse(request.body.read)
94
+ controller = klass.new(session, settings)
95
+ result = controller.send(action.name, *data["args"])
96
+ json result
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,116 @@
1
+ require 'sinatra/activerecord'
2
+
3
+ module Menilite
4
+ module ActiveRecord
5
+ def self.create_model(model_class)
6
+ klass = Class.new(::ActiveRecord::Base) do
7
+ model_class.field_info.select{|name, field| field.type == :reference }.each do |name, field|
8
+ belongs_to field.name, primary_key: 'guid', foreign_key: name + '_guid', class_name: name.capitalize
9
+ #klass.instance_eval { define_method(name + '_id') { send(name + '_guid') } }
10
+ end
11
+ end
12
+ self.const_set(model_class.to_s, klass)
13
+ end
14
+ end
15
+
16
+ class Store
17
+ def initialize
18
+ @tables = {}
19
+ @armodels = {}
20
+ end
21
+
22
+ def self.instance
23
+ @instance ||= Menilite::Store.new
24
+ end
25
+
26
+ def register(model_class)
27
+ @tables[model_class] = {}
28
+ @armodels[model_class] = Menilite::ActiveRecord.create_model(model_class)
29
+ end
30
+
31
+ def armodel(model_class)
32
+ @armodels[model_class]
33
+ end
34
+
35
+ def find(model_class, id)
36
+ ar_obj = @armodels[model_class].find_by(guid: id)
37
+ to_model(ar_obj, model_class) if ar_obj
38
+ end
39
+
40
+ def save(model)
41
+ is_array = model.is_a?(Array)
42
+ models = is_array ? model : [ model ]
43
+ model_class = models.first.class
44
+
45
+ models.each do |m|
46
+ ar_obj = @armodels[model_class].find_by(guid: m.id)
47
+ if ar_obj
48
+ ar_obj.update!(attributes(m))
49
+ else
50
+ @armodels[model_class].create!(attributes(m))
51
+ end
52
+ end
53
+
54
+ yield model if block_given?
55
+ end
56
+
57
+ def fetch(model_class, filter: nil, order: nil)
58
+ assoc = @armodels[model_class].all
59
+
60
+ assoc = assoc.where(filter_condition(model_class, filter)) if filter
61
+ assoc = assoc.order([order].flatten.map(&:to_sym)) if order
62
+
63
+ yield assoc.map {|ar| to_model(ar, model_class) } || [] if block_given?
64
+ end
65
+
66
+ def delete(model_class)
67
+ @armodels[model_class].delete_all
68
+ end
69
+
70
+ def max(model_class, field_name)
71
+ fetch(model_class).max(field_name.to_sym)
72
+ end
73
+
74
+ def to_model(ar_obj, model_class)
75
+ model_class.new(fields(ar_obj, model_class))
76
+ end
77
+
78
+ private
79
+
80
+ def [](model_class)
81
+ @tables[model_class]
82
+ end
83
+
84
+ def filter_condition(model_class, filter)
85
+ references = model_class.field_info.values.select{|i| i.type == :reference}
86
+ filter.clone.tap do |hash|
87
+ references.each do |r|
88
+ hash["#{r.name}_guid".to_sym] = hash.delete("#{r.name}_id".to_sym) if hash.has_key?("#{r.name}_id".to_sym)
89
+ end
90
+
91
+ hash[:guid] = hash.delete(:id) if hash.has_key?(:id)
92
+ end
93
+ end
94
+
95
+ def attributes(model)
96
+ references = model.class.field_info.values.select{|i| i.type == :reference}
97
+ model.to_h.tap do |hash|
98
+ references.each do |r|
99
+ hash["#{r.name}_guid".to_sym] = hash.delete("#{r.name}_id".to_sym)
100
+ end
101
+
102
+ hash[:guid] = hash.delete(:id)
103
+ end
104
+ end
105
+
106
+ def fields(ar_obj, model_class)
107
+ references = model_class.field_info.values.select{|i| i.type == :reference}
108
+ ar_obj.attributes.tap do |hash|
109
+ references.each do |r|
110
+ hash["#{r.name}_id"] = hash.delete("#{r.name}_guid")
111
+ end
112
+ hash["id"] = hash.delete("guid")
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,73 @@
1
+ require 'json'
2
+
3
+ module Menilite
4
+ class Store
5
+ DEFAULT_DB_DIR = './.store'
6
+
7
+ def initialize(db_dir = DEFAULT_DB_DIR)
8
+ @tables = {}
9
+ @db_dir = db_dir
10
+ Dir.mkdir(@db_dir) unless Dir.exist?(@db_dir)
11
+ end
12
+
13
+ def self.instance
14
+ @instance ||= Store.new
15
+ end
16
+
17
+ def register(model_class)
18
+ @tables[model_class] = {}
19
+ end
20
+
21
+ def [](model_class)
22
+ @tables[model_class]
23
+ end
24
+
25
+ def find(model_class, id)
26
+ return self[model_class][id] if self[model_class][id]
27
+
28
+ fetch(model_class)
29
+ @tables[model_class][id]
30
+ end
31
+
32
+ def save(model)
33
+ is_array = model.is_a?(Array)
34
+ models = is_array ? model : [ model ]
35
+ model_class = models.first.class
36
+
37
+ models.each do |m|
38
+ @tables[model_class][m.id] = m
39
+ end
40
+
41
+ File.open(filename(model_class), "w") do |file|
42
+ file.write @tables[model_class].values.to_json
43
+ end
44
+
45
+ yield model if block_given?
46
+ end
47
+
48
+ def fetch(model_class, filter: nil, order: nil)
49
+ File.open filename(model_class) do |file|
50
+ records = JSON.parse(file.read)
51
+ records.select! {|r| filter.all? {|k,v| r[k.to_s] == v } } if filter
52
+ records.sort_by!{|r| [order].flatten.map{|o| r[o.to_s] } } if order
53
+ @tables[model_class] = records.map {|m| [m["id"], model_class.new(m)] }.to_h
54
+ end
55
+
56
+ yield @tables[model_class].values || [] if block_given?
57
+ end
58
+
59
+ def delete(model_class)
60
+ filename = self.filename(model_class)
61
+ File.delete(filename) if File.exist?(filename)
62
+ end
63
+
64
+ def max(model_class, field_name)
65
+ fetch(model_class)
66
+ @tables[model_class].reduce(nil) {|v, max| v.fields[field_name] < max ? max : v.fields[field_name] }
67
+ end
68
+
69
+ def filename(model_class)
70
+ @db_dir + "/#{model_class}.db"
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module Menilite
2
+ VERSION = "0.1.0"
3
+ end
data/menilite.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'menilite/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "menilite"
8
+ spec.version = Menilite::VERSION
9
+ spec.authors = ["youchan"]
10
+ spec.email = ["youchan01@gmail.com"]
11
+
12
+ spec.summary = %q{Isomorphic models between client side opal and server side ruby.}
13
+ spec.description = %q{This is isomorphic models for sharing between client side and server side.}
14
+ spec.homepage = "https://github.com/youchan/menilite"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.12"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+
25
+ spec.add_runtime_dependency "sinatra-activerecord"
26
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: menilite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - youchan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-08-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra-activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: This is isomorphic models for sharing between client side and server
70
+ side.
71
+ email:
72
+ - youchan01@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - lib/menilite.rb
86
+ - lib/menilite/client/store.rb
87
+ - lib/menilite/controller.rb
88
+ - lib/menilite/model.rb
89
+ - lib/menilite/router.rb
90
+ - lib/menilite/server/activerecord_store.rb
91
+ - lib/menilite/server/store.rb
92
+ - lib/menilite/version.rb
93
+ - menilite.gemspec
94
+ homepage: https://github.com/youchan/menilite
95
+ licenses: []
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.5.1
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Isomorphic models between client side opal and server side ruby.
117
+ test_files: []