menilite 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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +36 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/menilite.rb +16 -0
- data/lib/menilite/client/store.rb +84 -0
- data/lib/menilite/controller.rb +66 -0
- data/lib/menilite/model.rb +282 -0
- data/lib/menilite/router.rb +104 -0
- data/lib/menilite/server/activerecord_store.rb +116 -0
- data/lib/menilite/server/store.rb +73 -0
- data/lib/menilite/version.rb +3 -0
- data/menilite.gemspec +26 -0
- metadata +117 -0
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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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
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
|
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: []
|